01|模块化问题最早不是“优雅不优雅”,而是代码开始真的互相踩了

今天如果你跟一个前端说“这个项目完全没有模块系统”,他大概率会先皱眉。

因为在现代开发者的直觉里,这几乎已经等于:

  • 依赖关系会乱
  • 命名会撞
  • 文件边界会糊
  • 维护成本会爆

可 JavaScript 最早并不是从“有模块”开始的。

恰恰相反。

它最早的世界里,根本没有语言级模块这回事。

没有 import

没有 export

没有统一的依赖声明机制。

也没有一套官方规定告诉你:

一段代码到底该怎样被封装、暴露、加载、复用。

这不是因为早期开发者不懂组织代码。

也不是因为大家当时不在乎工程性。

更大的原因是:

JavaScript 最初被期待解决的问题太小了,小到还不够逼出正式模块制度。

最早那批脚本常常只是:

  • 表单校验
  • 按钮交互
  • 一点 DOM 操作
  • 一点页面小动画

在这个规模下,很多问题都还能靠“忍一忍”过去。

可一旦网页开始变大,脚本开始变长,页面开始挂上多个库、插件和业务代码,麻烦就会立刻冒出来:

所有东西都在同一个世界里说话。

这就是第一篇真正要讲的起点。

模块化最初不是为了追求抽象美感。

它先是为了止血。


02|最早的 JavaScript 世界,没有模块,只有 script 标签、全局对象和“你最好别写重名”

如果你回到那个时代,最原始的“代码组织方式”其实非常朴素:

把脚本通过 <script> 标签一个一个挂到页面里。

谁先加载,谁先执行。

后面的代码如果想用前面的东西,就默认去全局作用域里拿。

这套办法之所以最初能跑起来,是因为它足够简单:

  • 浏览器本来就会顺序处理脚本
  • 页面作者不需要学额外制度
  • 小脚本之间的关系也往往没那么复杂

可它的代价也同样直接:

所有脚本默认共用一片大院子。

在这片院子里:

  • 变量名可能撞
  • 函数名可能撞
  • 同名库可能互相覆盖
  • 文件顺序一改,代码就可能坏

也就是说,最早的 JavaScript 并不是“没有组织”。

它有组织。

只是它的组织原则非常原始:

谁先上场、谁往全局里放什么、后来的人记不记得避开。

这套原则在脚本很小时还能凑合。

一旦规模上来,它就会开始显得特别脆。

因为它把几个本该明确制度化的问题,全都变成了人肉约定:

  • 依赖靠记忆
  • 加载靠顺序
  • 隔离靠自觉
  • 复用靠命名习惯

这其实已经是在用最昂贵的方式管理复杂度了。


03|所以最早先长出来的,不是模块系统,而是“少往全局泄漏一点”的生存技巧

这一步很关键。

因为 JavaScript 模块化历史最早期的样子,并不是谁突然发明出了一整套成熟制度。

更真实的情况是:

大家先本能地发展出一批“别把事情搞得更糟”的生存技巧。

最朴素的一种,就是命名空间。

既然所有东西都会落到全局对象上,那就别把函数和变量直接裸着扔出去。

先挂到一个总对象下面。

比如:

  • MyApp.utils
  • YAHOO.util
  • myLib.ajax

这样做当然还是在全局里。

但至少从“到处散着一堆名字”,变成了“尽量先收进一个大箱子里”。

这就是早期很多库和团队特别依赖的办法。

YUI 那类实践里,namespace() 这样的工具本身就很能说明问题。

它说明大家当时真正急需的,并不是理论上多漂亮的模块系统。

而是一个最现实的问题:

能不能先少撞点名字。

所以命名空间模式的历史意义很特别。

它当然不是现代模块。

可它至少说明:

JavaScript 社区很早就已经意识到,全局世界不能再这么裸奔下去了。


04|可命名空间并没有真正解决问题,它只是把“全局污染”从碎片化,改成了集中化

这也是第一篇必须讲清楚的一层。

很多人后来回头看命名空间,会觉得它已经算是某种模块前身。

这没错。

但它还远远不够。

因为命名空间真正解决的,主要只是“名字别太容易撞”。

可模块化想解决的问题,从来不只这一层。

比如这些事,命名空间都没有真正处理:

  • 私有状态怎么藏
  • 文件之间的依赖怎么声明
  • 加载顺序怎么自动安排
  • 一段代码怎样只暴露必要接口

换句话说,命名空间只是把很多东西收进一个对象里。

可它并没有改变底层现实:

这套代码仍然默认活在全局世界里。

