js当中CommonJS 和es6的模块化引入方案以及比较

    在es6之前,对于模块化方案主要是CommonJS和AMD两种。咱们这次说一下ES6和CommonJS的区别。

 

    它们有两个重大差异:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

    第一个差异:

    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的变动了。

 

    ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

还是举上面的例子。

    // lib.js

    export let counter = 3;

    export function incCounter() {

      counter++;

    }

 

    // main.js

    import { counter, incCounter } from './lib';

    console.log(counter); // 3

    incCounter();

    console.log(counter); // 4

    上面代码说明,ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。

    再举一个出现在export一节中的例子。

    // m1.js

    export var foo = 'bar';

    setTimeout(() => foo = 'baz', 500);

 

    // m2.js

    import {foo} from './m1.js';

    console.log(foo);

    setTimeout(() => console.log(foo), 500);

    上面代码中,m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于baz。

    让我们看看,m2.js能否正确读取这个变化。

    $ babel-node m2.js

 

    bar

    baz

    上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

    由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。

    // lib.js

    export let obj = {};

 

    // main.js

    import { obj } from './lib';

 

    obj.prop = 123; // OK

    obj = {}; // TypeError

    上面代码中,main.js从lib.js输入变量obj,可以对obj添加属性,但是重新赋值就会报错。因为变量obj指向的地址是只读的,不能重新赋值,这就好比main.js创造了一个名为obj的const变量。

 

    第二个差异:

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

    Es6模块的设计思想是尽量放入静态化,使得在编译时就能确定依赖关系,而CommonJS就只能在运行时确定这些输入和输出的变量。

    // CommonJS模块

    let { stat, exists, readFile } = require('fs');

 

    // 等同于

    let _fs = require('fs');

    let stat = _fs.stat;

    let exists = _fs.exists;

    let readfile = _fs.readfile;

    上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 通过export命令显式指定输出的代码,再通过import命令输入。

    // ES6模块

    import { stat, exists, readFile } from 'fs';

    上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。

 

    在es6中,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

    export var foo = 'bar';

    setTimeout(() => foo = 'baz', 500);

    上面代码输出变量foo,值为bar,500 毫秒之后变成baz。

    这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新。

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

    function foo() {

      export default 'bar' // SyntaxError

    }

    foo()

    上面代码中,export语句放在函数之中,结果报错。

    注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。

    foo();

 

    import { foo } from 'my_module';

    上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

    由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

    // 报错

    import { 'f' + 'oo' } from 'my_module';

 

    // 报错

    let module = 'my_module';

    import { foo } from module;

 

    // 报错

    if (x === 1) {

      import { foo } from 'module1';

    } else {

      import { foo } from 'module2';

    }

    上面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是没法得到值的。

 

    这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能。

    const path = './' + fileName;

    const myModual = require(path);

    上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。import命令做不到这一点。

    因此,有一个提案,建议引入import()函数,完成动态加载。

    import(specifier)

    上面代码中,import函数的参数specifier,指定所要加载的模块的位置。import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。

    import()返回一个 Promise 对象。下面是一个例子。

    const main = document.querySelector('main');

 

    import(`./section-modules/${someVariable}.js`)

      .then(module => {

        module.loadPageInto(main);

      })

      .catch(err => {

        main.textContent = err.message;

      });

    import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

 

 

    目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。

    require('core-js/modules/es6.symbol');

    require('core-js/modules/es6.promise');

    import React from 'React';

 

 

注:

    import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

    import {a} from './xxx.js'

 

    a = {}; // Syntax Error : 'a' is read-only;

    上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

    import {a} from './xxx.js'

 

    a.foo = 'hello'; // 合法操作

    上面代码中,a的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,轻易不要改变它的属性。

 

更多文章jacktesla的博客

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值