CommonJS

  1. 使用require和exports关键字和模块系统进行交互
  2. CommonJS不支持异步加载
  3. 一个文件就是一个模块

Nodejs的模块规范受到了CommonJS的影响,但Nodejs支持使用module.exports导出对象,而CommonJS只使用exports。CommonJS模块在未经编译前无法使用。示例如下:

// modules/physics.js
module.exports = {
  lorentzTransformation () {
  },
  maxwellSEquations () {
  }
}

index.js

const physics = require('./modules/physics')

physics.lorentzTransformation()
physics.maxwellSEquations()

module是一个带有exports属性的对象,exports是普通的js变量,是module.exports的引用。如果设置exports.name = '叶奈法',相当于设置module.exports.name = '叶奈法'。但是,如果给exports设置了一个新的对象,exportsmodule.exports将不再是同一个对象。

// 简化的理解
var module = { exports: {} }
var exports = module.exports

AMD

AMD诞生的原因是,是因为CommonJS不支持异步加载,不适合浏览器环境。

Node.js 主要用于服务器编程,加载的模块文件一般都已经存在本地硬盘,加载起来较快,不用考虑异步加载的方式,所以 CommonJS 的同步加载模块规范是比较适用的。
但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD,CMD 等解决方案。

RequireJS实现了AMD API。示例如下:
在index.html中使用<script/>标签加载RequireJS,通过data-main属性指定主文件。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>AMD</title>
  <!-- require.js -->
  <script data-main='./index.js' src="./require.js"></script>
</head>
<body>
</body>
</html>

define关键字用于定义模块,模块分为独立模块(不依赖其他模块的模块)以及非独立模块(依赖其他模块的模块)。

独立模块:

// libs/geometry.js
define(function() {
  'use strict';
  return {
    pythagoreanTheorem(a, b) {
      return a * a + b * b
    }
  }
})

非独立模块,本模块引用了geometry模块:

// libs/math.js
define(['./geometry.js'], function(geometry) {
  'use strict';
  return {
    geometry: {
      pythagoreanTheorem: geometry.pythagoreanTheorem
    }
  }
})

require关键字用来引用模块:

// index.js
// 加载math模块
require(['./libs/math'], function (math) {
  var c = math.geometry.pythagoreanTheorem(1, 2)
  alert(c)
}
)

CMD

专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行

//定义没有依赖的模块
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})

//定义有依赖的模块
define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
    //引入依赖模块(异步)
    require.async('./module3', function (m3) {
    })
  //暴露模块
  exports.xxx = value
})

//引入使用模块
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

CMD与AMD区别

AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。
AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;
而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
一句话总结:
两者都是异步加载,只是执行时机不一样。AMD是依赖前置,提前执行,CMD是依赖就近,延迟执行。

ES6

ES6在语言层面上实现了模块机制,与CommonJSAMD规范不同的是ES6的模块是静态的,不能在文件的任何地方使用。这种行为使得编译器编译时就可以构建依赖关系树,但是在ES6模块没法在浏览器中完全实现,需要使用babelwebpack

// src/modules/physics.js
export function maxwellSEquations () {
  alert('maxwellSEquations')
}
// src/main.js
import { maxwellSEquations } from './modules/physics'

maxwellSEquations()

ES6模块和CommonJS区别

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析时,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。ES6 模块中,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值
  1. ES6 模块输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

值的引用

// lib.js
var counter = 3;
var obj = {
    name: 'David'
};

function changeValue() {
    counter++;
    obj.name = 'Peter';
};

module.exports = {
    counter: counter,
    obj: obj,
    changeValue: changeValue,
};

CommonJS:
CommonJS 模块输出的是值的拷贝(类比于基本类型和引用类型的赋值操作)。对于基本类型,一旦输出,模块内部的变化影响不到这个值。对于引用类型,效果同引用类型的赋值操作。

var mod = require('./lib');
console.log(mod.counter);  // 3
console.log(mod.obj.name);  //  'David'
mod.changeValue();
console.log(mod.counter);  // 3
console.log(mod.obj.name);  //  'Peter'