你还是得靠 <script> 标签顺序把它们摆好。

你还是得先保证上游对象已经存在。

你还是得自己记住哪个文件依赖哪个文件。

如果顺序错了,照样出事。

所以命名空间更像一种早期止痛片。

它让疼痛没有那么直白。

但病根还在。


05|于是更进一步的办法出现了:既然躲不开全局,那至少把真正的内部细节藏起来

这就把故事带到了更重要的一步:

闭包、IIFE,还有后来被反复讲起的 module pattern。

这里最值得注意的,是它们最初也不是“官方模块语法替代品”。

它们更像 JavaScript 社区从语言本身现有能力里,硬挖出来的一种自救办法。

因为 JavaScript 虽然没有模块系统,

但它有一件很关键的东西:

函数作用域和闭包。

这意味着开发者开始意识到:

如果把一段代码包进函数里,再立刻执行,

那函数里的变量就不必直接暴露到全局。

真正需要暴露出去的,只返回那一小部分接口就行。

于是你开始看到这种思路越来越流行:

  • 用函数包住内部实现
  • 用返回对象暴露公共方法
  • 把不想给外面碰的状态留在闭包里

这就是后来大家熟悉的 IIFE 和 module pattern 背后的核心直觉。

它的历史意义非常大。

因为这其实是 JavaScript 社区第一次比较系统地说:

代码组织不该只是“别重名”,还应该包括“哪些东西根本不该被外面看到”。

这是从命名冲突治理,往边界治理迈出的一步。


06|所以 module pattern 真正厉害的地方,不是写法巧,而是它第一次让 JavaScript 世界尝到了“私有边界”的甜头

Douglas Crockford 早年讲 private members,后来各种文章继续推广 module pattern,本质上都在证明一件事:

JavaScript 并不是完全做不到封装,它只是没有把这件事做成语言级制度。

这点特别重要。

因为它解释了为什么在正式模块系统到来之前,社区还能靠很多“半模块化”办法先活那么久。

不是因为问题不严重。

而是因为语言里刚好有闭包这把工具,足够大家先搭起一些临时隔断。

从这之后,很多库、组件和业务代码开始越来越习惯这样想:

  • 不是所有变量都该公开
  • 不是所有函数都该挂全局
  • 对外应该尽量只给最小接口

这在今天看很像常识。

可在当时,它其实已经是一种很大的进步。

因为这意味着 JavaScript 世界终于不再只是“共享一切”。

它开始学会:

有些东西该被藏起来。

而一门语言一旦开始形成这种共识,离真正的模块制度就已经不远了。


07|但即便有了 IIFE 和 module pattern,最麻烦的问题仍然没被解决:依赖关系还是靠人肉维护

这就是第一篇真正要把疼点写透的地方。

很多人讲模块化前史,容易把 IIFE / module pattern 写得像阶段性胜利。

其实还没有。

它们确实改进了隔离。

也改善了封装。

可它们仍然没有真正解决 JavaScript 模块化里最难啃的那块骨头:

依赖管理。

因为你就算把内部状态藏得再好,只要代码还得通过一堆 <script> 标签拼起来,问题就还在:

  • 先加载谁
  • 后加载谁
  • 谁依赖谁
  • 出错时到底是哪一步顺序乱了

这些事情,还是得开发者自己安排。

也就是说,早期“模块模式”解决的主要是:

  • 作用域污染
  • 暴露边界

但真正现代模块系统还会解决的这些事,它还没碰到核心:

  • 依赖声明
  • 依赖解析
  • 加载协调
  • 循环引用语义
  • 可复用分发

所以第一篇里一个很重要的判断就是:

在 CommonJS、AMD 这些正式路线出现之前,JavaScript 社区并不是已经拥有了模块系统,而是只拥有了一批越来越聪明的局部补丁。

这些补丁很有用。

但补丁终究还是补丁。


08|这也就是为什么 Web 应用一变重,旧办法立刻显得不够:它们能藏实现,却藏不住依赖爆炸

如果 JavaScript 一直只写小脚本,也许这些办法还能撑更久。

问题是,Web 没有停在小脚本时代。

页面越来越像应用。

交互越来越多。

插件越来越多。

团队协作越来越复杂。

这时候,原先那些还勉强能忍的问题,会一起放大:

  • 文件越来越多
  • 依赖链越来越长
  • 页面初始化越来越脆
  • 第三方库和业务代码越来越容易互相缠住

于是你会发现,旧世界的组织方式虽然还没彻底崩,

但已经越来越像:

