阅读 5290

一篇不是标题党的CommonJS和ES6模块规范讲解

前言

你盼世界,我盼望你无bug。Hello 大家好!我是霖呆呆。

没错,看着这篇文章的标题你可能就不想看了,CommonJSES6 Modules规范这都是啥时候的知识点了,你还在这写呢...

哭😢...

因为呆呆之前对这些规范还真的就只是看一些教材,没有去实践,然后今日实践起来才发现很多教材中提到的知识点并不是那么回事,所以做了一篇总结想分享给大家。

不过其实在发出来之前自己心里也没有太多底,不知道文章中说的一些东西对不对...唔...因此也是做好了被喷的准备,如果确实有理解不到位的地方还请大大们能够指出,一起学习一下哈。(我这也是翻了很多资料没辙了😂)

正文

通过阅读本篇文章你可以学习到:

  • 原始模拟模块的一些写法
  • CommonJS规范
  • AMD规范
  • CMD规范
  • AMD和CMD的区别
  • ES6 Modules规范
  • CommonJS与ES6 Modules规范的区别

📒 本文已被收入niubility-coding-js 霖呆呆个人博客汇总,欢迎Star~

🌰 本文教材案例地址:CommonJS-example

🔥本文排版工具:mdnice, 感谢作者画手 😊。

原始写法

在没有CommonJSES6的时候,我们想要达到模块化的效果可能有这么三种:

1. 一个函数就是一个模块

<script>
  function m1 () {
    // ...
  }
  function m2 () {
    // ...
  }
</script>
复制代码

缺点:污染了全局变量,无法保证不会与其它模块发生冲突,而且模块成员之间看不出直接关系。

2. 一个对象就是一个模块

对象写法 为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。

index.html

<script>
  var module1 = new Object({
    _sum: 0,
    foo1: function () {},
    foo2: function () {}
  })
</script>
复制代码

缺点:会暴露所有模块成员,内部的状态可能被改写。

例如,我们如果只是想暴露出两个方法而不暴露出_sum,就做不到。

而此时,_sum可能被外部改写:

module1._sum = 2;
复制代码

3. 立即执行函数为一个模块

<script>
  var module1 = (function() {
    var _sum = 0;
    var foo1 = function () {};
    var foo2 = function () {};
    return {
      foo1: foo1,
      foo2: foo2
    }
  })();
</script>
复制代码

利用立即执行函数内的作用域已经闭包来实现模块功能,导出我们想要导出的成员。

此时外部代码就不能读取到_sum了:

console.log(module1._sum) // undefined
复制代码

CommonJS规范

这里不做具体的介绍了,我只把一些重要的知识点以及混淆点例举出来。

主要是从这四个方面说:

  • 暴露模块
  • 引用模块
  • 模块标识符
  • CommonJS规范的特点

1. 暴露(定义)模块

正确的暴露方式:

暴露模块有两种方式:

  • module.exports = {}
  • exports.xxx = 'xxx'

例如有一个m1.js文件:

第一种暴露方式:

module.exports = {
    name: 'lindaidai',
    sex: 'boy'
}
复制代码

第二种暴露方式:

exports.name = 'lindaidai';
exports.sex = 'boy'
复制代码

为什么可以有这两种写法呢?

我是这样理解的:module这个变量它代表的就是整个模块,也就是m1.js。而其实这个module变量是有一个属性exports的,它是一个叫做exports变量的引用,我们可以写一下伪代码:

var exports = {};
var module = {
    exports: exports
}
return module.exports
复制代码

(当然这只是伪代码啊,实际你这么去用会发现没有效果)

最后导出的是module.exports,而不是exports

容易混淆的暴露方式:

如果你在代码中试图exports = { name: 'lindaidai' },你会发现在引入的地方根本获取不到name属性。

// m1.js
exports = {
    name: 'lindaidai'
}
复制代码
// test.js
const math = require('./m1.js')

console.log(m1); // {}
复制代码

在控制台执行node test.js,发现打印出来的m1是一个空的对象。

我是这样理解的:整个模块的导出是靠module.exports的,如果你重新对整个exports对象赋值的话,它和module.exports就不是同一个对象了,因为它们指向的引用地址都不同:

