日常的學習筆記,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue全家桶,后續可能還會繼續更新 Typescript、Vue3 和 常見的面試題 等等。
參考文獻 vm 虛擬機 | Node 官網
在上一篇文章中,我們提到了一個問題。
字符串如何能變成 JS 執行呢?
我們詳細介紹了兩種方法,分別是 eval函數 和 new Function 。
在這里我們需要再強調一下, 由 Function
構造器創建的函數不會創建當前環境的閉包,它們總是被創建於全局環境,因此在運行時它們只能訪問全局變量和自己的局部變量,不能訪問它們被 Function
構造器創建時所在的作用域的變量。這一點與使用 eval
執行創建函數的代碼不同。
global.a = 100; // 掛在到全局對象global上
var b = 200; // this !== global
new Function("console.log(a)")() // 100
new Function("console.log(b)")() // b is not defined
Function
可以獲取全局變量,所以他還是可能會有變量污染的情況出現。Function
是 模塊引擎的實現原理 ,后續我會出一篇文章進行單獨講解。
還有一種解決方案,我們在上一次文章中沒有進行詳細的展開,那就是 vm
模塊 。
vm模塊
在上述文字中,我一直在強調一個概念,那就是 變量的污染。
VM的特點就是不受環境的影響,也可以說他就是一個 沙箱環境 (沙箱模式給模塊提供一個環境運行而不影響其它模塊和它們私有的沙箱)。
const vm = require('vm')
global.a = 100;
// 運行在當前環境中[當前作用域]
vm.runInThisContext('console.log(a)'); // 100
// 運行在新的環境中[其他作用域]
vm.runInNewContext('console.log(a)'); // a is not defined
在這里我們要強調一下,因為 在Node中全局變量是在多個模塊下共享的,所以盡量不要在global中定義屬性。 Demo中的定義是為了方便理解。
假設我們在同級目錄下有一個文件 1.js ,里面定義了 global.a = 100;
。 現在我們引入這個文件
requrie(./1);
console.log(a); // 100
我們可以發現,在當前文件中我們並沒有定義變量a,僅僅只是把兩個模塊文件關聯在了一起。這就是我上面提到的,Node中全局變量是在多個模塊下共享的。
他的原理是因為在 Node 的環境中,全局中有一個執行上下文。
// 模擬一下Node的全局環境
// vm.runInThisContext在當前全局環境執行,但不會產生新函數
- function(exports, module, require, __dirname, __filename){ // ... }
- vm.runInThisContext ...
// vm.runInNewContext在全局環境之外執行
vm.runInNewContext ...
所以,vm.runInThisContext
可以訪問到 global
上的全局變量,但是訪問不到自定義的變量。而 vm.runInNewContext
訪問不到 global
,也訪問不到自定義變量,他存在於一個全新的執行上下文。
而我們require
就是通過 vm.runInThisContext
實現的。
實現require
主要可以分為以下四步。
- 讀取需要引入的文件。
- 讀取到文件后,將代碼封裝成一個函數。
- 通過
vm.runInThisContext
將他轉變成 JS 語法。 - 代碼調用。
假設我們現在有以下兩個文件。分別是 a.js 和 b.js
// 文件a通過module.exports導出一個變量,在文件b中使用require進行接收。
// a.js
module.exports = "a"
// b.js
let a = require('./a');
console.log(a); // a
我們可以通過上面的四個步驟,分析一下導入導出的實現邏輯是什么樣的。
-
讀取文件。
將需要引入的文件內容引入到需要接收的文件里,就會變成這個樣子
let a = module.exports = "a";
但是這種形式,Node根本解析不了,所以我們就需要進行第二步。
-
將讀取的文件封裝成函數。
let a = (function(exports, module, require, __dirname, __filename){ module.exports = "a"; return module.exports })(...args) // exports, module, require, __dirname, __filename 將五個參數傳入
封裝成函數的原因,我們可以參考下面這個例子。
假設我們現在傳入的不是字符串,而是一個函數。
// a.js var a = 100; module.exports = function(){}
這樣我們在解析的時候,就會被解析成下面這種格式
let a = (function(exports, module, require, __dirname, __filename){ var a = 100; module.exports = function(){}; return module.exports })(...args) // exports, module, require, __dirname, __filename 將五個參數傳入
我們導出的是
module.exports
,所以在模塊文件中定義的變量a,也只屬於當前這個執行上下文。在解析的時候,變量a 會被放到函數中。真正的實現了 作用域分離。
-
vm.runInThisContext
解析成可執行的Js代碼我們處理過的代碼會以字符串的形式存在,所以我們需要通過
vm.runInThisContext
將字符串進行解析。 -
進行代碼調用
在此之前,我們其實還需要對代碼進行調試。
下一篇文章我會詳細講解一下 Node中的模塊調試。
本篇文章由莫小尚創作,文章中如有任何問題和紕漏,歡迎您的指正與交流。
您也可以關注我的 個人站點、博客園 和 掘金,我會在文章產出后同步上傳到這些平台上。
最后感謝您的支持!