利用webpack理解CommonJS和ES Modules的差異區(qū)別
前言
問(wèn): CommonJS 和 ES Modules 中模塊引入的區(qū)別?
CommonJS 輸出的是一個(gè)值的拷貝;ES Modules 生成一個(gè)引用,等到真的需要用到時(shí),再到模塊里面去取值,模塊里面的變量,綁定其所在的模塊。
我相信很多人已經(jīng)把這個(gè)答案背得滾瓜爛熟,好,那繼續(xù)提問(wèn)。
問(wèn):CommonJS 輸出的值是淺拷貝還是深拷貝?
問(wèn):你能模擬實(shí)現(xiàn) ES Modules 的引用生成嗎?
對(duì)于以上兩個(gè)問(wèn)題,我也是感到一臉懵逼,好在有 webpack 的幫助,作為一個(gè)打包工具,它讓 ES Modules, CommonJS 的工作流程瞬間清晰明了。
準(zhǔn)備工作
初始化項(xiàng)目,并安裝 beta 版本的 webpack 5,它相較于 webpack 4 做了許多優(yōu)化:對(duì) ES Modules 的支持度更高,打包后的代碼也更精簡(jiǎn)。
$ mkdir demo && cd demo $ yarn init -y $ yarn add webpack@next webpack-cli # or yarn add webpack@5.0.0-beta.17 webpack-cli
早在 webpack4 就已經(jīng)引入了無(wú)配置的概念,既不需要提供 webpack.config.js 文件,它會(huì)默認(rèn)以 src/index.js 為入口文件,生成打包后的 main.js 放置于 dist 文件夾中。
確保你擁有以下目錄結(jié)構(gòu):
├── dist │ └── index.html ├── src │ └── index.js ├── package.json └── yarn.lock
在 index.html 中引入打包后的 main.js:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script src="main.js"></script> </body> </html>
在 package.json 中添加命令腳本:
"scripts": {
"start": "webpack"
},
運(yùn)行無(wú)配置打包:
$ yarn start
終端會(huì)提示:
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
webpack 要求用戶在打包時(shí)必須提供 mode 選項(xiàng),來(lái)指明打包后的資源用于開發(fā)環(huán)境還是生產(chǎn)環(huán)境,從而讓 webpack 相應(yīng)地使用其內(nèi)置優(yōu)化,默認(rèn)為 production(生產(chǎn)環(huán)境)。
我們將其設(shè)置為 none 來(lái)避免默認(rèn)行為帶來(lái)的干擾,以便我們更好的分析源碼。
修改 package.json:
"scripts": {
"start": "webpack --mode=none"
},
重新運(yùn)行,webpack 在 dist 目錄下生成了打包后的 main.js,由于入口文件是空的,所以 main.js 的源碼只有一個(gè) IIFE(立即執(zhí)行函數(shù)),看似簡(jiǎn)單,但它的地位卻極其重要。
(() => {
// webpackBootstrap
})();
我們知道無(wú)論在 CommonJS 或 ES Modules 中,一個(gè)文件就是一個(gè)模塊,模塊之間的作用域相互隔離,且不會(huì)污染全局作用域。此刻 IIFE 就派上了用場(chǎng),它將一個(gè)文件的全部 JS 代碼包裹起來(lái),形成閉包函數(shù),不僅起到了函數(shù)自執(zhí)行的作用,還能保證函數(shù)間的作用域不會(huì)互相污染,并且在閉包函數(shù)外無(wú)法直接訪問(wèn)內(nèi)部變量,除非內(nèi)部變量被顯式導(dǎo)出。
var name = "webpack";
(() => {
var name = "parcel";
var age = 18;
console.log(name); // parcel
})();
console.log(name); // webpack
console.log(age); // ReferenceError: age is not defined
引用 vs 拷貝
接下里進(jìn)入實(shí)踐部分,涉及源碼的閱讀,讓我們深入了解 CommonJS 和 ES Modules 的差異所在。
CommonJS
新建 src/counter.js
let num = 1;
function increase() {
return num++;
}
module.exports = { num, increase };
修改 index.js
const { num, increase } = require("./counter");
console.log(num);
increase();
console.log(num);
如果你看過(guò)前面敘述,毫無(wú)疑問(wèn),打印 1 1.
so why?我們查看 main.js,那有我們想要的答案,去除無(wú)用的注釋后如下:
(() => {
var __webpack_modules__ = [
,
module => {
let num = 1;
function increase() {
return num++;
}
module.exports = { num, increase };
},
];
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
// Check if module is in cache
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
const { num, increase } = __webpack_require__(1);
console.log(num);
increase();
console.log(num);
})();
})();
可以簡(jiǎn)化為:
(() => {
var __webpack_modules__ = [...];
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {...}
(() => {
const { num, increase } = __webpack_require__(1);
console.log(num);
increase();
console.log(num);
})();
})();
最外層是一個(gè) IIFE,立即執(zhí)行。
__webpack_modules__,它是一個(gè)數(shù)組,第一項(xiàng)為空,第二項(xiàng)是一個(gè)箭頭函數(shù)并傳入 module 參數(shù),函數(shù)內(nèi)部包含了 counter.js 中的所有代碼。
__webpack_module_cache__ 緩存已經(jīng)加載過(guò)的模塊。
function __webpack_require__(moduleId) {...} 類似于 require(),他會(huì)先去 __webpack_module_cache__ 中查找此模塊是否已經(jīng)被加載過(guò),如果被加載過(guò),直接返回緩存中的內(nèi)容。否則,新建一個(gè) module: {exports: {}},并設(shè)置緩存,執(zhí)行模塊函數(shù),最后返回 module.exports
最后遇到一個(gè) IIFE,它將 index.js 中的代碼包裝在內(nèi),并執(zhí)行 __webpack_require__(1),導(dǎo)出了 num 和 increase 供 index.js 使用。
這里的關(guān)鍵點(diǎn)在于 counter.js 中的 module.exports = { num, increase };,等同于以下寫法:
module.exports = {
num: num,
increase: increase,
};
num 屬于基本類型,假設(shè)其內(nèi)存地址指向 n1,當(dāng)它被 賦值 給 module.exports['num'] 時(shí),module.exports['num'] 已經(jīng)指向了一個(gè)新的內(nèi)存地址 n2,只不過(guò)其值同樣為 1,但和 num 已是形同陌路,毫不相干。
let num = 1; // mun 相當(dāng)于 module.exports['num'] mun = num; num = 999; console.log(mun); // 1
increase 是一個(gè)函數(shù),屬于引用類型,即 increase 只作為一個(gè)指針,當(dāng)它被賦值給 module.exports['increase'] 時(shí),只進(jìn)行了指針的復(fù)制,是 淺拷貝(基本類型沒(méi)有深淺拷貝的說(shuō)法),其內(nèi)存地址依舊指向同一塊數(shù)據(jù)。所以本質(zhì)上 module.exports['increase'] 就是 increase,只不過(guò)換個(gè)名字。
而由于詞法作用域的特性,counter.js 中 increase() 修改的 num 變量在函數(shù)聲明時(shí)就已經(jīng)綁定不變了,永遠(yuǎn)綁定內(nèi)存地址指向 n1 的 num.
JavaScript 采用的是詞法作用域,它規(guī)定了函數(shù)內(nèi)訪問(wèn)變量時(shí),查找變量是從函數(shù)聲明的位置向外層作用域中查找,而不是從調(diào)用函數(shù)的位置開始向上查找
function foo() {
var x = 10;
console.log(x);
}
function bar(f) {
var x = 20;
f();
}
bar(foo); // 10
調(diào)用 increase() 并不會(huì)影響內(nèi)存地址指向 n2 的 num,這也就是為什么打印 1 1 的理由。
ES Modules
分別修改 counter.js 和 index.js,這回使用 ES Modules.
let num = 1;
function increase() {
return num++;
}
export { num, increase };
import { num, increase } from "./counter";
console.log(num);
increase();
console.log(num);
很明顯,打印 1 2.
老規(guī)矩,查看 main.js,刪除無(wú)用的注釋后如下:
(() => {
"use strict";
var __webpack_modules__ = [
,
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.d(__webpack_exports__, {
num: () => /* binding */ num,
increase: () => /* binding */ increase,
});
let num = 1;
function increase() {
return num++;
}
},
];
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {} // 筆者注:同一個(gè)函數(shù),不再展開
/* webpack/runtime/define property getters */
(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
(() => {
var _counter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
(0, _counter__WEBPACK_IMPORTED_MODULE_0__.increase)();
console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
})();
})();
經(jīng)過(guò)簡(jiǎn)化,大致如下:
(() => {
"use strict";
var __webpack_modules__ = [...];
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {...}
(() => {
__webpack_require__.d = (exports, definition) => {...};
})();
(() => {
__webpack_require__.o = (obj, prop) => {...}
})();
(() => {
var _counter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
(0, _counter__WEBPACK_IMPORTED_MODULE_0__.increase)();
console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
})();
})();
首先查看兩個(gè)工具函數(shù):__webpack_require__.o 和 __webpack_require__.d。
__webpack_require__.o 封裝了 Object.prototype.hasOwnProperty.call(obj, prop) 的操作。
__webpack_require__.d 則是通過(guò) Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }) 來(lái)對(duì) exports 對(duì)象設(shè)置不同屬性的 getter
隨后看到了熟悉的 __webpack_modules__,它的形式和上一節(jié)差不多,最主要的是以下這段代碼:
__webpack_require__.d(__webpack_exports__, {
num: () => /* binding */ num,
increase: () => /* binding */ increase,
});
與 CommonJS 不同,ES Modules 并沒(méi)有對(duì) module.exports 直接賦值,而是將值作為箭頭函數(shù)的返回值,再把箭頭函數(shù)賦值給 module.exports,之前我們提過(guò)詞法作用域的概念,即這里的 num() 和 increase() 無(wú)論在哪里執(zhí)行,返回的 num 變量和 increase 函數(shù)都是 counter.js 中的。
在遇到最后一個(gè) IIFE 時(shí),調(diào)用 __webpack_require__(1),返回 module.exports 并賦值給 _counter__WEBPACK_IMPORTED_MODULE_0__,后續(xù)所有的屬性獲取都是使用點(diǎn)操作符,這觸發(fā)了對(duì)應(yīng)屬性的 get 操作,于是執(zhí)行函數(shù)返回 counter.js 中的值。
所以打印 1 2.
懂了詞法作用域的原理,就可以實(shí)現(xiàn)一個(gè)”乞丐版“的 ES Modules:
function my_require() {
var module = {
exports: {},
};
let counter = 1;
function add() {
return counter++;
}
module.exports = { counter: () => counter, add };
return module.exports;
}
var obj = my_require();
console.log(obj.counter()); // 1
obj.add();
console.log(obj.counter()); // 2
總結(jié)
多去看源碼,會(huì)有不少的收獲,這是一個(gè)思考的過(guò)程。
ES Modules 已經(jīng)寫入了 ES2020 規(guī)范中,意味著瀏覽器原生支持 import 和 export,有興趣的小伙伴可以試試 Snowpack,它能直接 export 第三方庫(kù)供瀏覽器使用,省去了 webpack 中打包的時(shí)間。
到此這篇關(guān)于利用webpack理解CommonJS和ES Modules的差異區(qū)別的文章就介紹到這了,更多相關(guān)webpack CommonJS和ES Modules 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
離開當(dāng)前頁(yè)面前使用js判斷條件提示是否要離開頁(yè)面
這篇文章主要介紹了離開當(dāng)前頁(yè)面前如何使用js判斷條件提示是否要離開頁(yè)面,需要的朋友可以參考下2014-05-05
Javascript繼承機(jī)制的設(shè)計(jì)思想分享
我花了很多時(shí)間,學(xué)習(xí)這個(gè)部分,還做了很多筆記。但是都屬于強(qiáng)行記憶,無(wú)法從根本上理解。2011-08-08
用javascript實(shí)現(xiàn)倒計(jì)時(shí)效果
這篇文章主要為大家詳細(xì)介紹了用javascript實(shí)現(xiàn)倒計(jì)時(shí)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-02-02
JS實(shí)現(xiàn)簡(jiǎn)易的圖片拖拽排序?qū)嵗a
這篇文章主要介紹了JS實(shí)現(xiàn)簡(jiǎn)易的圖片拖拽排序?qū)嵗a,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
JavaScript下申明對(duì)象的幾種方法小結(jié)
在JavaScript中可以用下面的幾種方法申明對(duì)象:(從"Truly"的文章中學(xué)到)2008-10-10
Javascript Function.prototype.bind詳細(xì)分析
這篇文章主要介紹了Javascript Function.prototype.bind詳細(xì)分析的相關(guān)資料,需要的朋友可以參考下2016-12-12
js判斷數(shù)組是否包含某個(gè)字符串變量的實(shí)例
下面小編就為大家分享一篇js判斷數(shù)組是否包含某個(gè)字符串變量的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助2017-11-11
layui之?dāng)?shù)據(jù)表格--與后臺(tái)交互獲取數(shù)據(jù)的方法
今天小編就為大家分享一篇layui之?dāng)?shù)據(jù)表格--與后臺(tái)交互獲取數(shù)據(jù)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09

