考察点:
- CommonJS规范是什么?"模块"指的是什么单元?(函数?代码段?文件?文件夹?npm包?)
- AMD规范与CMD规范是什么?其对应的实现是什么?为什么要提出AMD与CMD?
- AMD与CMD的区别?
- CommonJS规范的require与ES6 module规范的import有什么不同?
面试题
循环依赖问题
以下代码的执行结果为何?原理?
// 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 执行完毕');
// 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);
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
CommonJS规范
内容:
- 规范规定文件为模块的基本单位。
- 规定了4个变量的实现:
- module[object]: 每个模块内部,module变量代表当前模块。模块有自己的作用域,在模块中定义的变量、函数、类都是私有的,其他模块不可见。(通过立即执行函数实现-闭包特性)
- exports: 模块对外的接口,加载模块实际上是加载模块的exports属性
- require: 用于加载模块,模块可以多次加载,但是只会在第一次加载时运行一次(浅复制),然后运行结果就会被缓存,以后再加载模块,就会直接读取缓存结果。想让模块再次运行,必须清除缓存。
- global: 全局对象,所有模块都可以进行访问。
- 规范加载模块是同步的
- 模块加载输出的时值的浅拷贝,内部的改变不影响输出的值
- 文件加载机制:
- 用于加载文件时,默认后缀名为'.js',找不到时,会以'.json','.node'再去搜索
- 若路径以'/'开头,则表示加载绝对路径文件(程序启动路径)
- 若路径以'.'开头,则表示相对路径
- 若路径以其他字符开头,则表示加载的是默认提供的核心模块(node_modules中的模块,向上各级查找)
- 目录加载规则
- 加载目录时,会自动查看该目录的
package.json
文件,然后加载文件下main
字段指定的入口文件 - 若该文件没有
package.json
或main字段
,则会加载目录下的index.js
或index.node
文件
- 加载目录时,会自动查看该目录的
- 模块的缓存: 缓存的模块保存在
require.cache
中,可以使用delete require.cache[moduleName]
清除缓存 - 循环依赖的加载: 当发生循环加载时,会先加载不完成的版本,以便加载能往下继续进行
interface module {
id: number[模块的标识符,通胀是带有绝对路径的模块文件名],
filename: string[模块的文件名,带有绝对路径],
loaded: boolean[表示模块是否已经完成加载],
parent: object<any>[调用该模块的模块,可以用途判断是否为根模块,或使用require.main判断],
children: object<any>[模块依赖的其他模块],
exports: object<any>[模块对外输出的值]
}
加载值为浅拷贝(例子也可以使用setTimeout实现)
// a.js
var counter = 1;
var countObj = { value: 1 }
function incCounter() {
counter++;
countObj.value ++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
countObj: countObj,
};
// main.js
var counter = require('./lib').counter;
var counterObj = require('./lib').countObj;
var incCounter = require('./lib').incCounter;
console.log(counter, countObj.value); // 1 1
incCounter();
console.log(counter, countObj.value); // 1 2
循环加载:
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
=>
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
main.js a2
main.js b2
参考资料:
AMD规范
由来: 由于commonJS规范主要用于node实现,而node端,模块一般是储存在本地的,所以读取和执行较快,而在浏览器端,则不太可能,所以需要异步的加载模块后再调用相关实现。 解决问题:
- 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
- js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长 实现: require.js、Dojo.js
/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})
CMD规范
实现: sea.js 与ADM的差别: AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
// 定义模块 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});
ES6 module规范
ES6 在语言标准的层面上,实现了模块功能,旨在成为浏览器和服务器通用的模块解决方案。 其由export与import命令组成,其大部分功能类似于commonJS。 详细资料
ES6 module 与 commonJS 的区别
- 语法不同,详细参见具体语法。
- commonJS输出的是一个值的拷贝,而ES6 输出的是值的引用。
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块
- commonJs模块是运行时加载,而ES6 是编译时输出接口
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。