提到動態執行腳本,大家想到的肯定是 eval
或 new Function()
,在 nodejs 中有專屬的 vm 模塊,可以完成相應的 sandbox 作用。
瀏覽器中動態執行腳本
eval()
函數會將傳入的字符串當做 JavaScript 代碼進行執行,返回字符串中代碼的返回值;如果參數不是字符串將原封不動返回。
如果你間接的使用 eval()
,比如通過一個引用來調用它,而不是直接的調用 eval
。從 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。
function test() {
let x = 2, y = 4;
console.log(eval('x + y')); // 直接調用,使用本地作用域,結果是 6
let geval = eval; // 等價於在全局作用域調用
console.log(geval('x + y')); // 間接調用,使用全局作用域,throws ReferenceError 因爲`x`未定義
(0, eval)('x + y'); // 另一個間接調用的例子
}
eval
中函數作爲字符串被定義需要“(”和“)”作爲前綴和後綴
let fctStr1 = 'function a() {}'
let fctStr2 = '(function a() {})'
let fct1 = eval(fctStr1) // 返回undefined
let fct2 = eval(fctStr2) // 返回一個函數
MDN 建議永遠不要使用 eval
-
eval()
使用與調用者相同的權限執行代碼。如果你用eval()
運行的字符串代碼被惡意方(不懷好意的人)修改,您最終可能會在您的網頁/擴展程序的權限下,在用戶計算機上運行惡意代碼。更重要的是,第三方代碼可以看到某一個eval()
被調用時的作用域,這也有可能導致一些不同方式的攻擊。 -
eval()
通常比其他替代方法更慢,因爲它必須調用 JS 解釋器,而許多其他結構則可被現代 JS 引擎進行優化。此外,現代JavaScript解釋器將javascript轉換爲機器代碼。 這意味着任何變量命名的概念都會被刪除。 因此,任意一個eval的使用都會強制瀏覽器進行冗長的變量名稱查找,以確定變量在機器代碼中的位置並設置其值。
Function 是替代 eval 的一個好的方法。
Function
new Function ([arg1[, arg2[, ...argN]],] functionBody)
每個 JavaScript 函數實際上都是一個 Function
對象。運行 (function(){}).constructor === Function // true
便可以得到這個結論。
與 eval
不同的是,Function
創建的函數只能在全局作用域中運行。
function test() {
let x = 2, y = 4;
console.log(new Function('return x + y')()); // 直接調用,使用全局作用域,throws ReferenceError
}
Nodejs 動態執行腳本
通過 node 的核心模塊 vm 來實現。vm可以使用v8的Virtual Machine contexts動態地編譯和執行代碼,而代碼的執行上下文是與當前進程隔離的,但是這裏的隔離並不是絕對的安全,不完全等同瀏覽器的沙箱環境。
-
vm.runInContext(code, contextifiedObject[, options])
在指定的
contextifiedObject
的上下文裏執行它並返回其結果。 被執行的代碼無法獲取本地作用域。contextifiedObject
必須是事先被vm.createContext()
方法上下文隔離化過的對象。const vm = require('vm') const contextObject = { a: 1 } vm.createContext(contextObject) const result = vm.runInContext('a += 1; b = 3', contextObject) console.log(result) // 3 { a: 2, b: 3 }
-
vm.runInNewContext(code[, contextObject[, options]])
給指定的
contextObject
(若爲undefined
,則會新建一個contextObject
)提供一個隔離的上下文, 再在此上下文中執行編譯的code
,最後返回結果。 運行中的代碼無法獲取本地作用域。const vm = require('vm') const result = vm.runInNewContext('a += 1; b = 3', {a: 1}) console.log(result) // 3 { a: 2, b: 3 }
-
vm.runInThisContext(code[, options])
在當前的
global
對象的上下文中編譯並執行code
,最後返回結果。 運行中的代碼無法獲取本地作用域,但可以獲取當前的global
對象。global.a = 1 const result = vm.runInThisContext('a += 1') console.log(result)
vm.runInThisContext()
更像是間接的執行eval()
, 就像(0,eval)('code')
。 -
eval()
Nodejs 中同樣可以使用 eval 函數,但性能和安全性有差異。請查看 https://odino.org/eval-no-more-understanding-vm-vm2-nodejs/
-
vm2
Node.js 的高級 vm/sandbox,https://github.com/patriksimek/vm2
上下文隔離化
所有用 Node.js 所運行的 JavaScript 代碼都是在一個“上下文”的作用域中被執行的。在 V8 中,一個上下文是一個執行環境,它允許分離的,無關的 JavaScript 應用在一個 V8 的單例中被運行。 必須明確地指定用於運行所有 JavaScript 代碼的上下文。
vm.createContext([contextObject[, options]])
contextObject
參數(如果 contextObject
爲 undefined
,則爲新創建的對象)在內部與 V8 上下文的新實例相關聯。 該 V8 上下文提供了使用 vm
模塊的方法運行的 code
以及可在其中運行的隔離的全局環境。
使用場景
動態執行字符串代碼。vue ssr 中是通過 runInNewContext 實現的( Vue SSR 指南)。
參考地址
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval
- http://nodejs.cn/api/vm.html
- https://ssr.vuejs.org/zh/api/#runinnewcontext
- https://odino.org/eval-no-more-understanding-vm-vm2-nodejs/
- https://github.com/patriksimek/vm2