ES6+ 模塊化擴(kuò)展
1. 前言
要深入前端學(xué)習(xí)時繞不開的是 Node 的學(xué)習(xí),而 Node 中自帶了模塊化系統(tǒng),Node 中的模塊化是基于 CommonJS 規(guī)范實(shí)現(xiàn)的。而 ES6 中的模塊化與之還有很多的不同的地方?,F(xiàn)階段 Node 還依然使用的是 CommonJS 規(guī)范,而前端正在逐漸使用 ES6 module 規(guī)范。兩個規(guī)定統(tǒng)一是一個漫長的過程,兩者都存在歷史遺留問題和兼容問題需要瀏覽器和 Node 核心的支持。有必要搞清楚兩個規(guī)范的區(qū)別和注意事項(xiàng),有助于我們深入地學(xué)習(xí)前端。
上一節(jié)我們學(xué)習(xí) ES6 Module 的環(huán)境搭建和基本用法,本節(jié)將繼續(xù)學(xué)習(xí)模塊化的相關(guān)知識,本節(jié)主要是學(xué)習(xí) CommonJS 規(guī)范,還有對比 ES6 module 規(guī)范的一些區(qū)別和注意事項(xiàng)。
2. CommonJS 規(guī)范
在維基百科中是這樣定義 CommonJS 的:
CommonJS 是一個項(xiàng)目,其目標(biāo)是為 JavaScript 在網(wǎng)頁瀏覽器之外創(chuàng)建模塊約定。創(chuàng)建這個項(xiàng)目的主要原因是當(dāng)時缺乏普遍可接受形式的 JavaScript 腳本模塊單元,模塊在與運(yùn)行 JavaScript 腳本的常規(guī)網(wǎng)頁瀏覽器所提供的不同的環(huán)境下可以重復(fù)使用
JavaScript 語言在很長一段時間是沒有模塊化的概念的,直到 Node.js 的誕生后,讓 JavaScript 有能力編寫服務(wù)端語言,對操作系統(tǒng)、網(wǎng)絡(luò)、文件系統(tǒng)等等的復(fù)雜業(yè)務(wù)場景,使用模塊化就是不可或缺。這樣也把模塊化的概念帶到了前端,而這時的客戶端的功能也很復(fù)雜,急需一種可以拆分代碼模塊方便管理代碼的一種模式。最終在社區(qū)的推動下 ES6 給出了 JavaScript 模塊化的規(guī)范。
在 Node 模塊中,CommonJS 規(guī)定每個文件就是一個模塊,有它自己的作用域。在一個文件里面定義的變量、函數(shù)、類,都是私有的,對其他文件不可見。
CommonJS 規(guī)定每個模塊內(nèi)部,module
變量代表當(dāng)前模塊。這個變量是一個對象,它的 exports
屬性(即 module.exports
)是對外的接口。加載某個模塊,其實(shí)是加載該模塊的 module.exports
屬性。
2.1 導(dǎo)出模塊
使用 module.exports
把需要暴露的內(nèi)容導(dǎo)出,沒有導(dǎo)出的在外面是訪問不了的。
// a.js
module.exports.name = 'imooc';
module.exports.fn = function(){}
const age = 18;
上面的代碼中在 a.js 文件中相當(dāng)于一個私有的作用域, module.exports
把 name 和 fn 兩個變量導(dǎo)出,但是 age 沒有導(dǎo)出,所以在外部是訪問不了的。
為了方便 module.exports
也可以省略 module 直接使用 exports 進(jìn)行導(dǎo)出操作:
exports.a = 'hello'
使用 module.exports
時還可以整體導(dǎo)出,整體導(dǎo)出時不能簡寫 exports
。
module.exports = { name: 'imooc', fn:function(){} }
2.2 導(dǎo)入模塊
使用 require
用于導(dǎo)入其他模塊暴露出來的內(nèi)容。導(dǎo)出的內(nèi)容是一個對象。
const a = require('./a');
console.log(a); // { name: 'imooc', fn: [Function (anonymous)] }
2.3 CommonJS 模塊的特點(diǎn)
- 所有代碼都運(yùn)行在模塊作用域,不會污染全局作用域。
- 模塊可以多次加載,但是只會在第一次加載時運(yùn)行一次,然后運(yùn)行結(jié)果就被緩存了,以后再加載,就直接讀取緩存結(jié)果。要想讓模塊再次運(yùn)行,必須清除緩存。
- 模塊加載的順序,按照其在代碼中出現(xiàn)的順序。
3. 不同規(guī)范之間的加載
3.1 import 加載 CommonJS 模塊
使用 import 命令加載 CommonJS 模塊,Node 會自動將 module.exports 屬性當(dāng)作模塊的默認(rèn)輸出,即等同于 export default
// a.js
module.exports = {
foo: 'hello',
bar: 'world'
}
// 在import引入時等同于
export default {
foo: 'hello',
bar: 'world'
}
CommonJs 模塊是運(yùn)行時確定輸出接口,所以采用 import 命令加載 CommonJS 模塊時,只能使用整體輸入(*)。
import {readfile} from 'fs' //當(dāng)'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. 面試題
模塊化在面試中經(jīng)常會被問到,掌握其深層原理是回答這類問題的關(guān)鍵。下面是面試中參考的兩道題,這里和大家分享一下,提供的答案僅供參考。
- commonjs 規(guī)范與 es module 規(guī)范的區(qū)別?
兩個規(guī)范的區(qū)別可以從以下幾個方面來回答:
- 模塊的導(dǎo)出和導(dǎo)入:commonjs 使用的是 module.exports 和 require;es module 使用的是 export 和 import;
- 模塊的引入方式:commonjs 是動態(tài)引用;esmodule 是靜態(tài)分析,export 和 import 只能出現(xiàn)在代碼的頂層,在編譯時就可以確定引用;
- 模塊的引用類型:commonjs 對基本類型傳遞值,esmodule 對基本類型是傳遞引用;
- CommonJs 的 this 是當(dāng)前模塊,ES6 Module 的 this 是 undefined;
- 對 webpack 來說,想要支持 tree shaking,包必須采用 es module 規(guī)范。
JS 在加載時分為兩個階段:編譯和執(zhí)行,而 ES6 模塊是在 編譯時進(jìn)行加載(也可以叫:靜態(tài)加載),這使得靜態(tài)分析成為可能。es module 自動采用嚴(yán)格模式,不管你有沒有在模塊頭部加上 "use strict";
。
- 題目:commonjs 規(guī)范的循環(huán)引用
這是一道經(jīng)典的 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 后的模塊是會被緩存的,還需要注意的是先加入緩存,然后再執(zhí)行。這樣在按照代碼同步的執(zhí)行順序去分析代碼就會很清晰。具體分析如下:
- 使用 node main.js 執(zhí)行 main.js 文件內(nèi)容;
- 執(zhí)行
require('./a')
會將 a 模塊加入緩存,然后執(zhí)行 a 模塊中的內(nèi)容,執(zhí)行權(quán)交到了 a 模塊中,執(zhí)行 a; - 執(zhí)行第一行將緩存的 a 值賦值為 1,然后執(zhí)行第二行
require('./b')
把 b 模塊加入緩存,并把執(zhí)行權(quán)交到 b 模塊中; - b 模塊中把 b 的值賦值為 11,在
require('./a')
時,是從緩存中取的值,這里就會在控制臺打印{a: 1}
,最后把緩存中的 b 值修改為 22,執(zhí)行權(quán)交給上一級; - 代碼執(zhí)行權(quán)回到 a 模塊中,這時 b 從緩存中取的值是 22,控制臺中打印
{ b: 22 }
,最后把緩存中的 a 值修改為 2,執(zhí)行權(quán)交給上一級; - 代碼執(zhí)行回到 main 模塊中,這時緩存中的 a 是 2,控制臺中打印
{ a: 2 }
,然后代碼執(zhí)行完畢。
5. 小結(jié)
本節(jié)主要學(xué)習(xí)了 CommonJS 的使用、在 CommonJS 和 ES Module 混用的一些問題,最后通過兩道面試題學(xué)習(xí)了兩個規(guī)范的區(qū)別和 CommonJS 在使用時會存在循環(huán)引用的問題,并分析了其執(zhí)行的順序和緩存的特點(diǎn)。