JavaScript 模块化

JS 模块化

1、模块化发展历程

  • IIFE 自执行函数
  • AMD 使用 requireJS 来编写模块化(依赖必须提前声明好。)
  • CMD 使用 seaJS 来编写模块化(支持动态引入依赖文件。)
  • CommonJS nodeJs 中自带的模块化
  • UMD 兼容 AMDCommonJS 语法
  • webpack(require.ensure)webpack 2.x 版本中的代码分割
  • ES ModulesES6 引入的模块化,支持 import 来引入另一个 js
  • script 标签 type="module"

js 模块化

2、AMDCMD 的区别

AMDCMD 最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块

  • AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块
  • CMD 推崇就近依赖,只有在用到某个模块的时候再去 require

3、CommonJS 规范的特点

  1. 所以代码都是运行在模块作用域中,不会污染全局作用域
  2. 模块是同步加载的,只有引入的模块加载完成,才会执行后面的操作
  3. 模块在首次执行后就会缓存,再次加载只返回缓存的结果
  4. CommonJS 输出的是值的拷贝,模块内部再次改变也不会影响这个值(引用类型和基本类型有区别)

4、ES6 modules 规范有什么特点

  1. 输出使用 export
  2. 引入使用 import
  3. 可以使用 export ... from ... 来达到一个中转的效果
  4. 输入的模块变量是不可重新赋值的。只是个可读引用,但是可以改写属性
  5. exportimport 命令处于模块顶层,不能位于作用域内,处于代码块中,没法做静态优化,违背了 ES6 模块的设计初衷
  6. import 有提升效果,会提升到整个模块的头部,首先执行
  7. Babel 会把 export/import 转化为 exports/require 的形式,所以可以使用 exportsimport

5、CommonJSES6 Modules 规范的区别

  1. CommonJS 模块是运行时加载,ES6Modules 是编译时加载
  2. CommonJS 输出值的拷贝,ES6Modules 输出值的引用(模块内部改变会影响引用)
  3. CommonJS 导入模块可以是一个表达式(是使用 require() 引入),ES6Modules 导入只能是字符串
  4. CommonJS 中 this 指向当前模块,ES6Modulesthis 指向 undefined
  5. ES6Modules 中没有 argumentsrequiremoduleexports__filename__dirname 这些顶层变量

6、如何异步进行模块的加载

AMDCMD 支持异步加载模块

7、开发一个模块需要考虑哪些问题?

  1. 安全性
  2. 封闭性
  3. 避免变量冲突
  4. 隔离作用域
  5. 公共代码的抽离

8、node require(X) 引入的处理顺序是什么样的?

  1. 如果 X 是内置模块,返回该模块,不再继续执行;
  2. 如果 X'./'、'/'、'../' 开头,将根据 X 所在的父模块,确定 X 的绝对路径:
    a. 将 X 当成文件,依次查找,存在,返回该文件,不再继续执行;
    b. 将 X 当成目录,依次查找目录下的文件,存在,返回该文件,不再继续执行;
  3. 如果 X 不带有路径:
    a. 根据 X 所在的父模块,确定 X 可能的安装目录
    b. 依次在每个目录中,将 X 当成文件名或者目录名加载
  4. 抛出 not found 错误

9、node 中相互引用

有个 a.jsb.js 两个文件,互相引用

1. CommonJS

{
  id: '...',
  exports: { ... },
  loaded: true, parent: null, filename: '', children: [], paths: []
}
复制代码

CommonJS 的一个模块,就是一个脚本文件。require 命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。以后需要用到这个模块的时候,就会到 exports 属性上面取值。即使再次执行 require 命令,也不会再次执行该模块,而是到缓存之中取值。

CommonJS 重要特性是加载时执行,脚本代码在 require 时,全部执行。

CommonJS 的做法是,一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

// 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 执行完毕');
复制代码
  1. a.js 脚本先输出一个 done 变量,然后加载另一个脚本文件 b.js。注意,此时 a.js 代码就停在这里,等待 b.js 执行完毕,再往下执行。
  2. b.js 执行到第二行,就会去加载 a.js,这时,就发生了”循环加载”。系统会去 a.js 模块对应对象的 exports 属性取值,可是因为 a.js 还没有执行完,从 exports 属性只能取回已经执行的部分,而不是最后的值。
  3. a.js 已经执行的部分,只有一行。
exports.done = false;
复制代码
  1. 因此,对于 b.js 来说,它从 a.js 只输入一个变量 done,值为 false
  2. b.js 接着往下执行,等到全部执行完毕,再把执行权交还给 a.js。于是,a.js 接着往下执行,直到执行完毕。我们写一个脚本 main.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);
// 运行
// 在 b.js 之中,a.done = false
// b.js 执行完毕
// 在 a.js 之中,b.done = true
// a.js 执行完毕
// 在 main.js 之中, a.done=true, b.done=true
复制代码
  1. 上面的代码证明了两件事。一是,在 b.js 之中,a.js 没有执行完毕,只执行了第一行。二是,main.js 执行到第二行时,不会再次执行 b.js,而是输出缓存的 b.js 的执行结果,即它的第四行。

2. ES6

ES6 模块的运行机制与 CommonJS 不一样,它遇到模块加载命令 import 时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。

ES6 模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块。

ES6 根本不会关心是否发生了”循环加载”,只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
  counter++;
  return n == 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
  return n != 0 && even(n - 1);
}
复制代码

按照 CommonJS 规范,是没法加载的,是会报错的,但是 ES6 就可以执行。
之所以能够执行,原因就在于 ES6 加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
复制代码

上面代码中,参数 n10 变为 0 的过程中,foo() 一共会执行 6 次,所以变量 counter 等于 6。第二次调用 even() 时,参数 n20 变为 0foo() 一共会执行 11 次,加上前面的 6 次,所以变量 counter 等于17

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享