A

JavaScript 模块化演进史

2026-01-28 12:00

模块化不是“语法糖”,而是工程规模增长后对可维护性、依赖管理与交付方式的必然回答。理解模块化的演进,本质是在理解:代码如何被组织、如何被加载、如何被链接、如何被打包与发布。

JavaScript 作为一种脚本语言,从诞生之初就面临一个棘手问题:如何组织和管理代码?早期 JS 代码往往塞在一个文件里,全局变量满天飞,命名冲突频发,维护起来像噩梦。随着 Web 应用从简单页面转向复杂系统,模块化成了刚需。

你可能经常看到这些词:CommonJS、AMD、CMD、UMD、ESM、tree-shaking、bundle、import maps、package.json 的 exports……它们看似分散,其实在回答同一组问题:

  • 我怎么把代码拆成多个文件并复用?
  • 依赖怎么声明?谁依赖谁?
  • 模块什么时候加载?同步还是异步?
  • 运行时怎么找到模块?靠文件系统、URL,还是打包产物?
  • 生产环境怎么交付:很多小文件,还是一个大包?

为了讲清楚,我们按时间线走一遍模块化的演进史吧。

为什么 JS 需要模块化?

想象一下 2000 年代的 JS 项目:所有代码写在 <script> 标签里或一个大文件,依赖顺序手动控制,变量全局共享。结果?

HTML
<script src="/a.js"></script> <script src="/b.js"></script> <script> window.add = (a, b) => a + b; </script>
  • 命名冲突:不同文件都想要 utilsconfigadd
  • 依赖顺序b.js 依赖 a.js,就必须保证先引入 a.js
  • 难以测试:全局状态很难隔离
  • 难以复用:把一段逻辑拿到另一个项目,要连同“全局约定”一起搬
  • 团队协作苦:大文件容易 merge 冲突,难并行开发。

所以模块化的核心目标就是:

  • 解耦:把代码拆分成多个文件,每个文件只负责一部分功能。
  • 复用:像搭积木一样导入模块。
  • 依赖管理:明确模块之间的依赖关系,避免“全局污染”。
  • 按需加载:提升性能,尤其浏览器端。

从 2009 年起,社区开始标准化模块化,演进出多种规范。下面我们逐一拆解。

早期自救:命名空间与 IIFE(立即执行函数)

命名空间(Namespace)

把所有东西塞进一个对象里,避免污染全局:

JS
window.App = window.App || {}; window.App.math = { add(a, b) { return a + b; }, };

好处:冲突减少了。坏处:依赖仍靠“约定”,模块边界依旧模糊。

IIFE:造一个私有作用域

用函数作用域封装内部实现,只暴露少量 API:

JS
window.App = window.App || {}; window.App.store = (function () { let count = 0; function inc() { count += 1; return count; } return { inc }; })();

这一招很重要:它把“私有实现”与“公共 API”分开了。它几乎就是后续模块系统的雏形。但依赖管理仍然是手工的:如果 store 依赖 math.js,你还是得自己保证加载顺序。当前依赖关系并未成为“语言或运行时的一等公民”,而只是隐藏在代码和 script 顺序中的隐式约定。

在很长一段时间里,依赖顺序并不是被“解决”,而只是通过 script 顺序和构建期约定被“维持”。

CommonJS: Node.js 的“老大哥”

2009 年,Ryan Dahl 发明 Node.js 时,JS 还没有官方模块系统。Node 社区推出 CommonJS(简称 CJS)规范,迅速成为服务器端事实标准。npm 生态的爆炸式增长,全靠它支撑。它的核心用法是:

  • 导出:用 module.exportsexports.xxx
  • 导入:用 require('path')
  • 支持默认导出module.exports = add;const add = require('./math');
  • 支持动态路径require(./${fileName});
JS
// math.js(导出) const add = (a, b) => a + b; module.exports = { add }; // 或 exports.add = add; // main.js(导入) const { add } = require('./math'); // 相对路径或 npm 包名 console.log(add(2, 3)); // 5

导入流程

