a 1. 模块化
模块化就是指将一个复杂的系统分解到多个模块方便编码,但是在js当中是没有模块体系的,在之前我们实现模块化普遍采用的是命名空间的方式,但是通过这种方式来组织代码会造成以下不好的几点:
- 命名空间冲突,两个库可能会使用同一个名称;
- 无法合理的管理项目的依赖和版本;
- 无法方便的控制依赖的加载顺序。
所以出现了几种模块加载方案,比如CommonJS
和AMD
。
2. CommonJS
CommonJS
是一种使用广泛的JavaScript
模块化规范,核心思想是通过require
方法来同步的加载依赖的其他模块,通过module.exports
导出需要暴露的接口,node
的模块化就是通过CommonJS
实现的。
用法如下:
// 导入
const moduleA = require('./moduleA');
// 导出
module.exports = moduleA;
下面看一下怎么使用:
// a.js
module.exports = '人生不是一蹴而就';
// b.js
let str = require('./a.js');
console.log(str); // 人生不是一蹴而就
通过以上的导入导出方法就可以读取另一个文件中的内容,下面通过简单的实现看一下require
导入的原理,先大概知道个思想,其实就是将a.js
的文件内容引进来了(通过node
中的fs
文件模块),并且用一个闭包包起来,然后执行这个闭包:
// a.js
module.exports = '人生不是一蹴而就';
// b.js
let fs = require('fs');
function req(moduleName) {
let content = fs.readFileSync(moduleName, 'utf8');
// 最后一个参数是函数的内容体 也就是将文件内容用一个闭包包起来
let fn = new Function('exports', 'module', 'require', '__dirname', '__filename', content+'\n return module.exports');
let module = {
exports: {}
};
// 执行函数 返回我们需要的结果
return fn(module.exports, module, req, __dirname, __filename);
}
let str = req('./a.js');
console.log(str); // 人生不是一蹴而就
其实相当于我们在req当中构建了一个函数,函数形式如下:
function(exports, module, require, __dirname, __filename) {
module.exports = '人生不是一蹴而就';
return module.exports;
}
这样就简单的实现了一个req
,最后返回结果就可以打印出文件中的内容了。commonJS
用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。
3. AMD
AMD
是采用异步的方式去加载依赖的模块,AMD
规范主要是为了解决针对浏览器环境的模块化问题,最具有代表性的实现是requirejs
,可在不转化代码的情况下直接在浏览器中运行;可以加载多个依赖;代码可以运行在浏览器环境下和node
环境下。
用法如下:
// define 声明模块 通过require使用模块
// define中参数分别为moduleName、依赖、处理函数
define('name', [], function() {
return 'zl';
});
define('age', [], function() {
return 25;
});
// 使用时可直接require然后打印其他模块的执行结果
require(['name', 'age'], function(name, age) {
console.log(name, age);
})
那我们现在写成这样define
是还没有定义的,下面来实现define
和require
:
// 工厂对象主要用于之后将模块名和其所对应的工厂函数关联起来,好在require中导出
let factories = {}
// 参数分别为 模块名 依赖 工厂函数
function define(moduleName, dependencies, factory) {
// 将模块名字和工厂函数关联起来
factories[moduleName] = factory;
}
function require(mods, callback) {
// mods -> name age
let result = mods.map((mod) => {
let factory = factories[mod];
let exports;
exports = factory();
return exports;
});
callback.apply(null, result);
}
// define 声明模块 通过require使用模块
define('name', [], function() {
return 'zl';
});
define('age', [], function() {
return 25;
});
require(['name', 'age'], function(name, age) {
console.log(name, age); // zl 25
})
上面是一个简单的实现,我们考虑一种情况,即:
define('name', [], function() {
return 'zl';
});
define('age', ['name'], function(name) {
return name+25;
});
require(['age'], function(age) {
console.log(age);
})
age
模块依赖于name
模块的执行结果,实际上age
模块返回的是zl 25
,这个时候就要考虑如果判断是否有依赖其他模块并执行的情况了:
let factories = {}
// 参数分别为 模块名 依赖 工厂函数
function define(moduleName, dependencies, factory) {
// 将依赖记到factory上
// 也可以写成点的形式 factory.dependencies = dependencies;
factory['dependencies'] = dependencies;
// 将模块名字和工厂函数关联起来
factories[moduleName] = factory;
}
function require(mods, callback) {
let result = mods.map((mod) => { // name age
let factory = factories[mod];
let exports;
// 将依赖取出来 然后递归
let dependencies = factory['dependencies'];
// require(['name'], function(na...me) {})
require(dependencies, function() {
exports = factory.apply(null, arguments);
})
return exports;
});
callback.apply(null, result);
}
define('name', [], function() {
return 'zl';
});
define('age', ['name'], function(name) {
return name+25;
});
require(['age'], function(age) {
console.log(age); // zl 25
})
当require
的模块依赖于一个或多个其他的模块时,主要就是一个递归执行依赖组件的工厂函数的过程。
4. ES6模块和 CommonJS 模块有哪些差异?
CommonJS
模块是运行时加载,ES6
模块是编译时输出接口。
ES6
模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。ES6
模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。CommonJS
加载的是一个对象,该对象只有在脚本运行完才会生成。
CommonJS
模块输出的是一个值的拷贝,ES6
模块输出的是值的引用。
也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。如:
//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
module.exports = {
name
};
//index.js
const name = require('./name');
console.log(name); //William
setTimeout(() => console.log(name), 300); //William
但是当输出的是复杂数据类型时,实际上拷贝的是栈内存中的地址,所以输出的值会发生改变:
// name.js
let name = 'William';
let hobbies = ['coding'];
setTimeout(() => {
name = 'Yvette';
hobbies.push('reading');
}, 300);
module.exports = { name, hobbies };
// index.js
const { name, hobbies } = require('./name');
console.log(name); // William
console.log(hobbies); // ['coding']
setTimeout(() => {
console.log(name); // William
console.log(hobbies); // ['coding', 'reading']
}, 500);
ES6 模块的运行机制与 CommonJS
不一样。JS
引擎对脚本静态分析的时候,遇到模块加载命令 import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
export { name };
//index.js
import { name } from './name';
console.log(name); //William
setTimeout(() => console.log(name), 300); //Yvette
-
ES6
模块自动采用严格模式,无论模块头部是否写了"use strict"
; -
require
可以做动态加载,import
语句做不到,import
语句必须位于顶层作用域中。 -
ES6
模块的输入变量是只读的,不能对其进行重新赋值 -
当使用
require
命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS
模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。