module.exports -> {} // 指向一个空的对象
exports -> { name: 'lindaidai' } // 指向的是另一个对象
复制代码

所以你对exports = {}做任何操作都影响不到module.exports

让我们来看几个正确和错误的示例吧:

// m1.js
// 1. 正确
module.exports = {
    name: 'lindaidai',
    sex: 'boy'
}

// 2. 正确
exports.name = 'lindaidai';
exports.sex = 'boy'

// 3. 正确
module.exports.name = 'lindaidai';
module.exports.sex = 'boy'

// 4. 无效
exports = {
    name: 'lindaidai',
    sex: 'boy'
}
复制代码

可以看到

  • exports.name = xxxmodule.exports.name = xxx的缩写。
  • exports = {}确不是module.exports = {}的缩写。

2. 引用(引入)模块

对于模块的引用使用全局方法require()就可以了。

注意⚠️这个全局方法是node中的方法哈,它不是window下面的,所以如果你没做任何处理想直接在html里用肯定就是不行的了:

index.html:

<body>
    <script>
        var m1 = require('./m1.js')
        console.log(m1);
    </script>
</body>
复制代码

例如上面👆这样你打开页面控制台肯定就报错了:

Uncaught ReferenceError: require is not defined
	at index.html:11
复制代码

而如果你是在另一个js文件中引用(例如test.js),并在终端执行node test.js是可以用的:

test.js:

var m1 = require('./m1.js')

console.log(m1);
复制代码

那是因为你的电脑上全局安装了Node.js,所以可以这样玩。

所以我们可以发现require()它是Node.js中的一个全局方法,并不是CommonJS独有的,CommonJS只是众多规范中的其中一种。

这种规范允许我们:

  • 使用module.exports = {}或者exports.name = xxx导出模块
  • 使用const m1 = require('./m1')引入模块

注意⚠️:

另外还有一点比较重要,那就是require()的参数甚至能允许你是一个表达式。

也就是说你可以把它设置为一个变量:

test.js:

var m1Url = './m1.js';
var m1 = require(m1Url);

// 甚至做一些字符串拼接:
var m1 = require('./m' + '1.js');
复制代码

但是需要注意咯,这个传参可以为表达式并不是require特有的。因为JS语言是传值调用,函数或者方法在调用的时候参数会被先计算出来,因此在我们使用require方法并传入表达式的时候,会先计算出表达式的值再传递给require。(感谢掘友骑自行车的指出)

3. 模块标识符(标识)

模块标识符其实就是你在引入模块时调用require()函数的参数。

你会看到我们经常会有这样的用法:

// 直接导入
const path = require('path');
// 相对路径
const m1 = require('./m1.js');
// 直接导入
const lodash = require('lodash');
复制代码

这其实是因为我们引入的模块会有不同的分类,像path这种它是Node.js就自带的模块,m1是路径模块,lodash是我们使用npm i lodash下载到node_modules里的模块。

分为以下三种:

  • 核心模块(Node.js自带的模块)
  • 路径模块(相对或绝对定位开始的模块)
  • 自定义模块(node_modules里的模块)

三种模块的查找方式:

  • 核心模块,直接跳过路径分析和文件定位
  • 路径模块,直接得出相对路径就好了
  • 自定义模块,先在当前目录的node_modules里找这个模块,如果没有,它会往上一级目录查找,查找上一级的node_modules,依次往上,直到根目录下都没有, 就抛出错误。

自定义模块的查找过程:

这个过程其实也叫做路径分析

现在我把刚刚的test.js来改一下:

// var m1 = require('./m1.js');

// console.log(m1);
console.log(module.paths)
复制代码

然后在终端执行:

node test.js
复制代码

会发现输出了下面的一个数组:

LinDaiDaideMBP:commonJS lindaidai$ node test.js
[
  '/Users/lindaidai/codes/test/CommonJS和ES6/commonJS/node_modules',
  '/Users/lindaidai/codes/test/CommonJS和ES6/node_modules',
  '/Users/lindaidai/codes/test/node_modules',
  '/Users/lindaidai/codes/node_modules',
  '/Users/lindaidai/node_modules',
  '/Users/node_modules',
  '/node_modules'
]
复制代码

