es6Module與CommonJS

什麼是模塊化

在瞭解es6Module與RequireJS以前,仍是須要先來簡單地瞭解下什麼是模塊化,模塊化開發node

模塊化是指在解決某一個複雜問題或者一系列的雜糅問題時,依照一種分類的思惟把問題進行系統性的分解以之處理。模塊化是一種處理複雜系統分解爲代碼結構更合理,可維護性更高的可管理的模塊的方式。es6

做爲一個模塊化系統所必須的能力:瀏覽器

  1. 定義封裝的模塊
  2. 定義新模塊對其餘模塊的依賴
  3. 可對其餘模塊的引入支持

歷史上,JavaScript 一直沒有模塊(module)體系,沒法將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼裝起來。其餘語言都有這項功能,好比 Ruby 的require、Python 的import,甚至就連 CSS 都有@import,可是 JavaScript 任何這方面的支持都沒有,這對開發大型的、複雜的項目造成了巨大障礙。緩存

因此社區制定了CommonJs規範,Node 從 Commonjs 規範中借鑑了思想因而有了 Node 的 module,而 AMD 異步模塊 也一樣脫胎於 Commonjs 規範,以後有了運行在瀏覽器上的 require.js。服務器

ES6 在語言標準的層面上,實現了模塊功能,並且實現得至關簡單,徹底能夠取代 CommonJS 和 AMD 規範,成爲瀏覽器和服務器通用的模塊解決方案。異步

CommonJS

Node.js是commonJS規範的主要實踐者,它有四個重要的環境變量爲模塊化的實現提供支持:module、exports、require、global。實際使用時,用module.exports定義當前模塊對外輸出的接口,用require加載模塊。模塊化

// 定義模塊 area.js
function area(radius) {
  return Math.PI * radius * radius;
}

// 在這裏寫上須要向外暴露的函數、變量
module.exports = { 
  area: area
}

// 引用自定義的模塊時,參數包含路徑
var math = require('./math');
math.area(2);

可是咱們並無直接定義 module、exports、require這些模塊,以及 Node 的 API 文檔中提到的__filename、__dirname。那麼是從何而來呢?其實在編譯的過程當中,Node 對咱們定義的 JS 模塊進行了一次基礎的包裝:函數

(function(exports, require, modules, __filename, __dirname)) {
  ...
})

這樣咱們即可以訪問這些傳入的arguments以及隔離了彼此的做用域。CommonJS 的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。工具

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

之後須要用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。commonJS用同步的方式加載模塊,只有在代碼執行到require的時候,纔回去執行加載。ui

CommonJS規範規定,每一個模塊內部,module變量表明當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,實際上是加載該模塊的module.exports屬性。

modules對象具備如下屬性

  • id:當前模塊的bi
  • exports:表示當前模塊暴露給外部的值
  • parent: 是一個對象,表示調用當前模塊的模塊
  • children:是一個對象,表示當前模塊調用的模塊
  • filename:模塊的絕對路徑
  • paths:從當前文件目錄開始查找node_modules目錄;而後依次進入父目錄,查找父目錄下的node_modules目錄;依次迭代,直到根目錄下的node_modules目錄
  • loaded:一個布爾值,表示當前模塊是否已經被徹底加載

CommonJS特性

  • 全部代碼都運行在模塊做用域,不會污染全局做用域
  • 模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。

    下面是Module._load的源碼

Module._load = function(request, parent, isMain) {

  //  計算絕對路徑
  var filename = Module._resolveFilename(request, parent);

  //  第一步:若是有緩存,取出緩存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;

  // 第二步:是否爲內置模塊
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // 第三步:生成模塊實例,存入緩存
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // 第四步:加載模塊
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // 第五步:輸出模塊的exports屬性
  return module.exports;
};
  • 模塊加載的順序,按照其在代碼中出現的順序。
  • CommonJS模塊輸出的是一個值的拷貝。