CommonJS 的 require 不是简单复制代码,而是有完整的加载流程。这设计源于 Node.js 的文件系统操作,确保高效和可靠。

  1. 解析路径:require 先解析模块路径(相对/绝对/npm 包)。Node 用 module.paths 数组查找(从当前目录向上到 node_modules)。
  2. 加载文件:找到文件后,Node 用 fs 模块读取文件内容。
  3. 执行代码:读取到代码后,Node 用 vm 模块在沙箱环境执行。
  4. 缓存模块:执行完成后,Node 把模块对象缓存起来,后续 require 直接返回缓存。 验证缓存:
JS
// counter.js let count = 0; module.exports = { increment: () => ++count, getCount: () => count }; // main.js const mod1 = require('./counter'); const mod2 = require('./counter'); // 第二次 require,读缓存 mod1.increment(); console.log(mod1.getCount()); // 1 console.log(mod2.getCount()); // 1(共享同一对象)

对象共享问题与解决方案

由于导出的是 module.exports 的引用(指向堆中对象),多次 require 得到同一对象。修改一方会影响所有导入处——这叫“共享状态副作用”。

JS
// shared.js module.exports = { value: 0 }; // main1.js const shared = require('./shared'); shared.value = 42; // main2.js const shared = require('./shared'); console.log(shared.value); // 42(被 main1 修改)

**为什么会有这个问题?**因为 CommonJS 返回的是 module.exports 的引用,多次 require 得到的是同一个对象实例,因此修改会在所有引用方之间共享。 解决方案:避免直接修改导出对象,而是导出“不可变值”(如函数、对象冻结等)。

为什么 CommonJS 要这样实现?

CommonJS 的设计背景是 Node.js 服务端:

  • 同步加载:Node 是服务器环境,文件 IO 快,阻塞无大碍。简化 API,无需回调/Promise。
  • 运行时加载:允许动态(如环境变量决定导入),灵活性高。
  • 长生命周期进程:Node 进程一般会长时间运行,模块也会长期存在。
  • 缓存机制:性能优化,避免重复 IO/编译。为什么值拷贝/引用共享?平衡效率和简单——导出对象引用节省内存,但需开发者小心。
  • 需要共享:
    • 全局状态(如配置、缓存)
    • 跨模块共享(如事件总线)
    • 连接池
    • logger
    • 进程级状态 例如:
JS
// db.js module.exports = { pool: createPool(), };

缺点 & 2026 年现状

  • 不适合浏览器(同步阻塞)。
  • 无 tree-shaking(死代码消除),打包体积大。
  • 2026 年:遗留项目和老 npm 包还在用。新 Node 项目建议 ESM,但 CJS 不会灭绝(兼容性太强)。
    • Node.js 的核心哲学之一是:Don't break the ecosystem。 CJS 是 Node 最底层、最稳定的 ABI 之一,一旦废弃,会造成灾难级破坏所以Node 永远会继续支持 CommonJS。

AMD:浏览器端的“异步救星”

浏览器不能像 Node 那样同步加载(会冻结页面),2010 年左右 RequireJS 和 SeaJS 推出 AMD(Asynchronous Module Definition)规范,专治浏览器模块化痛点。AMD 曾风靡一时,尤其在 Backbone/Angular 1.x 时代。

核心语法:

  • define:定义模块。
  • require:加载模块。
JS
// math.js define(function () { // 或 define(['dep'], function(dep) {}) const add = (a, b) => a + b; return { add }; // 返回导出 }); // main.js define(['./math'], function (math) { // 依赖数组前置 console.log(math.add(2, 3)); // 5 }); // 或全局 require 加载 require(['./math'], function (math) { ... });

特点与原理

  • 异步加载:依赖通过回调加载,不阻塞主线程。
  • 依赖前置:在 define 中声明依赖,加载器会提前拉取。
  • 提前执行:依赖模块加载后立即执行。
  • 需要 loader(如 RequireJS)注入 define/require 函数。

但是因为:

  • 语法繁琐(回调地狱)。
  • 不支持服务器端。
  • 打包工具(如 Webpack、Rollup)兴起,AMD 被内置模块系统取代。

CMD: 国服路线(SeaJS 的风格)

CMD 是淘宝前端团队(玉伯)在推广 Sea.js 时提出的规范(2011–2013 年左右)。它本质上是 AMD 的“中国变种”,针对 AMD 的“依赖前置 + 提前执行”痛点做了优化,更接近 CommonJS 的“就近依赖 + 延迟执行”风格。