这里所说的查找,是指查找你现在用的这个模块,我现在用的是test.js,你可能看不出什么效果。现在让我们来模拟一个我们使用npm i安装的一个自定义模块功能。

首先,我在根目录下新建了一个名叫node_modules的文件夹,并在其中新建了一个名叫lindaidai.js的文件,用来模拟一个npm安装的依赖。

目录结构:

稍微编写一下lindaidai.js:

module.exports = {
  print: function () {
    console.log('lindaidai')
  }
}
console.log('lindaidai模块:', module.paths)
复制代码

然后在test.js中引入这个lindaidai模块:

// var m1 = require('./m1.js');
// console.log(m1);
// console.log(module.paths)

var lindaidai = require('lindaidai');
lindaidai.print();
复制代码

现在执行node test.js,会发现输出了:

LinDaiDaideMBP:commonJS lindaidai$ node test.js
lindaidai模块: [
  '/Users/lindaidai/codes/test/CommonJS和ES6/commonJS/node_modules',
  '/Users/lindaidai/codes/test/CommonJS和ES6/node_modules',
  '/Users/lindaidai/codes/test/node_modules',
  '/Users/lindaidai/codes/node_modules',
  '/Users/lindaidai/node_modules',
  '/Users/node_modules',
  '/node_modules'
]
lindaidai
复制代码

所以现在你可以知道,平常我们使用这种依赖的时候,它是怎样的一个查找顺序了吧,它其实就是按照自定义模块的顺序来进行查找。

文件定位:

上面👆已经介绍完了路径分析,但是还有一个问题,就是我们导入的模块它的后缀(扩展名)是可以省略的啊,那Node怎么知道我们是导入了一个js还是一个json呢?这其实就涉及到了文件定位。

在NodeJS中, 省略了扩展名的文件, 会依次补充上.js, .node, .json来尝试, 如果传入的是一个目录, 那么NodeJS会把它当成一个包来看待, 会采用以下方式确定文件名

第一步, 找出目录下的package.json, 用JSON.parse()解析出main字段

第二步, 如果main字段指定的文件还是省略了扩展, 那么会依次补充.js, .node, .json尝试.

第三部, 如果main字段制定的文件不存在, 或者根本就不存在package.json, 那么会默认加载这个目录下的index.js, index.node, index.json文件.

以上就是文件定位的过程, 再搭配上路径分析的过程, 进行排列组合, 这得有多少种可能呀. 所以说, 自定义模块的引入, 是最费性能的.