靠一堆经验丰富的人,辛苦地维持一套没有正式宪法的秩序。

这当然撑不了太久。

因为只要规模继续上去,大家迟早会要求更硬的东西:

  • 能不能明确写出我依赖谁
  • 能不能别靠页面顺序猜
  • 能不能不要所有环境都自己发明一套土办法

到这里,模块化需求才真正从“代码整洁偏好”升级成“工程制度刚需”。

这就是第一篇真正的收束点。

JavaScript 并不是先有标准模块,大家再慢慢学着用。

而是先在混乱里被逼到不行,社区才开始认真追问:

我们到底能不能给这门语言一套像样的代码组织制度。


09|为什么模块化最早要解决的,不是优雅,而是别再继续失控

把前面这些线叠一起看,第一篇最重要的判断可以压成一句:

模块化最初不是为了优雅,而是为了让 JavaScript 别再继续裸着长大。

全局变量时代的问题,不只是“不够高级”。

而是它把一门正在变大的语言,长期放在一个几乎没有正式边界管理的环境里。

命名空间、对象字面量、IIFE、module pattern,这些办法都很重要。

因为它们说明社区并没有坐着等官方答案。

大家一直在先自救。

但这些办法也共同暴露出另一件事:

只靠编码技巧,是撑不起长期制度的。

当代码规模、依赖深度、运行环境复杂度一起上来时,

模块化迟早要从“技巧”变成“系统”。

这就是下一篇要进入的历史时刻。

因为最先把这件事真正制度化的,不是浏览器世界。

而是 Node.js 和它背后的 CommonJS


10|早期 JavaScript 不是没有自救,只是还没有制度

模块化江湖的第一篇,最值得记住的,不是“早期开发者很土,只会往全局上挂东西”。

更值得记住的是:

他们其实已经很早就知道全局世界有问题,也发展出了命名空间、闭包、IIFE、module pattern 这些相当聪明的自救办法。

只是这些办法再聪明,也还不足以替代一套真正的模块制度。

因为它们能做的,更多是:

少泄漏一点,少撞一点,少乱一点。

而不是:

正式回答 JavaScript 里的代码,到底该怎样声明依赖、组织边界、安排加载和形成可复用秩序。

所以第一篇如果只记一句,就记这句:

在模块出现之前,JavaScript 不是不会组织代码,而是只能靠全局变量、命名空间、闭包和加载顺序这些临时办法,辛苦地把一门正在长大的语言先硬撑住。


编者注(事实核对):文中关于早期 JavaScript 缺乏原生模块系统的描述,主要依据 MDN 对 JavaScript modules 背景的回顾,其中明确指出大型项目的模块拆分需求是后来才被社区与运行时逐步补上的。关于命名空间与早期社区实践,主要参考 YUI 对全局命名空间与模块注册方式的官方文档。关于 private members、闭包与 module pattern 的历史脉络,主要参考 Douglas Crockford 对 JavaScript 私有成员的早期说明,以及后来围绕 IIFE / revealing module pattern 的社区写作。正文将这些方法概括为“半模块化自救办法”,是基于它们在隔离、封装上有效,但在依赖声明与加载治理上仍然不足这一历史位置做出的总结。


关键人物速览

  • Douglas Crockford:早年持续推动 JavaScript 闭包、private members 和 module pattern 的关键传播者之一。理解“模块化前史为什么先长在编码模式里”,绕不开他。
  • Eric MiragliaYUI 相关实践的关键人物之一。理解命名空间、模块模式怎样在大型前端库里被推广,绕不开这一支线。
  • Christian Heilmann:围绕 module pattern / revealing module pattern 的早期传播者之一。理解社区如何把闭包式封装逐步变成通用写法,他这条线很典型。

参考与延伸阅读

  1. MDN: JavaScript modules
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules

  2. Douglas Crockford: Private Members in JavaScript
    https://www.crockford.com/javascript/private.html

  3. YUI 2: YAHOO Global / namespace
    https://yui.github.io/yui2/docs/yui_2.5.0/docs/YAHOO.js.html

  4. YUI 3: Creating YUI Modules
    https://yuilibrary.com/yui/docs/yui/create.html

  5. Christian Heilmann: Show love to the Module Pattern
    https://christianheilmann.com/2007/07/24/show-love-to-the-module-pattern/

  6. Christian Heilmann: Again with the Module Pattern
    https://christianheilmann.com/2007/08/22/again-with-the-module-pattern-reveal-something-to-the-world


下篇预告:CommonJS 为什么先在 Node 世界把秩序立住。