CMD 当时在阿里系内部和国内很多团队非常流行(Sea.js vs RequireJS 是当年经典对决),但国际影响力较小,最终被 ESM 取代。

核心语法:

  • define:定义模块,但不强制依赖前置。
  • require:在 factory 函数内部动态 require(延迟执行)。
JS
// math.js define(function(require, exports, module) { var add = function(a, b) { return a + b; }; // 内部动态 require(延迟加载) var multiply = require('./multiply'); // 只在需要时加载 exports.add = add; module.exports = { add: add }; // 支持 module.exports 风格 }); // main.js define(function(require) { var math = require('./math'); // 就近依赖 console.log(math.add(2, 3)); // 5 });

特点与原理

  • 就近依赖 + 延迟执行:依赖在 factory 函数内部写 require,只有真正用到时才加载/执行。
    • AMD:依赖前置(define(['dep1', 'dep2'])),所有依赖提前下载 + 执行。
    • CMD:依赖就近(require('dep')),按需加载 + 执行。
  • 异步加载:适合浏览器,Sea.js 提供 loader 实现。
  • 兼容 CommonJS 风格:factory 函数参数是 require、exports、module(类似 Node 的 wrapper)。
  • 循环依赖:处理更友好(延迟执行让循环依赖更容易解决)。

CMD vs AMD

  • AMD:依赖前置(先声明 deps)
  • CMD:依赖就近(用到再 require)

两者都服务于浏览器异步加载的时代,只是取舍不同:AMD 更利于提前并行加载,CMD 更符合写代码的直觉。

UMD:万能兼容方案

当时的现实是:同一个库要同时跑在:

  • 浏览器脚本(全局变量)
  • AMD(RequireJS)
  • CommonJS(Node)

UMD(Universal Module Definition)出现,作为“万金油”兼容多种环境。

核心语法就是一个模板,判断环境自动切换:

JS
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD 模式 define(['dependency'], factory); } else if (typeof module === 'object' && module.exports) { // CommonJS 模式 module.exports = factory(require('dependency')); } else { // 浏览器全局模式 root.myModule = factory(root.dependency); } }(this, function (dependency) { // 模块实现 return { add: (a, b) => a + b }; }));

它的特点与原理是:

  • 兼容性极强:浏览器全局、AMD、CJS 一网打尽。
  • 本质是 wrapper:运行时检测环境。

为什么 现在不需要 UMD 了?

  • 打包工具已普及:Webpack/Rollup 会自动处理多环境兼容
  • ESM 成为主流:现代库直接输出 ESM + CJS 双包(通过 exports 字段)
  • 构建工具智能化:tsup、unbuild 等工具一键生成多格式产物

如果你还在手写 UMD,说明你的工具链该升级了 😄

ESM(ES Modules):现代 JS 的官方标准(异步、静态分析

2015 年 ES6 标准引入 ESM(ES Modules),作为官方模块系统。浏览器从 2017 年起原生支持,Node.js 在 2019–2020 年稳定兼容(v12+)。ESM 结束了模块化战争。它的核心语法是:

  • 导出:export / export default。
  • 导入:import(静态)或 import()(动态)。
JS
// math.mjs export const add = (a, b) => a + b; // 命名导出 export default function multiply(a, b) { return a * b; } // 默认导出 // main.mjs import { add } from './math.mjs'; // 静态导入 import multiply from './math.mjs'; // 默认导入 import * as math from './math.mjs'; // 全部导入 // 动态导入(返回 Promise) const { add: asyncAdd } = await import('./math.mjs');

它的关键特点是:

  • 静态分析:import/export 必须在模块顶层(不能在 if/函数里),编译时(打包工具阶段)就能确定整个依赖树。
  • 异步加载:浏览器 <script type="module"> 非阻塞,Node 也支持异步解析。
  • 值的 live binding:导出是实时引用,修改导出值会立即影响所有导入处(不像 CJS 的快照拷贝)。
  • 严格模式默认:自动开启 strict mode。
  • top-level await:模块顶层直接 await(Node 14+)。
  • 循环依赖:支持,但静态分析让问题更易暴露(需小心设计)。

那么与 AMD 和 CJS 比好在哪?

加载方式:同步 vs 异步 vs 异步 + 静态

CommonJS 对服务端友好但是浏览器致命,因为我们要知道 CommonJS 并不是“不阻塞”,而是“阻塞成本在服务端是可控的、可一次性消化的”。它阻塞的是“进程初始化阶段”或“首次 require 阶段”,而不是而不是高频的请求处理路径。在 Node 中:

JS
// app.js const express = require('express'); const router = require('./router'); const db = require('./db'); const app = express(); app.listen(3000);

这些 require() 只在进程启动时执行一次,发生在服务监听端口之前,请求还没进来之前。这段时间的阻塞没有用户请求和 SLA 压力,这是一次性成本,不是运行时成本。并且 CJS 会缓存模块导出,后续 require 不会重复执行。在 Node 中主要的瓶颈在于:

  • HTTP 请求
  • 数据库查询
  • RPC 调用
  • 网络 IO 相比之下几百个 JS 文件同时加载的启动延迟是可以接受到,但是在浏览器中 <script> 同步加载会阻塞 UI 线程,再加上可能会出现网络延迟、模块数量过多和每次刷新可能会重新加载,用户体验会直线下降这是无法接受的。

你可能会问那如果在请求处理中 require 呢?

JS
app.get('/api', (req, res) => { const heavy = require('./heavy'); // ❌ res.send(heavy.run()); });

这里的主要问题是每次请求都会触发 require ,即使有缓存也会出现路径解析等同步成本,这会导致阻塞事件循环,并发下明显拖慢吞吐量。所以永远不要在高频请求路径中使用 require

服务端可以接受“慢启动”,浏览器不能接受“慢首屏”。

AMD 专为浏览器而生,它通过异步加载解决了“阻塞”问题。它的核心逻辑是:“先声明,后执行”

JS
// 浏览器加载过程 // 1. 解析依赖数组 ['./a', './b'] // 2. 动态创建 <script> 标签异步下载 a.js 和 b.js // 3. 等两个文件都下载并执行完,再回调 factory 函数 define(['./a', './b'], function(a, b) { return { run: () => a.do() + b.do() }; });

在那个没有打包工具的年代,AMD 是浏览器端的唯一答案。但它也有明显的代价:

  • “包裹感”太重:每个文件都要套一层 define,代码显得臃肿。
  • 管理负担:依赖数组和 factory 参数必须严格一一对应,错位一个就是灾难。
  • 网络开销:模块越多,HTTP 请求越多。在 HTTP/1.1 时代,这会导致明显的延迟。

AMD 解决了“不阻塞”,但带来了“模板代码”和“复杂的异步编排”成本。

ESM 是终极方案,它在语言层面实现了“异步加载 + 静态分析”。

与 CJS 和 AMD 不同,ESM 的加载分为三个阶段:构建(Parsing)实例化(Instantiating)运行(Evaluating)

  1. 构建:浏览器从入口文件开始,静态分析 import 语句,递归生成一张完整的“依赖关系图”。注意:此时代码还没运行,只是在拉取文件。
  2. 实例化:根据依赖图,在内存中分配空间,并将导出和导入“链接”在一起(即 Live Binding)。
  3. 运行:最后才真正执行代码。
JS
// ESM 的优势:编译时确定依赖 import { add } from './math.js'; // 打包工具(如 Vite/Rollup)通过静态分析, // 知道你只用了 add,没用 subtract,从而把 subtract 删掉(Tree Shaking)。

ESM 的伟大之处在于:它让模块系统从“运行时的逻辑”变成了“编译时的结构”。

ESM 消费 CJS:天然的互通性

在 Node.js 环境中,ESM 引入 CJS 是被官方完美支持的,因为 ESM 诞生之初就考虑到了生态兼容。

JS
// 在 ESM 模块中 (app.mjs) import fs from 'node:fs'; // 引入内置 CJS 模块 import path from 'path'; // 引入第三方或本地 CJS // 甚至可以直接解构 import { readFileSync } from 'node:fs';

适配逻辑与问题:

  1. 默认导出映射:CJS 的 module.exports 会被映射为 ESM 的 default 导出。
  2. 命名导出识别:Node.js 会通过静态分析(cjs-module-lexer)尝试识别 CJS 中的 exports.xxx,让你能直接 import { xxx }。但如果 CJS 是动态生成的导出,静态分析会失败,此时你只能通过 import pkg from '...' 然后再解构。
  3. 不可混用 require:在 ESM 文件中,require 关键字是不存在的。如果你非要用,得通过 createRequire(import.meta.url) 手动造一个出来。

维度CommonJS (CJS)AMD (RequireJS)ESM (ES Modules)谁完胜?(2026)
加载方式同步阻塞异步回调异步 + 静态分析(非阻塞 + 高效)ESM
依赖解析时机运行时(动态灵活)运行时(前置)编译时(tree-shaking 神器)ESM(打包体积最小)
执行时机同步顺序提前执行(可能浪费)延迟 + 可预测ESM
tree-shaking 支持×(无法静态分析)×✓(极致优化)ESM
语法简洁度中等(require 简单)差(回调地狱)优秀(import/export 最干净)ESM
浏览器原生支持×(必须打包)×(必须 loader)✓(<script type="module">ESM
Node 支持原生默认×原生 + 双端统一ESM
动态导入支持(条件 require)支持支持(import() Promise)ESM(更现代)
值拷贝/引用值快照(拷贝)值拷贝live binding(实时引用)ESM(更灵活)
top-level await××✓(模块顶层 await)ESM
2026 年生态占比遗留项目 + 老 npm 包(~30%)几乎 0新项目 95%+,npm 主导ESM 完胜

浏览器原生 ESM:不仅仅是 import

虽然我们习惯了用 Webpack/Vite 打包,但现代浏览器已经原生支持 ESM。你只需要在 <script> 标签上加一个属性:

HTML
<script type="module"> import { init } from './app.js'; init(); </script>

但原生 ESM 在浏览器中运行有几个硬性规则

  1. CORS 限制:模块脚本必须通过 CORS 协议加载。如果你直接双击打开 HTML(file:// 协议),浏览器会报错。
  2. 路径必须明确:不能写 import { x } from 'lodash'(这叫 Bare Specifier),必须写完整的路径 ./lodash.js 或 URL。
  3. 默认延迟执行type="module" 的脚本默认带有 defer 属性,会等待 HTML 解析完再运行。

Import Maps:浏览器的“别名表”

为了解决上面提到的“不能写包名”的问题,W3C 引入了 Import Maps。它允许你在 HTML 中定义一个映射表:

HTML
<script type="importmap"> { "imports": { "utils": "/js/shared/utils.js", "lodash": "https://cdn.jsdelivr.net/npm/[email protected]/lodash.js" } } </script> <script type="module"> import { debounce } from 'lodash'; // 现在可以这样写了! </script>

Node.js 的 ESM 转型:一场长跑

Node.js 对 ESM 的支持经历了一个漫长且痛苦的过程,因为 CJS 实在太根深蒂固了。在 2026 年的今天,你主要需要掌握这三点:

1. type: module

package.json 中设置 "type": "module" 后,该项目下所有的 .js 文件都会被视为 ESM。如果你想保留几个 CJS 文件,可以改后缀为 .cjs

2. exports 字段:现代包的守门人

传统的 main 字段只能定义一个入口。现在的包建议使用 exports,它不仅能定义入口,还能根据环境自动切换:

JSON
{ "name": "my-lib", "exports": { ".": { "import": "./dist/index.mjs", // ESM 加载时用这个 "require": "./dist/index.cjs" // CJS 加载时用这个 }, "./utils": "./dist/utils.js" // 支持子路径导出 } }

3. 双包陷阱(Dual Package Hazard)

当一个 npm 包同时提供 ESM 和 CJS 两个版本时,如果你的项目通过不同方式引入了它,可能会导致同一个包在内存中存在两个完全独立的实例。这就是所谓的"双包陷阱"。

JS
// 某个库 my-state-lib 的 package.json { "name": "my-state-lib", "exports": { "import": "./dist/index.mjs", // ESM 入口 "require": "./dist/index.cjs" // CJS 入口 } } // my-state-lib 的实现(简化版) let globalCounter = 0; export function increment() { globalCounter++; } export function getCount() { return globalCounter; } // 使用时 // a.js (CJS 模块) const { increment } = require('my-state-lib'); // 加载 CJS 版本 increment(); increment(); // b.mjs (ESM 模块) import { getCount } from 'my-state-lib'; // 加载 ESM 版本 console.log(getCount()); // 期望输出 2,实际输出 0 ❌

为什么会这样:

  • require('my-state-lib') 走的是 ./dist/index.cjs,在 CJS 模块缓存中保存了一个实例
  • import 'my-state-lib' 走的是 ./dist/index.mjs,在 ESM 模块缓存中保存了另一个实例
  • 两个缓存系统是完全隔离的,所以 globalCounter 有两份,互不影响

这会导致一些比如常见的单例模式的库出现问题:

  • 状态管理库(zustand):如果一个组件通过 CJS 引入,另一个通过 ESM 引入,它们会操作两个不同的 store
  • 数据库连接池: 可能创建两个连接池,浪费资源且难以调试
  • 事件总线: 一个模块发布事件,另一个模块订阅不到(因为是两个实例)

而它目前的解决方案有:

  • 库作者: 建议只提供 ESM 版本,或者在 package.json 中声明 exports 字段,明确指定 CJS 入口(如 ./dist/index.cjs)。
  • 使用者: 可以通过 import 引入 ESM 版本,或者使用 require(esm) 引入 CJS 版本。

核心原则:双包陷阱本质是"模块系统不统一"导致的缓存隔离。最彻底的解决方案是全面 ESM 化,但在过渡期,库作者和使用者都需要额外小心。


现代转折点:Node.js 支持 require(esm)

在很长一段时间里,CJS 项目无法直接 require 一个 ESM 模块,这被称为 Node.js 生态中最深的沟壑。如果你尝试这么做,Node 会抛出著名的 ERR_REQUIRE_ESM 错误。

但在 2024 年,Node.js 22(以及 v20.19.0+)终于支持了同步的 require(esm)。这是一个划时代的改进,因为它允许 CJS 项目直接消费 ESM 包,而不需要强迫整个项目重构。

它是如何适配的?

Node.js 实现了一个“同步 ESM 图”加载器。当你执行 require('./module.mjs') 时:

  1. Node 会解析该模块及其所有依赖。
  2. 它会同步执行这些模块(就像 CJS 一样)。
  3. 最终返回一个 Module Namespace Object(模块命名空间对象)。
JS
// CJS 脚本 const math = require('./math.mjs'); console.log(math.add(1, 2)); // 正常运行!

存在什么问题与限制?

虽然这看起来像魔法,但它有几个硬性的“坑”:

  1. Top-Level Await (TLA) 是死穴: 如果被引入的 ESM 模块(或者它的任何子依赖)使用了 await 顶层声明,require 会立即报错 ERR_REQUIRE_ASYNC_MODULE。因为 require 本质是同步的,它无法等待异步操作。这意味着 带有异步初始化逻辑的 ESM 包仍然无法被 CJS 直接引入

  2. 导出结构的微小差异require(esm) 返回的是命名空间对象。这意味着如果 ESM 只有 export default,在 CJS 里你必须通过 require('./file.mjs').default 来访问。这与习惯上的 require 行为略有不同,可能导致一些心智负担。

  3. 生态系统的版本滞后: 虽然 Node 22 支持了,但很多生产环境还在运行 Node 16 或 18。这意味着库作者如果想兼容这些旧版本,仍然需要提供 CJS 版本,require(esm) 并不能立刻消灭“双包”现象。

Node.js 22 引入了受限的同步 require(esm) 能力,主要用于缓解 CJS 遗留项目的迁移压力,但并不改变 Node 对 ESM 作为未来主流的推荐方向。


总结:如何选择模块化方案?

演进史告诉我们,技术的发展总是在“灵活性”与“确定性”之间寻找平衡。

  • 如果你在写应用(业务代码): 始终使用 ESM。配合 Vite、Webpack 或 TS,这是目前最现代、工具链支持最好的选择。

  • 如果你在写库(供他人使用): 优先输出 ESM 以支持 Tree Shaking。如果需要兼容旧的 Node 生态,利用 exports 提供 CJS 降级方案。

  • 如果你在做极简的原型或脚本: 浏览器端可以直接用 原生 ESM + Import Maps;Node 端可以直接写 .mjs 或在 package.json 里开 type: module

模块化的核心不在于你用 require 还是 import,而在于你是否建立了一个**“可预测、可追踪”**的依赖网络。


如果你看到这里,谢谢你花时间阅读。希望这篇演进史能帮你理清那些看似混乱的 JS 模块化术语。