fanxinjie / blog

issues blog
0 stars 0 forks source link

JavaScript 模块演化 #3

Open fanxinjie opened 2 years ago

fanxinjie commented 2 years ago

模块化

模块化是指把一个复杂的系统分解到多个模块以方便编码。

很久以前,开发网页要通过命名空间的方式来组织代码,例如 jQuery 库把它的API都放在了 window.$ 下,在加载完 jQuery 后其他模块再通过 window.$ 去使用 jQuery。 这样做有很多问题,其中包括:

当项目变大时这种方式将变得难以维护,需要用模块化的思想来组织代码。

类模块化

Function

最初写代码的时候,没有学习模块管理,基本上都是暴力写,记得每次都会定义好多function,心想反正就几个方法,名称应该不会重复,随着手法的放飞自我还有代码的放飞自我,方法名甚至会出现asd1, asd2,放到现在怕是能被打死。

闭包

立即执行函数

CommonJS

CommonJS 是一种使用广泛的 JavaScript 模块化规范,核心思想是通过 require 方法来同步地加载依赖的其他模块,通过 module.exports 导出需要暴露的接口。 CommonJS 规范的流行得益于 Node.js 采用了这种方式,后来这种方式被引入到了网页开发中。需要注意的是,CommonJS 规范的主要适用场景是服务器端编程,所以采用同步加载模块的策略。如果我们依赖3个模块,代码会一个一个依次加载它们。

采用 CommonJS 导入及导出时的代码如下:

// 导入
const moduleA = require('./moduleA');

// 导出
module.exports = moduleA.someFunc;

CommonJS 的优点在于:

CommonJS 的缺点

CommonJS 还可以细分为 CommonJS1 和 CommonJS2,区别在于 CommonJS1 只能通过 exports.XX = XX 的方式导出,CommonJS2 在 CommonJS1 的基础上加入了 module.exports = XX 的导出方式。 CommonJS 通常指 CommonJS2。

AMD

AMD 也是一种 JavaScript 模块化规范,与 CommonJS 最大的不同在于它采用异步的方式去加载依赖的模块。 AMD 规范主要是为了解决针对浏览器环境的模块化问题,最具代表性的实现是 require.js

采用 AMD 导入及导出时的代码如下:

// 定义一个模块
define('module', ['dep'], function(dep) {
  return exports;
});

// 导入和使用
require(['module'], function(module) {
});

AMD 的优点在于:

AMD 的缺点在于JavaScript 运行环境没有原生支持 AMD,需要先导入实现了 AMD 的库后才能正常使用。

CMD

CMD是在AMD基础上改进的一种规范,和AMD不同在于对依赖模块的执行时机处理不同,CMD是就近依赖,而AMD是前置依赖。在CMD中,一个模块就是一个文件。最具代表性的实现是 sea.js

// a.js
define(function (require, exports, module){
  exports.a = 'hello world';
});

// b.js
define(function (require, exports, module){
    var moduleA = require('./a.js');
    console.log(moduleA.a); // 打印出:hello world
});

ES6 模块化

ES6 模块化是欧洲计算机制造联合会 ECMA 提出的 JavaScript 模块化规范,它在语言的层面上实现了模块化。浏览器厂商和 Node.js 都宣布要原生支持该规范。它将逐渐取代 CommonJSAMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6的设计思想是尽量的静态化,使得编辑时就能确定模块的依赖关系,以及输出和出入的变量。CommonJSAMD 都只能在运行时确定这些东西。

采用 ES6 模块化导入及导出时的代码如下:

// 导入
import { readFile } from 'fs';
import React from 'react';
// 导出
export function hello() {};
export default {
  // ...
};

其中 import { readFile } from 'fs' 相当于

let _fs = require('fs');
let readFile = _fs.readFile;

上述代码称为“运行时加载”,也就是完全加载整个模块,之后再读取对应的方法,只有运行时才会拿到这个对象,所以无法实现编译时静态化. 而 ES中的代码 import { readFile } from 'fs' 称为“静态加载”,即只加载 readFile 方法,相比之下静态加载效率更高。

ES6模块虽然是终极模块化方案,但它的缺点在于目前无法直接运行在大部分 JavaScript 运行环境下,必须通过工具转换成标准的 ES5 后才能正常运行。

ES6 与 CommonJS 差异

着重说下第一个差异:

// export.js
var counter = 3;

function incCounter() {
    counter++;
}
module.exports = {
    counter: counter,
    get counter1() {
        return counter
    },
    incCounter: incCounter,
};

// import.js
var mod = require('./export');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

console.log(mod.counter1);  // 4
mod.incCounter();
console.log(mod.counter1); // 5

上面代码说明,lib.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

// export.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// import.js
import { counter, incCounter } from './export';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

另一个例子验证 module 是动态加载的

// export.js
export var a = 'asd';
setTimeout(() => a = 'asd111', 500);

// import.js
import {a} from './export.js';
console.log(a);
setTimeout(() => console.log(a), 500);

// 结果是
// asd, asd111