Loading

JS模块化规范

前言

在ES6之前,JavaScript并未提供一种原生的、语言级别的模块化组织模式,而是将模块化的方法交由开发者来实现。因此出现了很多种JavaScript模块化的实现方式,比如,CommonJS、AMD、CMD等

一、原始模拟模块的一些写法

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

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

  <script>
        function m1() {}
        function m2() {}
  </script>

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

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

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

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

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

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

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

利用立即执行函数内的作用域以及闭包来实现模块功能,导出我们想要导出的成员。此时外部代码就不能读取到_sum了。

二、CommonJS规范

CommonJS是一个有志于构建JavaSctipt生态圈的组织,整个社区致力于提高JavaScript程序的可移植性和可交换性,无论在服务端还是浏览器端。

Node.js采用了这个规范。注意在浏览器中是不兼容CommonJS的,因为浏览器中缺少四个Node.js环境的变量(module, exports, require, global),只要能够提供这四个变量,浏览器就能加载CommonJS模块,Browerify是目前最常用的CommonJS格式转换工具。
下面从以下四个方面介绍CommonJS:
* 暴露模块
* 引用模块
* 模块标识符
* CommonJS规范的特点

1.暴露(定义)模块

正确的暴露方式:

  暴露模块有两种方式:
  • module.exports = {}
  • exports.xxx = 'xxx'
    例如有一个m1.js文件:

第一种暴露方式:

  module.exports = {
        name: 'aaa',
        sex: 'boy'
  }

第二种暴露方式:

  exports.name = 'aaa';
  exports.sex = 'boy'

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

node为每个模块提供了一个exports变量(可以说是一个对象),指向module.exports。这相当于每个模块中都有一句这样的命令var exports = module.exports;
这样,在对外输出时,可以在这个变量上添加方法或属性。

但是要注意,不能把exports直接指向一个值,这样就相当于切断了exports和module.exports的关系。

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

注意这个全局方法是node中的方法,它不是window下面的。
如果你没做任何处理直接在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()的参数甚至能允许你是一个表达式。也就是说你可以把它设置为一个变量:

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

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

  D:\study\JS\testjs>node test.js
  [ 'D:\\study\\JS\\testjs\\node_modules',
    'D:\\study\\JS\\node_modules',
    'D:\\study\\node_modules',
    'D:\\node_modules' ]

这其实就是自定义模块的查找顺序。

文件定位:

上面已经介绍完路径分析,但是还有一个问题,就是我们导入的模块它的后缀(扩展名)是可以省略的啊,那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文件。

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

4.CommonJS规范的特点

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

第一点还是还是好理解的,模块的一个重要功能不就是这个吗
第二点同步加载,这个写个案例来验证一下。

同步加载案例
m1.js:

  console.log("我是m1模块")
  module.exports = {
        name: "m1",
        sex: "boy"
  }

test.js

  var m1 = require('./m1.js');
  console.log('我是test模块');

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

  我是m1模块
  我是test模块

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

第三点 模块首次执行后会缓存,也可以验证一下。

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

  var name = 'm1';
  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却是相等的:

  { name: 'lindaidai', sex: 'girl' }
  { name: 'lindaidai', sex: 'girl' }

也就是说模块在首次执行后就会缓存,再次加载只返回缓存结果。

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

第四点 CommonJS输出是值的拷贝,也就是说一旦输出一个值,模块内部的变化就影响不到这个值。

CommonJS输出是值的拷贝案例

m1.js:

  var name = 'm1';
  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)
  })

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

  read count after 1000ms in commonjs is boy
  read count after 1000ms in commonjs is [ 'handsome', 'cute' ]

也就是说,在m1被引入之后,过了500ms后我改变了m1里的一些属性,sex这种基本数据类型是不会被改变的,但是advantage这种引用类型共用的还是同一个内存地址。

如果你这样写的话:
m1.js:

  var name = 'm1';
  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;

那执行结果肯定就是:

  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属性就是一个取值器函数。

三、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的官网看看如何使用它,

方式1:直接下载require.js文件

我们新建一个叫AMD的文件夹,作为AMD的案例,把require.js文件放在该目录下,就可以加载了。

  <script src="./require.js"></script>

有人可能会想到,加载这个文件,也可能会造成网页失去响应,解决办法有两个,一是把它放在底部加载,另一个是写成下面这样:

  <script src="./require.js" defer async="true"></script>

async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上。加载require.js以后,下一步就是加载我们自己的代码了。

假定我们自己的代码文件为test.js,也放在amd目录下面,那么只需简写下面这样就行了:

  <script src="./require.js" data-main="./test"></script>

data-main属性的作用是,指定网页程序的主模块,在上例中,就是amd目录下的test.js文件,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写main。

实例如下:
新建amd文件夹,并下载require.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文件并引入math.js模块:

  require(['math'], function(math){
    console.log('我是test,我被加载了。。。')
    console.log(math.add(1, 2))
    math.print()
  })
  function other() {
    console.log("我是test模块内的,但是我不依赖math")
  }
  other()