ES6:

import { counter, changeValue } from './lib';
console.log(counter); // 3
changeValue();
console.log(counter); // 4
console.log(mod.obj.name);  //  'Peter'

加载时机

CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
ES6 模块是编译时输出接口,因此有如下2个特点

  • import 命令会被 JS 引擎静态分析,优先于模块内的其他内容执行
  • export 命令会有变量声明提升的效果

    共同点

    ES6模块和CommonJS相同点:模块不会重复执行。

UMD

UMD (Universal Module Definition), 希望提供一个前后端跨平台的解决方案(支持AMD与CommonJS模块方式)。

AMD 模块以浏览器第一的原则发展,异步加载模块。
CommonJS 模块以服务器第一原则发展,选择同步加载。它的模块无需包装(unwrapped modules)。
这迫使人们又想出另一个更通用的模式 UMD(Universal Module Definition),实现跨平台的解决方案。

实现原理

UMD的实现很简单:

  1. 先判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
  2. 先判断是否支持Node.js模块格式(exports是否存在),存在则使用Node.js模块格式。
  3. 前两个都不存在,则将模块公开到全局(window或global)。

各种具体的实现方式,可以查看UMD github。我这里举例一个没有依赖的,按照如上方式实现的代码:

// if the module has no dependencies, the above pattern can be simplified to

(function (root, factory) {

if (typeof define === 'function' && define.amd) {

// AMD. Register as an anonymous module.

define([], factory);

} else if (typeof module === 'object' && module.exports) {

// Node. Does not work with strict CommonJS, but

// only CommonJS-like environments that support module.exports,

// like Node.

module.exports = factory();

} else {

// Browser globals (root is window)

root.returnExports = factory();

}

}(typeof self !== 'undefined' ? self : this, function () {

// Just return a value to define the module export.

// This example returns an object, but the module

// can return a function as the exported value.

return {};

}));
why define.amd?
The first line checks whether you have an AMD loader available, and will use the AMD loader if present. If a define function exists but it does not have the amd property set, then it is some random foreign define.
The name define is pretty generic. If it were not for the amd property, it would be sometimes difficult to determine whether the define that is present is really the one we care about.

循环依赖

CommonJS

// a.js
console.log('a starting');
exports.done = false;
const b = require('./b');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');

// b.js
console.log('b starting');
exports.done = false;
const a = require('./a');
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');
// node a.js
// 执行结果:
// a starting
// b starting
// in b, a.done = false
// b done
// in a, b.done = true
// a done

一旦出现某个模块被“循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。并缓存执行的结果,当下次再次加载时不会重复执行,而是直接取缓存的结果。

ES6

跟 CommonJS 模块一样,ES6 不会再去执行重复加载的模块,又由于 ES6 动态输出绑定的特性,能保证 ES6 在任何时候都能获取其它模块当前的最新值。

import()

为了解决 ES6 模块无法在运行时确定模块的引用关系,所以需要引入 import()。

  • 动态的 import() 提供一个基于 Promise 的 API
  • 动态的 import() 可以在脚本的任何地方使用 import() 接受字符串文字,可以根据需要构造说明符

模块编译

在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。
在头部添加了(function (exports, require, module, __filename, __dirname) {})
一个正常的JavaScript文件会被包装成如下的样子:

(function (exports, require, module, filename, dirname) {
  var math = require('math');
  exports.area = function (radius) {
  return Math.PI * radius * radius;
  };
});

这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这function()执行。
这就是这些变量并没有定义在每个模块文件中却存在的原因。在执行之后,模块的exports属性被返回给了调用方。exports属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用。
许多初学者都曾经纠结过为何存在exports的情况下,还存在module.exports。理想情况下,只要赋值给exports即可:

exports = function () {
// My Class
};

但是通常都会得到一个失败的结果。其原因在于,exports对象是通过形参的方式传入的,直接赋值形参会改变形参的引用,但并不能改变作用域外的值。

参考


specialCoder
2.1k 声望167 粉丝

前端 设计 摄影 文学