Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CommonJS,AMD 和 ES6 模块化的差异 #17

Open
liangbus opened this issue Dec 4, 2019 · 2 comments
Open

CommonJS,AMD 和 ES6 模块化的差异 #17

liangbus opened this issue Dec 4, 2019 · 2 comments
Labels
ES6 ES6 新特性 JavaScript

Comments

@liangbus
Copy link
Owner

liangbus commented Dec 4, 2019

上述三者是我们听得最多的模块化方式,此外还有 CMD,UMD 这些,比较小众的,本次就不单独拿出来说了

模块化,相信任何人多多少少都有接触过,项目到了一定程度,都需要将其拆分成不同的模块,用以解决命名冲突,文件依赖相关问题,因而就催生了上述多种的模块化标准,在 ES6 之前,JavaScript 一直都没有一个官方的模块化标准,而社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种,前者用于服务器,后者用于浏览器。直到 ES6 的推出,终于在语言层面上实现了实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块化与 CommonJS 和 AMD 最大的不同就是

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

其中 nodeJs 采用的就是 CommonJs 规范

CommonJS

nodeJs 是 CommonJs 规范的主要实践者,它有4个重要的环境变量为模块化实现提供支持: module, exports, require, global。
实际使用时,推荐使用 module.exports 来输出模块接口,(阮一峰老师规范教程也是不推荐直接使用 exports)

  • CommonJS规范加载模块是同步的,只有加载完成,才能执行后面的操作
  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。

// main.js
var mod = require('./lib');

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

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

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。

@liangbus liangbus added ES6 ES6 新特性 JavaScript labels Dec 4, 2019
@liangbus
Copy link
Owner Author

liangbus commented Dec 4, 2019

AMD

由于 NodeJs 主要用于服务端,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以 CommonJS 规范比较适用
但是如果是在客户端(浏览器)环境,要从服务端加载模块,这时就必须采用异步的加载模式,因此客户端一般使用 AMD 规范,require.js 就是以 AMD 规范实现的。示例:

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});

引用模块的时候,我们将模块名放在 [] 中作为 require 的第一参数,如果 我们定义的模块本身也依赖其他模块,那就需要将它们放在 [] 作为 define 的第一参数,示例:

// 定义math.js模块
define(function() {
	var baseNum = 0
	var add = function(x, y) {
		return x + y
	}
	return {
		add,
		baseNum
	}
})
// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
  var classify = function(list){
    _.countBy(list,function(num){
      return num > 30 ? 'old' : 'young';
    })
  };
  return {
    classify :classify
  };
})

// 父文件
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
  var sum = math.add(10,20);
  $("#sum").html(sum);
});


@liangbus
Copy link
Owner Author

liangbus commented Dec 4, 2019

ES6 module

ES6 在语言层面实现了模块化标准(但目前浏览器不能直接识别),目的是成为浏览器和服务器通用的模块解决方案。
其模块主要由两个关键字命令组成:
export: 模块输出接口
import: 模块引入接口
与 CommonJS 运行时加载不同,ES6 module 是编译时加载,所以在编译时就会确认好模块的依赖关系,也正因此可以对其做一些静态分析的工作
比如下面代码,fs 中定义了很多属性和方法,CommonJS 会把整个 fs 加载进来作为对象

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6 module 只会加载需要的属性和方法

// ES6模块
import { stat, exists, readFile } from 'fs';

结论:

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值(仅限基本类型,如果是对象,还是可以改变的)。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

参考:
Module 的加载实现
CommonJS规范 - JavaScript 标准参考教程

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ES6 ES6 新特性 JavaScript
Projects
None yet
Development

No branches or pull requests

1 participant