单纯的文件划分
最早开始的模块化基于简单的相互约定,用文件的形式进行划分,每一个 <script> 标签代表一个模块引入进 HTML 文件中。
<!-- 假定模块a下有函数foo() --><script src="module-a.js"></script><!-- 假定模块b下有变量bar --><script src="module-b.js"></script><script>foo() // 在引入多个文件的情况下,foo()可能存在命名的冲突bar = 'foo' // 数据变量可能会被更改且无法知道是由谁更改的</script>
此类模块化在面对大型开发时暴露的缺点包括:
- 无法确定模块间彼此的依赖关系。
- 无私有空间,所有方法与变量都会污染全局作用域,容易出现命名冲突/被意外修改的情况。
- 无法明确模块内容(无法简单判断出
foo()是来自于a模块还是b模块)。
针对以上缺点,产生了一些派生的改进。
命名空间形式
针对命名冲突/无法明确方法来源的问题,引入了命名空间的形式:每个模块只对外部暴露一个Object对象,所有需要的数据与方法都写在这个对象内。
<!-- 模块foo --><script>window.foo = {data:'foo',method: function() {alert(`${this.data}'s method`)}}</script><!-- 模块bar --><script>window.bar = {data:'bar',method: function() {alert(`${this.data}'s method`)}}</script><script>// 通过引入模块名的命名空间解决了命名冲突的问题foo.method() // alert "foo's method"bar.method() // alert "bar's method"// 内部数据仍然不受控的会被修改和重写foo.data = 'bar'</script>
如上所示,只解决了命名会冲突的问题的模块化虽然有所进步,但依旧是不够的:外部代码依旧可以对本该私有的模块内部数据进行修改。
IIFE与有带依赖的IIFE
命名空间形式所存在的问题在于其无法拥有一个私有的作用域,而无法拥有私有成员对于模块的自组织无疑是致命的。
IIFE的定义、特性与用法
定义
立即执行函数表达式(IIFE,Immediately-Invoked Function Expression)是一个拥有自己的词法作用域的自执行函数,其执行的时间就是这个函数被定义的时间。
A Javascript function that runs as soon as it is defined. -MDN
特性与用法
var result = (function () {var name = 'foo';return name;})();result; // "foo"name; // "Uncaught ReferenceError: name is not defined"
由以上的代码可以总结出 IIFE 在模块化场景下的最大优势:函数拥有自己独立的词法作用域,使用这个特性可以实现私有成员,且不会对全局作用域污染。
IIFE形式
;(function () {var name = 'foo'function method () {console.log(`${name}'s method`)}window.moduleA = {method}})()
可以看出,这种形式只是在前面命名空间形式上包裹了一层 IIFE 函数,但通过这种包裹,我们已经实现了模块化中的私有成员需求,对比最初的文件划分形式,到此已经解决了命名冲突与私有空间的问题,只剩下一个缺点:
如何解决模块之间相互依赖的问题。
带依赖的IIFE形式
jQuery 时代的插件注册是这种形式最常见的地方,通过指定传入 IIFE 的参数,较为优雅的体现了模块间彼此依赖的关系,同时也符合代码设计中的单一职责原则。在出现具体的模块化标准之前,该形式已经是刀耕火种时代的终极形式。
(function($){$.method();})(jQuery);
在痛点解决之后,下面要解决的的就是更多满足开发者的爽点,提升效率的阶段,为了实现解决各类开发维护的痛点,模块标准化的出现了。
