JavaScript - 模块化
起因
当我输入了大量来自优质个人博客文章,经典书籍,名牌讲师的 JavaScript 系统知识之后,我反而变得有些困惑。我明明可以流利的书写八大继承,流利的书写 Underscore 库中的防抖节流,深拷贝……,我也知晓什么是闭包,什么是迭代器,生成器……。但是问题来了,当我试图将一个 JavaScript 核心概念,比如闭包……,介绍给同学,讲解给自己,对话面试官的时候。原本自认为可以脱口而出的语言,到嘴边却显得吞吞吐吐。只能用只言片语,或者东拼西凑的知识点来表达给对方。这无疑让我有了一种,虽然花了大量时间但是却从未拥有过 JavaScript 的感觉。
目的
所以这个系列要尝试解决的问题就是当别人询问或者考察我JavaScript 核心概念的时候,我可以尽可能流畅的,清晰的表达给他人。
期望
希望掘金优秀的前端技术人员和前辈们可以在百忙之中多多补充这篇文章,多多审查这篇文章,多多提问我。我期望可以通过这个系列来解决我当前的问题
参考链接
模块化定义
模块化是一种传统的软件开发方法,通常是将开发软件划分为一些功能相对独立的模块,模块与模块之间约定要暴露或者要接受的 API ,然后将各个模块可以分别单独开发、调试、运行和测试,然后,再将多个模块组合起来,完成整个软件的开发。
JavaScript 模块化历史
刀耕火种
在原始 JavaScript 社会,出现模块化规范之前,js 文件之间的通信基本上靠的是 window 对象。我们通过 script 文件来按照 js 文件之间依赖关系来按顺序引入 js。一旦顺序没有按照依赖关系来引入,就会报错。
// 引入顺序颠倒就会报错
<body>
<script src="./a.js"></script>
<scrip src="./b.js"></script>
</body>
// a.js
var nameP = "ryan";
// b.js
console.log(nameP);
所以当业务变得复杂时,就会产生很多问题:
- 第一个问题:多而复杂的
js文件不好开发和维护,需要考虑js文件之间的依赖关系。 - 第二个问题:
window上挂载的全局变量避免不了存在命名冲突、占用内存无法被回收、代码可读性低等诸多问题。 - 如果按照这么来开发前端,开发体验会非常糟糕。
所以尽管当时没有模块化的时候,但也涌现出了一些解决方案。
解决方案 1:函数
function m1() {
//...
}
function m2() {
//...
}
将不同的功能函数当成一个个模块,这样做虽然起到了一定的效果,但是还是避免不了存在命名冲突,最重要的是,这些函数被当作模块,我们看不出来模块之间的关系。
解决方案 2:对象
var module1 = new Object({
_count: 0,
m1: function () {
//...
},
m2: function () {
//...
},
});
将不同的功能对象当成一个个模块,这样做虽然起到了一定的效果,但是还是避免不了存在命名冲突,最重要的是,这样的写法会暴露所有的模块成员。模块成员可以被随意修改。
解决方案 3:IIFE(匿名立即执行函数)
// a.js
var moduleA = (function () {
return {
name: "ryan",
};
})();
//console.log(moduleA.name); ryan
随着前端业务增重,代码越来越复杂,前端急需一种清晰有效方案来处理代码功能模块之间的依赖关系。
AMD 和 CMD
node 服务器端编程出来的时候,模块系统就是参照 CommonJs 规范实现的,所以有了服务器端模块化规范,大家也想要客户端模块化开发,而且大家也希望两者可以相互兼容,但是 CommonJs 对客户端模块化来说,一个非常大的局限就是:同步加载模块的方式不适合浏览器环境,因为同步加载模块会导致浏览器卡死,阻塞渲染,所以在这样的背景下 AMD 这种异步加载模块化的方式出现了。
CMD 和 AMD 一样,都是 JS 的社区模块化规范,主要应用于浏览器端,可以异步加载模块。
我们只要按照规范的方式去书写,就可以被 require.js, sea.js 正确解析。从而实现模块化。
- 比如
require.js中的require函数,define函数 sea.js的require函数
AMD
例子来自于yayu
- require.js
// main.js
require(["./add", "./square"], function (addModule, squareModule) {
console.log(addModule.add(1, 1));
console.log(squareModule.square(3));
});
// add.js
define(function() {
console.log('加载了 add 模块');
var add = function(x, y) { 
return x + y;
};
return {      
add: add
};
});
// multiply.js
define(function() {
console.log('加载了 multiply 模块')
var multiply = function(x, y) { 
return x * y;
};
return {      
multiply: multiply
};
});
// square.js
define(['./multiply'], function(multiplyModule) {
console.log('加载了 square 模块')
return {      
square: function(num) {
return multiplyModule.multiply(num, num)
}
};
});
// 加载了 add 模块
// 加载了 multiply 模块
// 加载了 square 模块
// 2
// 9
CMD
- sea.js
// main.js
define(function(require, exports, module) {
var addModule = require('./add');
console.log(addModule.add(1, 1))
var squareModule = require('./square');
console.log(squareModule.square(3))
});
// add.js
define(function(require, exports, module) {
console.log('加载了 add 模块')
var add = function(x, y) { 
return x + y;
};
module.exports = {      
add: add
};
});
// square.js
define(function(require, exports, module) {
console.log('加载了 square 模块')
var multiplyModule = require('./multiply');
module.exports = {      
square: function(num) {
return multiplyModule.multiply(num, num)
}
};
});
// multiply.js
define(function(require, exports, module) {
console.log('加载了 multiply 模块')
var multiply = function(x, y) { 
return x * y;
};
module.exports = {      
multiply: multiply
};
});
// 加载了 add 模块
// 2
// 加载了 square 模块
// 加载了 multiply 模块
// 9
AMD 与 CMD 的区别
yayu总结的区别:
根据上面代码的打印结果,我们也可以更好的理解他们之间的区别
CMD推崇依赖就近,AMD推崇依赖前置。AMD是将需要使用的模块全部加载完再执行代码,而CMD是在require的时候才去加载模块文件,加载完再接着执行。
CommonJS(CJS)
CommonJS 基本说明
随着 node 诞生,服务器端的模块规范 CommonJS 被创建出来。在 commonjs 中每一个 js 文件都是一个单独的模块,我们在控制台也可以打印出这个 module 对象。所有代码都运行在模块作用域,不会污染全局作用域。同时,CommonJS 是运行时同步加载模块,比较适合服务器端的模块加载,因为服务器的模块文件都在硬盘,即使是同步也非常快。
CJS 基本用法
导入
const a = require("./a.js");
// a是module.exports导出的对象。
导出
// 为module.exports对象上添加属性。
exports.name = "kobe";
exports.age = 18;
// 重置了module.exports对象引用。
module.exports = {};
// 导出module.exports对象
cjs 模块导入导出的基本原理
module.exports = {};
exports = module.exports;
require 引入细节
- 情况一:X 是一个
Node核心模块,比如path、http。- 直接返回核心模块,并且停止查找。
- 情况二:X 是以 ./ 或 ../ 或 /(根目录)开头的。
- 第一步:将 X 当做一个文件在对应的目录下查找。
- 如果有后缀名,按照后缀名的格式查找对应的文件
- 如果没有后缀名,会按照如下顺序:按照
js->json->node的顺序。
- 第二步:没有找到对应的文件,将 X 作为一个目录。
- 作为目录下查找
index.js->.json->.node。
- 作为目录下查找
- 第一步:将 X 当做一个文件在对应的目录下查找。
ES6module(ESM)
AMD、CMD等都是在原有JS语法的基础上二次封装的一些社区规范,ES6 module是 JavaScript 语言层面的规范,ES6 module编译时静态加载。编译时就能确定模块的依赖关系,以及输入和输出的变量。而运行时加载是加载该模块的所有方法,生成一个对象,然后再从这个对象上面读取导出的方法。因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6module 与 CommonJS 的区别
CommonJS 模块输出的是一个值的拷贝对于引用类型而言就是引用,ES6模块输出的无论是什么类型都是值的引用。CommonJS 模块是运行时加载,ES6module是编译时就能确定模块的依赖关系和输入输出的接口。
ES6module 与 CommonJS 导出简单值
CommonJS
// 输出模块 counter.js -> 简单值
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// 引入模块 main.js
var mod = require("./counter");
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
ES6module
ES6 module模块输出的无论是什么类型都是值的引用。
// counter.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from "./counter";
console.log(counter); // 3
incCounter();
console.log(counter); // 4
CommonJS 导出引用值
// 输出模块 counter.js -> 引用值
var counter = {
value: 3,
};
function incCounter() {
counter.value++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// 引入模块 main.js
var mod = require("./counter.js");
console.log(mod.counter.value); // 3
mod.incCounter();
console.log(mod.counter.value); // 4
ES Module 的解析流程
阶段一: 构建(Construction),根据所有依赖关系去查找 js 文件,并且下载,将其解析成模块记录(Module Record)
阶段二: 实例化(Instantiation),对模块记录进行实例化为模块环境记录,这时import 会自动提升到代码的顶层。开始解析模块的导入和导出语句。并且为 bindings 的值(也就是导出的变量)分配内存空间。
阶段三: 运行(Evaluation),运行代码,计算值,并且赋值到导出变量对应的内存地址中,以供导入消费。
导入的模块不可以修改值,因为在模块环境记录当中用的是 const,修改就会报错。
export 用法
export const name = "why";
export const age = 18;
export { name, age, foo };
export { name as fName, age as fAge, foo as fFoo };
// 直接导出,无需先引入。
export { add, sub } from "./math.js";
export { timeFormat, priceFormat } from "./format.js";
export * from "./math.js";
export * from "./format.js";
// 默认导出
export default foo;
import 用法
import { name, age } from "./foo.js";
import { name as fName, age as fAge, foo as fFoo } from "./foo.js";
import * as foo from "./foo.js";
// 默认导出的引入
import why from "./foo.js";
// 动态引入
import("./foo.js").then((res) => {
console.log("res:", res.name);
});