面试官问: 解释一下JS中的模块化
刚开始接触前端时, 看见那几个名词就觉得太高大上了, 都不知道是什么, 慢慢的我发现, 其实大多数时候那些专有名词都是给很简单的概念一个好听的名字罢了, 比如:
- react中的高阶组件(不就是组件作为参数进行一定的装饰再返回一个组件嘛)
- 比如模块化, 就是JS中将不同功能的代码封装在不同的文件中, 再互相引用时不会发生命名冲突的一种思想, 大多数情况下, 一个文件就是一个模块
模块化这一思想的不同实现, 有多种方案, 主要有以下几种
下面展开来说
CommonJS
CommonJS
是nodejs
中使用的模块化规范
在 nodejs
应用中每个文件就是一个模块,拥有自己的作用域,文件中的变量、函数都是私有的,与其他文件相隔离。
CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分。
- 模块引用
var math = require('math');
在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。
-
模块定义
在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式
在另一个文件中,我们通过require()
方法引入模块后,就能调用定义的属性或方法了 -
模块标识
模块标识其实就是传递给require()
方法的参数,它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js
模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。如图所示,每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落。
事实上,在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了
(function (exports, require, module, __filename, __dirname) {\n,在尾部添加了\n});
这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()
方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function
对象。
最后,将当前模块对象的exports
属性、require()
方法、module
(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()
执行。
ES6模块化
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD
规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD
模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
ES6 module
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
ES6模块功能主要由两个命令构成:export和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
二者都可以通过as
来给要导出或者引入的变量重命名
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
//上面两种写法都会报错,因为没有提供对外的接口
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
//export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
//上面代码输出变量foo,值为bar,500 毫秒之后变成baz
//这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新
export default
export default
是模块的默认导出
// 第一组
export default function crc32() { // 输出
// ...
}
import crc32 from 'crc32'; // 输入
// 第二组
export function crc32() { // 输出
// ...
};
import {crc32} from 'crc32'; // 输入
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import
命令后面才不用加大括号,因为只可能唯一对应export default
命令。
对于CommonJS和ES6模块
CommonJS
模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS
模块是运行时加载,ES6 模块是编译时输出接口。CommonJS
模块的require()
是同步加载模块,ES6 模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
上面说CommonJS
模块输出的是值的拷贝, 其实是一个浅拷贝, 也就是说, 如果变量是一个原始值那么就不会随着模块中变量变化而变化, 如果是一个引用值, 那么拷贝的是引用地址, 共享同一个数据, 还是会获取到最新的值
//a.js
var counter = {
a: 3,
}
function incCounter() {
counter.a++
}
module.exports = {
counter: counter,
incCounter: incCounter,
}
//main.js
var mod = require('./a.js')
console.log(mod.counter.a)
mod.incCounter()
console.log(mod.counter.a)
打印结果为
3
4
AMD和CMD
聊到 AMD
和 CMD
这两个规范都离不开 require.js
和 sea.js
,这是早些年,为了解决浏览器异步加载模块而诞生的方案。
随着打包工具的发展,commonjs
和es6
都可以在浏览器上运行了,所以 AMD、CMD 将逐渐被替代。
AMD规范的模块化:用 require.config()
指定引用路径等,用define()
定义模块,用require()
加载模块。
CMD规范的模块化:用define()
定义模块, seajs.use
引用模块。