接下来在test.html文件引入我们下载的require.js文件并引入执行test.js文件

    <script src="./require.js"></script>
    <script src="./test.js"></script>

我们可以在浏览器控制台看到输出结果:

  我是test模块内的,但是我不依赖math
  我是m1,我被加载了。。。
  我是math,我被加载了。。。
  我是test,我被加载了。。。
  3
  m1

方式2:使用npm install方式

这个案例我们在Node环境中测试,在amd目录下,执行

  npm i requirejs

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

math.js

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

这个模块很简单,导出了一个加法函数。
(至于这里为什么add: add要这样写,而不是只简写add呢?别忘了这种对象同名属性简写是ES6才出来的)

test.js文件

  var require = require('requirejs')  // 注意要再手动引入一下


require(['math'], function(math){
console.log('我是test,我被加载了。。。')
console.log(math.add(1, 2))
})
function other() {
console.log("我是test模块内的,但是我不依赖math")
}
other()
在node中执行

  D:\study\JS\amd>node test.js
  我是test模块内的,但是我不依赖math
  我是m1,我被加载了。。。
  我是math,我被加载了。。。
  我是test,我被加载了。。。
  3

3.引用模块

上面已经使用了,也就是require,基本语法就是:

  require([dependencies], function(){});

require()函数接受两个参数

  • 第一个参数是一个数组,表示所依赖的模块。
  • 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将调用,加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。

require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题

四、CMD规范

CMD(Common module Definition)是seajs推崇的规范。

CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器实现的SeaJSSeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)实际上有所不同,CMD则是依赖就近,用的时候再require

CMD语法

Sea.js推崇一个模块一个文件,遵循统一的写法,看段代码感受一下它是怎么用的:

  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: 当前这个模块
    现在再来说说AMD和CMD的区别。

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

  • AMD中会把当前模块的依赖模块放到dependencies中加载,并在factory回调中拿到加载成功的依赖。

  • CMD一般不在dependencies中加载,而是写在factory中,使用require加载某个依赖模块。

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

  // 所有的模块都通过define来定义
  define(function(require, exports, module) {
        
        // 通过require引入依赖
        var $ = require('jquery');
        var spinning = require('./spinning');

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

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

五、AMD与CMD的区别

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

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

同样是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是就近依赖,需要把模块变为字符串解析一遍才知道依赖了哪些模块。

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

六、ES6 Module规范

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

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

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

1、export导出模块
export有两种方式导出模块:

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

命名式导出
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取,如果你希望外部能读取模块内部的某个变量,就必须使用export关键字输出该变量,下面是一个js文件,里面使用export命令输出变量。

  // profile.js
  export var firstName = 'Michael';
  export var lastName = 'Jackson';
  export var year = 2020;

上面代码是profile.js文件,保存了用户信息。ES6将其视为一个模块,里面export命令对外部输出三个变量。

export的写法除了像上面那样,还有另外一种。

  // profile.js
  var firstName = 'Michel';
  var lastName = 'Jackson';
  var year = 2020;

  export {firstName, lastName, year};

上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。

export命令除了输出变量,还可以输出函数或类(class)。

  // 输出了一个函数multiply
  export function multiply(x, y) {
        return x * y;
  }

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

  function v1(){}
  function v2(){}

  export { v1 as a1, v2 as a2 }

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

  // 报错
  export 1;

  // 报错
  var m = 1;
  export m;

上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出1,第二种写法通过变量m,还是直接输出1。1只是一个值,不是接口。正确的写法是下面这样。

  // 写法一
  export var m = 1;
  
  // 写法二
  var m = 1;
  export {m};

  // 写法三
  var m = 1;
  export { n as m };

上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

同样的,function和class的输出,也必须遵守这样的写法。

  // 报错
  function f() {}
  export f;

  // 正确
  export function f() {};

  // 正确
  function f() {}
  export {f}

默认导出
在export后面加上一个default就是默认导出:

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

  //2.
  const a = 1
  export default { a };
  
  //3.
  export default function() {}; // 可以导出一个函数
  export default class{}; // 也可以是一个类

其实,默认导出可以理解为另一种形式上的命名导出,也即是导出模块的属性名可以省略不写(相当于被重写成了default),当我import的时候可以任意命名。

  const a = 1;
  export default a;
  
  // 等价于
  export { a as defaulf }

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

来看看写法:

  // 某个模块的导出module.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 './mudule'

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

3.export...from ...

其实还有一种写法,可以将exportfrom结合起来用。
例如,我有三个模块a、b、c

c模块现在想要引入a模块,但是它不直接引用a,而是通过b模块来引用,那么你可能会想到b引用a再把a输出:

  //b.js
  import { someVarible } from './a';
  
  export { 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);

正是因为这种转换关系,才能让我们把exportimport结合起来用:
也就是说你可以这样用:

  // 输出模块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模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

参考:
https://javascript.ruanyifeng.com/nodejs/module.html
https://juejin.cn/post/6844904145443356680
https://www.cnblogs.com/dolphinX/p/4381855.html

posted @ 2021-01-01 15:27  Yang-0394  阅读(179)  评论(0)    收藏  举报