JS
模块化
1、模块化发展历程
IIFE
自执行函数AMD
使用requireJS
来编写模块化(依赖必须提前声明好。)CMD
使用seaJS
来编写模块化(支持动态引入依赖文件。)CommonJS
nodeJs
中自带的模块化UMD
兼容AMD
、CommonJS
语法webpack(require.ensure)
:webpack 2.x
版本中的代码分割ES Modules
:ES6
引入的模块化,支持import
来引入另一个js
script
标签type="module"
2、AMD
和 CMD
的区别
AMD
和CMD
最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块
AMD
推崇依赖前置,在定义模块的时候就要声明其依赖的模块CMD
推崇就近依赖,只有在用到某个模块的时候再去require
3、CommonJS
规范的特点
- 所以代码都是运行在模块作用域中,不会污染全局作用域
- 模块是同步加载的,只有引入的模块加载完成,才会执行后面的操作
- 模块在首次执行后就会缓存,再次加载只返回缓存的结果
CommonJS
输出的是值的拷贝,模块内部再次改变也不会影响这个值(引用类型和基本类型有区别)
4、ES6 modules
规范有什么特点
- 输出使用
export
- 引入使用
import
- 可以使用
export ... from ...
来达到一个中转的效果 - 输入的模块变量是不可重新赋值的。只是个可读引用,但是可以改写属性
export
和import
命令处于模块顶层,不能位于作用域内,处于代码块中,没法做静态优化,违背了ES6
模块的设计初衷import
有提升效果,会提升到整个模块的头部,首先执行Babel
会把export/import
转化为exports/require
的形式,所以可以使用exports
和import
5、CommonJS
和 ES6 Modules
规范的区别
CommonJS
模块是运行时加载,ES6Modules
是编译时加载CommonJS
输出值的拷贝,ES6Modules
输出值的引用(模块内部改变会影响引用)CommonJS
导入模块可以是一个表达式(是使用require()
引入),ES6Modules
导入只能是字符串CommonJS
中 this 指向当前模块,ES6Modules
中this
指向undefined
ES6Modules
中没有arguments
、require
、module
、exports
、__filename
、__dirname
这些顶层变量
6、如何异步进行模块的加载
AMD
和 CMD
支持异步加载模块
7、开发一个模块需要考虑哪些问题?
- 安全性
- 封闭性
- 避免变量冲突
- 隔离作用域
- 公共代码的抽离
8、node require(X)
引入的处理顺序是什么样的?
- 如果
X
是内置模块,返回该模块,不再继续执行; - 如果
X
以'./'、'/'、'../'
开头,将根据X
所在的父模块,确定X
的绝对路径:
a. 将X
当成文件,依次查找,存在,返回该文件,不再继续执行;
b. 将X
当成目录,依次查找目录下的文件,存在,返回该文件,不再继续执行; - 如果
X
不带有路径:
a. 根据X
所在的父模块,确定X
可能的安装目录
b. 依次在每个目录中,将X
当成文件名或者目录名加载 - 抛出
not found
错误
9、node 中相互引用
有个 a.js
和 b.js
两个文件,互相引用
1. CommonJS
{
id: '...',
exports: { ... },
loaded: true, parent: null, filename: '', children: [], paths: []
}
复制代码
CommonJS
的一个模块,就是一个脚本文件。require
命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。以后需要用到这个模块的时候,就会到 exports
属性上面取值。即使再次执行 require
命令,也不会再次执行该模块,而是到缓存之中取值。
CommonJS
重要特性是加载时执行,脚本代码在 require
时,全部执行。
CommonJS
的做法是,一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
复制代码
a.js
脚本先输出一个done
变量,然后加载另一个脚本文件b.js
。注意,此时a.js
代码就停在这里,等待b.js
执行完毕,再往下执行。b.js
执行到第二行,就会去加载a.js
,这时,就发生了”循环加载”。系统会去a.js
模块对应对象的exports
属性取值,可是因为a.js
还没有执行完,从exports
属性只能取回已经执行的部分,而不是最后的值。a.js
已经执行的部分,只有一行。
exports.done = false;
复制代码
- 因此,对于
b.js
来说,它从a.js
只输入一个变量done
,值为false
。 b.js
接着往下执行,等到全部执行完毕,再把执行权交还给a.js
。于是,a.js
接着往下执行,直到执行完毕。我们写一个脚本main.js
,并运行,验证这个过程。
// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
// 运行
// 在 b.js 之中,a.done = false
// b.js 执行完毕
// 在 a.js 之中,b.done = true
// a.js 执行完毕
// 在 main.js 之中, a.done=true, b.done=true
复制代码
- 上面的代码证明了两件事。一是,在
b.js
之中,a.js
没有执行完毕,只执行了第一行。二是,main.js
执行到第二行时,不会再次执行b.js
,而是输出缓存的b.js
的执行结果,即它的第四行。
2. ES6
ES6
模块的运行机制与 CommonJS
不一样,它遇到模块加载命令 import
时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。
ES6
模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。ES6
模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块。
ES6
根本不会关心是否发生了”循环加载”,只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
return n != 0 && even(n - 1);
}
复制代码
按照 CommonJS
规范,是没法加载的,是会报错的,但是 ES6
就可以执行。
之所以能够执行,原因就在于 ES6
加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。
$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
复制代码
上面代码中,参数 n
从 10
变为 0
的过程中,foo()
一共会执行 6
次,所以变量 counter
等于 6
。第二次调用 even()
时,参数 n
从 20
变为 0
,foo()
一共会执行 11
次,加上前面的 6
次,所以变量 counter
等于17
。