// exportDemo.js
count = 1;
module.exports.count = count;
module.exports.Hello = function() {
  var name;
  this.setName = function(newName) {
    name = newName;
  }
  this.sayHello = function() {
    console.log("hello Mr." + name);
  }
  this.getId = function() {
    return count++
  }
}

var { Hello, count } = require('./exportDemo')
var hello = new Hello();
// 讓count自增
console.log(hello.getId());
console.log(hello.getId());
// 發現獲取的count仍是原值
console.log(count)

// 真正的count實際上是已經改了的
var newHello = new Hello();
console.log(newHello.getId())

var { Hello: newHello, count: newCount } = require('./exportDemo')
console.log(newCount, 'newCount');
// 再次require,取得的newHello和以前require的Hello指向同一個拷貝
console.log(newHello === Hello)
  • CommonJS模塊是運行時加載。

es6 Modules

es6在語言標準的層面上,實現了模塊功能。

ES6 模塊不是對象,而是經過export命令顯式指定輸出的代碼,再經過import命令輸入。

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

上面代碼的實質是從fs模塊加載 3 個方法,其餘方法不加載。這種加載稱爲「編譯時加載」或者靜態加載,即 ES6 能夠在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。固然,這也致使了無法引用 ES6 模塊自己,由於它不是對象。

因爲 ES6 模塊是編譯時加載,使得靜態分析成爲可能。有了它,就能進一步拓寬 JavaScript 的語法,好比引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。

除了靜態加載帶來的各類好處,ES6 模塊還有如下好處。

  • 再也不須要UMD模塊格式了,未來服務器和瀏覽器都會支持 ES6 模塊格式。目前,經過各類工具庫,其實已經作到了這一點。
  • 未來瀏覽器的新 API 就能用模塊格式提供,再也不必須作成全局變量或者navigator對象的屬性。
  • 再也不須要對象做爲命名空間(好比Math對象),將來這些功能能夠經過模塊提供。

es6 Modules特性

  • ES6 Modules 輸出的是值的引用
// exportDemo.mjs
export let counter = 1;
export function incCounter() {
  counter ++;
}

// importDemo.mjs
import { counter, incCounter } from './exportDemo.mjs'

incCounter();
console.log(counter)        // 打印結果爲2,而不是初始值的1
  • ES6模塊是編譯時加載

動態import()

這是一個從靜態到動態導入轉換的例子

// STATIC
import './a.js';

import b from './b.js';
b();

import {c} from './c.js';
c();

// DYNAMIC
import('./a.js').then(()=>{
  console.log('a.js is loaded dynamically');
});

import('./b.js').then((module)=>{
  const b = module.default;
  b('isDynamic');
});

import('./c.js').then(({c})=>{
  c('isDynamic');
});
  • 動態的 import() 提供一個基於Promise的API
  • import() 遵循ES模塊規則:singleton,說明符,CORS等.
  • import() 能夠在經典腳本和模塊腳本中使用
  • 在代碼中使用的import()的順序與它們被解析的順序沒有什麼共同之處

動態import()給咱們提供了用異步方式使用ES模塊的額外功能。可讓咱們根據咱們的須要動態或有條件地加載它們。

二者的主要區別

CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用

  1. CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。
  2. ES6 Modules 的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。換句話說,ES6 的 import 有點像 Unix 系統的「符號鏈接」,原始值變了,import加載的值也會跟着變。所以,ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。

CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

  1. 運行時加載: CommonJS 模塊就是對象;即在輸入時是先加載整個模塊,生成一個對象,而後再從這個對象上面讀取方法,這種加載稱爲「運行時加載」。
  2. 編譯時加載: ES6 模塊不是對象,而是經過 export 命令顯式指定輸出的代碼,import時採用靜態命令的形式。即在import時能夠指定加載某個輸出值,而不是加載整個模塊,這種加載稱爲「編譯時加載」

CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

ES6 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當前模塊

如下這些頂層變量在ES6模塊之中都是不存在的

  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname
相關文章
相關標籤/搜索