(总结来源:https://zhuanlan.zhihu.com/p/27644026)

4. CommonJS规范的特点

唔...呆呆我先把CommonJS规范的一些特点列举出来吧,然后我们再一点一点的去看例子。

  • 所有代码都运行在模块作用域,不会污染全局作用域;

  • 模块是同步加载的,即只有加载完成,才能执行后面的操作;

  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存;

  • CommonJS输出是值的拷贝(即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。

(总结来源:https://juejin.im/post/6844903983987834888)

第一点还是好理解的,咱模块的一个重要的功能不就是这个吗。

第二点同步加载,这个写个案例我们来验证一下。

同步加载案例

m1.js:

console.log('我是m1模块')
module.exports = {
    name: 'lindaidai',
    sex: 'boy'
}
复制代码

test.js

var m1 = require('./m1');
console.log('我是test模块');
复制代码

可以看到,test模块依赖于m1,且是先下载的m1模块,所以如果我执行node test.js,会有以下的执行结果:

LinDaiDaideMBP:commonJS lindaidai$ node test.js
我是m1模块
我是test模块
复制代码

这也就验证了CommonJS中,模块是同步加载的,即只有加载完成,才能执行后面的操作。

第三点模块首次执行后会缓存,我们也可以写个案例来验证一下。

模块首次执行后会缓存案例:

m1.js:

var name = 'lindaidai';
var sex = 'boy';

exports.name = name;
exports.sex = sex;
复制代码

test.js:

var m1 = require('./m1');
m1.sex = 'girl';
console.log(m1);

var m2 = require('./m1');
console.log(m2);
复制代码

test同样依赖于m1,但是我会在其中导入两次m1,第一次导入的时候修改了m1.sex的值,第二次的时候命名为m2,但是结果m1m2竟然是相等的:

LinDaiDaideMBP:commonJS lindaidai$ node test.js
{ name: 'lindaidai', sex: 'girl' }
{ name: 'lindaidai', sex: 'girl' }
复制代码

也就是说模块在首次执行后就会缓存,再次加载只返回缓存结果,这里我是用了改变m1.sex的值来证明它确实是取了缓存结果。

那么就有小伙伴会疑惑了,其实你这样写也并不能证明啊,因为你改变了m1.sex也可能是影响原本m1模块里的sex属性呀,这样的话第二次m2拿到的肯定就是被改变的值了。

唔...我正想证明来着呢。因为CommonJS的第四个特点就可以很好的解决你这个疑问。

第四点CommonJS输出是值的拷贝,也就是说你用require()引入了模块,但是你在最新的模块中怎样去改变,也不会影响你已经require()的模块。来看个案例。

CommonJS输出是值的拷贝案例

m1.js:

var name = 'lindaidai';
var sex = 'boy';
var advantage = ['handsome']

setTimeout(function () {
  sex = 'girl';
  advantage.push('cute');
}, 500)

exports.name = name;
exports.sex = sex;
exports.advantage = advantage;
复制代码

test.js:

var m1 = require('./m1');
setTimeout(function () {
  console.log('read count after 1000ms in commonjs is', m1.sex)
  console.log('read count after 1000ms in commonjs is', m1.advantage)
}, 1000)
复制代码

执行node test.js之后的执行结果是:

LinDaiDaideMBP:commonJS lindaidai$ node test.js
read count after 1000ms in commonjs is boy
read count after 1000ms in commonjs is [ 'handsome', 'cute' ]
复制代码

也就是说,在开始var m1 = require('./m1')的时候,m1已经被引入进来了,但是过了500ms后我改变了原本m1里的一些属性,sex这种基本数据类型是不会被改变的,但是advantage这种引用类型共用的还是同一个内存地址。(这种复制的关系让我想到了之前学原型链继承的时候,它那里也是,会影响Father.prototype上的引用类型)

(这也再次证明了霖呆呆真的是个boy,就算你试图改变他的性别也是不能成功的)

如果这里你是这样写的话:

m1.js:

var name = 'lindaidai';
var sex = 'boy';
var advantage = ['handsome']

setTimeout(function () {
  sex = 'girl';
  // advantage.push('cute');
  advantage = ['cute'];
}, 500)

exports.name = name;
exports.sex = sex;
exports.advantage = advantage;
复制代码

现在的执行结果肯定就是:

LinDaiDaideMBP:commonJS lindaidai$ node test.js
read count after 1000ms in commonjs is boy
read count after 1000ms in commonjs is [ 'handsome' ]
复制代码

因为相当于对m1advantage重新赋值了。

当然,或者如果你的m1.js中返回的值是会有一个函数的话,在test.js也能拿到变化之后的值了,比如这里的一个例子:

var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};
复制代码

因为在这里实际就形成了一个闭包,而counter属性就是一个取值器函数。

好滴,这基本就是CommonJS的特点了,总结就不写了,在开头已经说过了,不过对于最后一点:CommonJS输出是值的拷贝,这个对于引用类型的变量来说还是会有一点歧义的,比如上面的advantage那个例子,大家知道就行了。

AMD规范

1. 产生原因

上面介绍的CommonJS规范看起来挺好用的啊,为什么又还要有其它的规范呢?比如AMD、CMD,那它们和CommonJS又有什么渊源呢?

我们知道,模块化这种概念不仅仅适用于服务器端,客户端同样也适用。

CommonJS规范就不太适合用在客户端(浏览器)环境了,比如上面的那个例子,也就是:

test.js:

const m1 = require('./m1.js')
console.log(m1);

// 与m1模块无关的一些代码
function other () {}
other();
复制代码

这段代码放在浏览器环境中,它会如何运行呢?

  • 首先加载m1.js
  • m1.js加载完毕之后才执行后面的内容

这点其实在CommonJS规范的特点中已经提到过了。

后面的内容要等待m1加载完才会执行,如果m1加载的很慢呢?那不就照成了卡顿,这对于客户端来说肯定是不友好的。像这种要等待上一个加载完才执行后面内容的情况我们可以叫做"同步加载",很显然,这里我们更希望的是other()的执行不需要等m1加载完才执行,也就是我们希望m1它是"异步加载"的,这也就是AMD

在介绍AMD之前让我们看看CommonJS规范对服务器端和浏览器的不同,它有助于让你理解为什么说CommonJS不太适合于客户端:

  • 服务器端所有的模块都存放在本地硬盘中,可以同步加载完成,等待时间就是硬盘的读取时间。
  • 浏览器,所有的模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。

2. 定义并暴露模块

有了上面这层背景,我们就知道了,AMD它的产生很大一部分原因就是为了能让我们采用异步的方式加载模块

所以现在来让我们看看它的介绍吧。

AMDAsynchronous Module Definition的缩写,也就是"异步模块定义"。(前面的A就很好记了,它让我不自觉的就想到async这个定义异步函数的修饰符)

它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

此时就需要另一个重要的方法来定义我们的模块:define()

它其实是会有三个参数:

define(id?, dependencies?, factory)
复制代码
  • id: 一个字符串,表示模块的名称,但是是可选的
  • dependencies: 一个数组,是我们当前定义的模块要依赖于哪些模块,数组中的每一项表示的是要依赖模块的相对路径,且这个参数也是可选的
  • factory: 工厂方法,一个函数,这里面就是具体的模块内容了

坑一

那其实就有一个问题了,看了这么多的教材,但我想要去写案例的时候,我以为这个define能直接像require一样去用,结果发现控制台一直再报错:

ReferenceError: define is not defined
复制代码

看来它还并不是Node.js自带的一个方法啊,搜寻了一下,原来它只是名义上规定的这样一个方法,但是你真的想要去用还是得使用对应的JavaScript库,也就是我们常常听到的:

目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。

我酸了...

让我们去requirejs的官网看看如何使用它,由于我的案例都是在Node执行环境中,于是我采用npm install的方式来下载了:

我新建了一个叫AMD的文件夹,作为AMD的案例。

在项目的根目录下执行:

npm i requirejs
复制代码

(找了一圈NPM也没看到能使用CDN远程引入的)

执行完毕之后,项目的根目录下出现了依赖包,打开看了看,确实是下载下来了:

现在可以开心的在项目里用define()了 😊。

来看个小例子,我重新定义了一个math.js

math.js

define(function () {
  var add = function (a, b) {
    return a + b;
  }
  return {
    add: add
  }
})
复制代码

这里模块很简单,导出了一个加法函数。

(至于这里为什么add: add要这样写,而不是只简写为add呢?别忘了这种对象同名属性简写是ES6才出来的哦)

3. 引用模块

坑二

OK👌,既然模块已经能导出了,那就让我们来看看如何引用吧,依照着教材,我在test.js中引入了math模块并想要调用add()方法:

test.js:

require(['math'],function(math) {
  console.log(math)
  console.log(math.add(1, 2));
})
复制代码

之后熟练的执行node test.js

我酸了...

又报错了,擦...

 throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
 TypeError [ERR_INVALID_ARG_TYPE]: The "id" argument must be of type string. Received an instance of Array
复制代码

确认了一下,和教材们中的写法一样啊,第一个参数为要加载的模块数组,第二个参数为加载完之后的回调。

难受😣...原来上面👆require([modules], callback)这样的写法它和define一样都只是个噱头,如果你真得用的话,还是得用JavaScript库中的方法。

由于上面已经安装过requirejs了,这里我直接使用就可以了,现在我修改了一下test.js文件:

var requirejs = require("requirejs"); //引入requirejs模块

requirejs(['math'],function(math) {
  console.log(math)
  console.log(math.add(1, 2));
})
复制代码

好了,现在执行node test.js就可以正常使用了...

(很难受...感觉明明已经是很常见耳熟能详的一些知识了,真的要去用的时候发现和很多教材中说的不是那么一回事...也希望大家在看完了一些教材之后最好能亲自去实践一下,因为呆呆自己也是写博客的,所以也知道有些时候一些知识点可能也是从别人的文章那里看来但是没有经过实践的,所以最好也还是自己动动手)

4. 依赖其它模块的define

可以看到define它还有另外两个参数的,第一个是模块的名称,没啥好说的,让我们来看看第二个它所依赖的模块。

还记得在CommonJS规范那里我们写了一个m1.js吗?现在就让我们把这个模块拿来用下,把它作为math.js中的一个依赖。

m1.js:

console.log('我是m1, 我被加载了...')
module.exports = {
    name: 'lindaidai',
    sex: 'boy'
}
复制代码

然后修改一下math.js

math.js:

define(['m1'], function (m1) {
  console.log('我是math, 我被加载了...')
  var add = function (a, b) {
    return a + b;
  }
  var print = function () {
    console.log(m1.name)
  }
  return {
    add: add,
    print: print
  }
})
复制代码

另外,为了方便大家看,我们再来修改一下刚刚的test.js

var requirejs = require("requirejs"); //引入requirejs模块

requirejs(['math'],function(math) {
  console.log('我是test, 我被加载了...')
  console.log(math.add(1, 2));
  math.print();
})
function other () {
  console.log('我是test模块内的, 但是我不依赖math')
};
other();
复制代码

所以我们可以看到,依赖关系依次为:

test -> math -> m1
复制代码

如果按照AMD的规范,模块的加载需要依靠前一个模块加载完才会执行回调函数内的内容,那么我们可以想象当我在终端输入node test.js的时候,要出现的结果应该是:

LinDaiDaideMBP:commonJS lindaidai$ node test.js
我是test模块内的, 但是我不依赖math
我是m1, 我被加载了...
我是math, 我被加载了...
我是test, 我被加载了...
3
lindaidai
复制代码

(这个,相信大家应该都看清了彼此的依赖关系吧😢)

但是现实总是那么的残酷,当我按下回车的时候,又报错了...

再酸...

 ReferenceError: module is not defined
复制代码

看了一下这个报错的内容,是在m1.js中...呆了几秒钟反应了过来...

既然是使用AMD的规范,那我们肯定是要一统到底了,m1.js中用的还是CommonJS的规范,当然不行了。

OK,来修改一下m1.js

m1.js:

define(function () {
  console.log('我是m1, 我被加载了...')
  return {
    name: 'lindaidai',
    sex: 'boy'
  }
})
复制代码

OK👌,这次没啥问题了,按照我们预期的去执行了...😊。

(当然据我的了解,requirejs还可用于在script中引用然后定义网页程序的主模块等使用,可以看一下:

http://www.ruanyifeng.com/blog/2012/11/require_js.html)

AMD的知识点大概就介绍到了这里,相信大家也知道它的基本使用了吧,至于其中的一些区别什么的我在最后也会列一份清单,不过现在让我们先来看看CMD吧。

CMD规范

CMD (Common Module Definition), 是seajs推崇的规范,CMD则是依赖就近,用的时候再require。

来看段代码,大概感受一下它是怎样用的:

define(function(require, exports, module) {
  var math = require('./math');
  math.print()
})
复制代码

看着和AMD有点像的,没错,其实define()的参数甚至都是一样的:

define(id?, dependencies?, factory)
复制代码

但是区别在于哪里呢?让我们来看看最后一个factory它参数。

factory函数中是会接收三个参数:

  • require

  • exports

  • module

    这三个很好理解,对应着之前的CommonJS那不就是:

  • require:引入某个模块

  • exports:当前模块的exports,也就是module.exports的简写

  • module:当前这个模块

现在再来说说AMDCMD的区别。

虽然它们的define()方法的参数都相同,但是:

  • AMD中会把当前模块的依赖模块放到dependencies中加载,并在factory回调中拿到加载成功的依赖
  • CMD一般不在dependencies中加载,而是写在factory中,使用require加载某个依赖模块

因此才有了我们常常看到的一句话:

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

(好吧,仔细读了2遍感觉还是没太明白,没事,后面呆呆还会详细说到)

比较有名一点的,seajs,来看看它推荐的CMD 模块书写格式吧:

// 所有模块都通过 define 来定义
define(function(require, exports, module) {

  // 通过 require 引入依赖
  var $ = require('jquery');
  var Spinning = require('./spinning');

  // 通过 exports 对外提供接口
  exports.doSomething = ...

  // 或者通过 module.exports 提供整个接口
  module.exports = ...

});
复制代码

这是官网的一个小案例,我也去seajs的文档中看了一下没啥太大问题,这里就不举例了。

AMD和CMD的区别

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

还是上面那句话,让我们来看个小例子理解一下。

同样是math模块中需要加载m1模块。

AMD中我们会这样写:

math.js

define(['m1'], function (m1) {
  console.log('我是math, 我被加载了...')
  var add = function (a, b) {
    return a + b;
  }
  var print = function () {
    console.log(m1.name)
  }
  return {
    add: add,
    print: print
  }
})
复制代码

但是对于CMD,我们会这样写:

math.js

define(function (require, exports, module) {
  console.log('我是math, 我被加载了...')
  var m1 = require('m1');
  var add = function (a, b) {
    return a + b;
  }
  var print = function () {
    console.log(m1.name)
  }
  module.exports = {
    add: add,
    print: print
  }
})
复制代码

假如此时m1.js中有一个语句是在m1模块被加载的时候打印出"我是m1, 我被加载了..."

执行结果区别:

  • AMD,会先加载m1"我是m1"会先执行
  • CMD,我是"我是math"会先执行,因为本题中console.log('我是math, 我被加载了...')是放在require('m1')前面的。

现在可以很明显的看到区别了。

AMD依赖前置,js很方便的就知道要加载的是哪个模块了,因为已经在definedependencies参数中就定义好了,会立即加载它。

CMD是就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块。

OK👌,来看个总结:

两者之间,最明显的区别就是在模块定义时对依赖的处理不同

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

ES6 Modules规范

ES6标准出来后,ES6 Modules规范算是成为了前端的主流吧,以import引入模块,export导出接口被越来越多的人使用。

下面,我也会从这么几个方面来介绍ES6 Modules规范:

export命令和import命令可以出现在模块的任何位置,只要处于模块顶层就可以。 如果处于块级作用域内,就会报错,这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。

1. export导出模块

export有两种模块导出方式:

  • 命名式导出(名称导出)
  • 默认导出(自定义导出)

命名式导出

来看几种正确和错误的写法吧:

// 以下两种为错误
// 1.
export 1;
// 2.
const a = 1;
export a;

// 以下为正确
// 3.
const a = 1;
export { a };

// 4. 接口名与模块内部变量之间,建立了一一对应的关系
export const a = 1, b = 2;

// 5. 接口名与模块内部变量之间,建立了一一对应的关系
export const a = 1;
export const b = 2;

// 或者用 as 来命名
const a = 1;
export { a as outA };

const a = 1;
const b = 2;
export { a as outA, b as outB };
复制代码

容易混淆的可能是24两种写法了,看着很像,但是2却不行。2直接导出一个值为1的变量是和情况一一样,没有什么意义,因为你在后面要用的时候并不能完成解构。

但是4中,接口名与模块内部变量之间,建立了一一对应的关系,所以可以。

默认导出

默认导出会在export后面加上一个default

// 1.
const a = 1;
export default a;

// 2.
const a = 1;
export default { a };

// 3.
export default function() {}; // 可以导出一个函数
export default class(){}; // 也可以出一个类
复制代码

其实,默认导出可以理解为另一种形式上的命名导出,也就是说a这个属性名相当于是被我重写了成了default

const a = 1;
export defalut a;
// 等价于
export { a as default }
复制代码

所以,我们才可以用const a = 1; export default a;这种方式导出一个值。

2. import导入模块

import模块导入与export模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和默认导入(定义式导入)。

来看看写法:

// 某个模块的导出 moudule.js
export const a = 1;

// 模块导入
// 1. 这里的a得和被加载的模块输出的接口名对应
import { a } from './module'

// 2. 使用 as 换名
import { a as myA } from './module'

// 3. 若是只想要运行被加载的模块可以这样写,但是即使加载2次也只是运行一次
import './module'

// 4. 整体加载
import * as module from './module'

// 5. default接口和具名接口
import module, { a } from './module'
复制代码

第四种写法会获取到module中所有导出的东西,并且赋值到module这个变量下,这样我们就可以用module.a这种方式来引用a了。

3. export ... from...

其实还有一种写法,可以将exportfrom结合起来用。

例如,我有三个模块a、b、c

c模块现在想要引入a模块,但是它不不直接引用a,而是通过b模块来引用,那么你可能会想到b应该这样写:

import { someVariable } from './a';

export { someVariable };
复制代码

引入someVariable然后再导出。

这还只是一个变量,我们得导入再导出,若是有很多个变量需要这样,那无疑会增加很多代码量。

所以这时候可以用下面这种方式来实现:

export { someVariable } from './a';
复制代码

不过这种方式有一点需要注意:

  • 这样的方式不会将数据添加到该聚合模块的作用域, 也就是说, 你无法在该模块(也就是b)中使用someVariable

4. ES6 Modules规范的特点

总结一下它的特点哈:

  • 输出使用export
  • 输入使用import
  • 可以使用export...from...这种写法来达到一个"中转"的效果
  • 输入的模块变量是不可重新赋值的,它只是个可读引用,不过却可以改写属性
  • export命令和import命令可以出现在模块的任何位置,只要处于模块顶层就可以。 如果处于块级作用域内,就会报错,这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。
  • import命令具有提升效果,会提升到整个模块的头部,首先执行。

5. Bable下的ES6模块转换

还有一点就是,如果你有使用过一些ES6的Babel的话,你会发现当使用export/import的时候,Babel也会把它转换为exports/require的形式。

例如我的输出:

m1.js:

export const count = 0;
复制代码

我的输入:

index.js:

import {count} from './m1.js'
console.log(count)
复制代码

当使用Babel编译之后,各自会被转换为:

m1.js:

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.count = void 0;
const count = 0;
exports.count = count;
复制代码

index.js:

"use strict";

var _m = require("./m1.js");

console.log(_m.count);
复制代码

正是因为这种转换关系,才能让我们把exportsimport结合起来用:

也就是说你可以这样用:

// 输出模块 m1.js
exports.count = 0;

// index.js中引入
import {count} from './m1.js'
console.log(count)
复制代码

CommonJS与ES6 Modules规范的区别

😂,我相信很多人就比较关心它两区别的问题,因为基本上面试问的就是这个。好吧,这里来做一个算是比较详细的总结吧。

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

  • CommonJS输出是值的拷贝;ES6 Modules输出的是值的引用,被输出模块的内部的改变会影响引用的改变

  • CommonJs导入的模块路径可以是一个表达式,因为它使用的是require()方法;而ES6 Modules只能是字符串

  • CommonJS this指向当前模块,ES6 Modules this指向undefined

  • 且ES6 Modules中没有这些顶层变量:argumentsrequiremoduleexports__filename__dirname

关于第一个差异,是因为CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

(应该还一些区别我没想到的,欢迎补充👏😊)

参考文章

知识无价,支持原创。

参数文章:

后语

你盼世界,我盼望你无bug。这篇文章就介绍到了这里。

唔...怎么说呢,虽然最后的总结记都很好记,但是如果不深入理解的话就只能知道一些浅显的知识,那样是不可以的哦。

(霖呆呆,你叫别人深入了解,你自己了解多少呢?)

喜欢霖呆呆的小伙还希望可以关注霖呆呆的公众号 LinDaiDai 或者扫一扫下面的二维码👇👇👇.

我会不定时的更新一些前端方面的知识内容以及自己的原创文章🎉

你的鼓励就是我持续创作的主要动力 😊.

相关推荐:

《全网最详bpmn.js教材》

《【建议改成】读完这篇你还不懂Babel我给你寄口罩》

《【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理)》

《【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)》

《【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)》

《【何不三连】做完这48道题彻底弄懂JS继承(1.7w字含辛整理-返璞归真)》

《【精】从206个console.log()完全弄懂数据类型转换的前世今生(上)》

本文使用 mdnice 排版