CommonJS和es6的區別


它們有兩個重大差異:

  • 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的屬性可以成功改寫,並且其他模塊也可以讀到改寫后的值。不過,這種寫法很難查錯,建議凡是輸入的變量,都當作完全只讀,輕易不要改變它的屬性。

 

 



   參考:https://blog.csdn.net/jackTesla/java/article/details/80796936


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM