CommonJS、AMD、CMD、ES6 模块规范讲解

目标

经过阅读本篇文章你能够学习到:javascript

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

模块化原始写法

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

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

<script>  
  function m1 () {  
    // ...  
  }  
  function m2 () {  
    // ...  
  }  
</script>
缺点:污染了全局变量,没法保证不会与其它模块发生冲突,并且模块成员之间看不出直接关系。

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

对象写法 为了解决上面的缺点,能够把模块写成一个对象,全部的模块成员都放到这个对象里面。前端

index.htmljava

<script>  
  var module1 = new Object({  
    _sum: 0,  
    foo1: function () {},  
    foo2: function () {}  
  })  
</script>
缺点:会暴露全部模块成员,内部的状态可能被改写。

例如,咱们若是只是想暴露出两个方法而不暴露出 _sum,就作不到。node

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

module1._sum = 2;

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

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

利用当即执行函数内的做用域已经闭包来实现模块功能,导出咱们想要导出的成员。面试

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

console.log(module1._sum) // undefined

CommonJS规范

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

主要是从这四个方面说:segmentfault

  • 暴露模块
  • 引用模块
  • 模块标识符
  • 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');

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/...

4. CommonJS规范的特色

我先把CommonJS规范的一些特色列举出来吧,而后咱们再一点一点的去看例子。

  • 全部代码都运行在模块做用域,不会污染全局做用域;
  • 模块是同步加载的,即只有加载完成,才能执行后面的操做;
  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,若是想要再次执行,可清除缓存;
  • CommonJS输出是值的拷贝(即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。

(总结来源:https://juejin.im/post/5db95e...

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

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

同步加载案例

_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上的引用类型)

备注 其实这里的拷贝是指 JavaScript 的浅拷贝,若是对于 JavaScript 的深浅拷贝有疑问,能够参考 JavaScript 的浅拷贝和深拷贝

若是这里你是这样写的话:

_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/blo..._js.html)

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

CMD规范

CMD (Common Module Definition), 是seajs推崇的规范,依赖就近,用的时候再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👌,来看个总结:

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

一、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块二、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只能是字符串
  • CommonJSthis指向当前模块,ES6 Modulesthis指向undefined
  • 且ES6 Modules中没有这些顶层变量:argumentsrequiremoduleexports__filename__dirname

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

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

参考文章

知识无价,支持原创。

参数文章:

  • 《这几个概念你可能仍是没搞清require、import和export》
  • 《前端模块化,AMD与CMD的区别》
  • 《node.js中使用define和require》
  • 《必需要知道的CommonJS和ES6 Modules规范》
  • 《再次梳理AMD、CMD、CommonJS、ES6 Module的区别》
  • 《阮一峰-Module 的加载实现》
相关文章
相关标签/搜索