亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

ES6+ 模塊化擴展

1. 前言

要深入前端學習時繞不開的是 Node 的學習,而 Node 中自帶了模塊化系統,Node 中的模塊化是基于 CommonJS 規范實現的。而 ES6 中的模塊化與之還有很多的不同的地方。現階段 Node 還依然使用的是 CommonJS 規范,而前端正在逐漸使用 ES6 module 規范。兩個規定統一是一個漫長的過程,兩者都存在歷史遺留問題和兼容問題需要瀏覽器和 Node 核心的支持。有必要搞清楚兩個規范的區別和注意事項,有助于我們深入地學習前端。

上一節我們學習 ES6 Module 的環境搭建和基本用法,本節將繼續學習模塊化的相關知識,本節主要是學習 CommonJS 規范,還有對比 ES6 module 規范的一些區別和注意事項。

2. CommonJS 規范

在維基百科中是這樣定義 CommonJS 的:

CommonJS 是一個項目,其目標是為 JavaScript 在網頁瀏覽器之外創建模塊約定。創建這個項目的主要原因是當時缺乏普遍可接受形式的 JavaScript 腳本模塊單元,模塊在與運行 JavaScript 腳本的常規網頁瀏覽器所提供的不同的環境下可以重復使用

JavaScript 語言在很長一段時間是沒有模塊化的概念的,直到 Node.js 的誕生后,讓 JavaScript 有能力編寫服務端語言,對操作系統、網絡、文件系統等等的復雜業務場景,使用模塊化就是不可或缺。這樣也把模塊化的概念帶到了前端,而這時的客戶端的功能也很復雜,急需一種可以拆分代碼模塊方便管理代碼的一種模式。最終在社區的推動下 ES6 給出了 JavaScript 模塊化的規范。

在 Node 模塊中,CommonJS 規定每個文件就是一個模塊,有它自己的作用域。在一個文件里面定義的變量、函數、類,都是私有的,對其他文件不可見。

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

2.1 導出模塊

使用 module.exports 把需要暴露的內容導出,沒有導出的在外面是訪問不了的。

// a.js
module.exports.name = 'imooc';
module.exports.fn = function(){}
const age = 18;

上面的代碼中在 a.js 文件中相當于一個私有的作用域, module.exports 把 name 和 fn 兩個變量導出,但是 age 沒有導出,所以在外部是訪問不了的。

為了方便 module.exports 也可以省略 module 直接使用 exports 進行導出操作:

exports.a = 'hello'

使用 module.exports 時還可以整體導出,整體導出時不能簡寫 exports

module.exports = { name: 'imooc', fn:function(){} }

2.2 導入模塊

使用 require 用于導入其他模塊暴露出來的內容。導出的內容是一個對象。

const a = require('./a');
console.log(a);	// { name: 'imooc', fn: [Function (anonymous)] }

2.3 CommonJS 模塊的特點

  • 所有代碼都運行在模塊作用域,不會污染全局作用域。
  • 模塊可以多次加載,但是只會在第一次加載時運行一次,然后運行結果就被緩存了,以后再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
  • 模塊加載的順序,按照其在代碼中出現的順序。

3. 不同規范之間的加載

3.1 import 加載 CommonJS 模塊

使用 import 命令加載 CommonJS 模塊,Node 會自動將 module.exports 屬性當作模塊的默認輸出,即等同于 export default

// a.js
module.exports = {
  foo: 'hello',
  bar: 'world'
}

// 在import引入時等同于
export default {
  foo: 'hello',
  bar: 'world'
}

CommonJs 模塊是運行時確定輸出接口,所以采用 import 命令加載 CommonJS 模塊時,只能使用整體輸入(*)。

import {readfile} from 'fs' //當'fs'為CommonJS模塊時錯誤
// 整體輸入
import * as express from 'express'
const app = express.default();

3.2 require 加載 ES6 模塊

require 命令加載 ES6 模塊時,所有的輸出接口都會成為輸入對象的屬性。

// es.js
let foo = {bar : 'my-default'};
exxport default foo;
foo = null;

// cjs.js
const es_namespace = require('./es')
console.log(es_namespace.default);// {bar:'my-default'}

4. 面試題

模塊化在面試中經常會被問到,掌握其深層原理是回答這類問題的關鍵。下面是面試中參考的兩道題,這里和大家分享一下,提供的答案僅供參考。

  1. commonjs 規范與 es module 規范的區別?

兩個規范的區別可以從以下幾個方面來回答:

  • 模塊的導出和導入:commonjs 使用的是 module.exports 和 require;es module 使用的是 export 和 import;
  • 模塊的引入方式:commonjs 是動態引用;esmodule 是靜態分析,export 和 import 只能出現在代碼的頂層,在編譯時就可以確定引用;
  • 模塊的引用類型:commonjs 對基本類型傳遞值,esmodule 對基本類型是傳遞引用;
  • CommonJs 的 this 是當前模塊,ES6 Module 的 this 是 undefined;
  • 對 webpack 來說,想要支持 tree shaking,包必須采用 es module 規范。

JS 在加載時分為兩個階段:編譯和執行,而 ES6 模塊是在 編譯時進行加載(也可以叫:靜態加載),這使得靜態分析成為可能。es module 自動采用嚴格模式,不管你有沒有在模塊頭部加上 "use strict";。

  1. 題目:commonjs 規范的循環引用

這是一道經典的 commonjs 的面試題,分析下列這段代碼,并解釋原理。

//main.js
var a = require('./a')
console.log(a)

// a.js
module.exports.a = 1
var b = require('./b')
console.log(b)
module.exports.a = 2

// b.js
module.exports.b = 11
var a = require('./a')
console.log(a)
module.exports.b = 22

回答本題的核心就是要知道 require 后的模塊是會被緩存的,還需要注意的是先加入緩存,然后再執行。這樣在按照代碼同步的執行順序去分析代碼就會很清晰。具體分析如下:

  1. 使用 node main.js 執行 main.js 文件內容;
  2. 執行 require('./a') 會將 a 模塊加入緩存,然后執行 a 模塊中的內容,執行權交到了 a 模塊中,執行 a;
  3. 執行第一行將緩存的 a 值賦值為 1,然后執行第二行 require('./b') 把 b 模塊加入緩存,并把執行權交到 b 模塊中;
  4. b 模塊中把 b 的值賦值為 11,在 require('./a') 時,是從緩存中取的值,這里就會在控制臺打印 {a: 1},最后把緩存中的 b 值修改為 22,執行權交給上一級;
  5. 代碼執行權回到 a 模塊中,這時 b 從緩存中取的值是 22,控制臺中打印 { b: 22 } ,最后把緩存中的 a 值修改為 2,執行權交給上一級;
  6. 代碼執行回到 main 模塊中,這時緩存中的 a 是 2,控制臺中打印 { a: 2 } ,然后代碼執行完畢。

5. 小結

本節主要學習了 CommonJS 的使用、在 CommonJS 和 ES Module 混用的一些問題,最后通過兩道面試題學習了兩個規范的區別和 CommonJS 在使用時會存在循環引用的問題,并分析了其執行的順序和緩存的特點。