你不知道的 eval
eval() 是 JavaScript 中一個非常有用的函式,它可以一段程式碼字串動態執行。然而各種編碼規範和最佳實踐都強烈抵制 eval,幾乎將 eval 打入了死牢,大牛 Douglas Crockford 也在《JavaScript 語言精粹》一書中將 eval 視為 JavaScript 中糟粕。這篇文章將帶大家重新認識這個函式,知道為什麼不用它,以及為什麼不得不用它。
eval 是什麼
在分析 eval 的利弊前,首先來認識一下它。 在不清楚一項技術的情況下,就對它做出武斷地評價,是有失公允的。
eval 是全域性物件上的一個函式,會把傳入的字串當做 JavaScript 程式碼執行。如果傳入的引數不是字串,它會原封不動地將其返回。 eval 分為直接呼叫和間接呼叫兩種 ,通常間接呼叫的效能會好於直接呼叫。
直接呼叫時,eval 運行於其呼叫函式的作用域下;
var context = 'outside'; (function(){ var context = 'inside'; return eval('context'); })(); // return 'inside' 複製程式碼
而間接呼叫時,eval 運行於全域性作用域。
var context = 'outside'; (function(){ var context = 'inside'; geval = eval; return geval('context'); // 下面兩種也屬於間接呼叫 // return eval.call(null, 'context'); // return (1, eval)('context'); })(); // return 'outside' 複製程式碼
因此,間接呼叫時,eval 並不會修改呼叫函式作用域內的任何東西。JS 直譯器有 fast path 和 slow path 兩種模式,當直接呼叫 eval 時,直譯器處於 slow path。因為此時作用域是不可控的,需要監聽整個作用域,不能應用 v8 的一些編譯優化,相應的編譯效率也會比 fast path 低。
為什麼不用 eval
大家抵制 eval 的原因主要是以下幾個原因:
- 降低效能 。具體原因上文已經提到了,網上一些文章甚至說 eval() 會拖慢效能 10 倍。
- 安全問題 。因為它的動態執行特性,給被求值的字串賦予了太大的權力,於是大家擔心可能因此導致 XSS 等攻擊。
- 除錯困難 。eval 就像一個黑盒,其執行的程式碼很難進行斷點除錯。
鑑於以上各種原因,很多人說 eval 是 evil(魔鬼)。另外,eval 還有一些難兄難弟,比如 new Function, setTimeout, setInterval。它們也具備執行一段程式碼字串的能力。 究其本質原因,還是因為 JS 賦予這個方法的許可權太大了,作為新手很難駕馭它 ,如果對 eval 沒有很好地理解,很容易寫出問題來。這有點像 C 語言中 goto 語句,同樣是因為許可權太大而被封殺的典範。
被誤解的 eval
事實上,eval 一直在被誤解,它可能是最強大的一個 JavaScript 函式,但卻因為一些人的誤用,而被開發者們打入了冷宮。接下來,我來根據上述被質疑最多的幾個點,給出一點自己的看法。
- 關於 eval 會拖慢效能 10 倍這個點,出自 Mozila 工程師的演講 ofollow,noindex">“Know Your Engines - How to make your JavaScript Fast” 。 這是一個釋出於 2011 年的演講,時至今日,JS 引擎已做了各種優化。我們來測試現在的 JS 引擎中,eval 的實際效能。依然使用上圖作為測試用例,測試環境為 node v8.11.1,設 N 的值為 10000。 Benchmark 跑出的資料來看,當 N = 10000 時,用了 eval 的 function 執行效能,相比沒有 eval 的情況,慢了 3 倍多。 將 N 的值設為 1000000,eval 的效能下降到 8 倍。
從測試結果可知,eval 的確會拖慢函式執行效能,而且隨著函式規模增大,效能也越慢。但是在一般情況下(N < 1000000),效能差異並沒有 10 倍那麼誇張。
- 關於 eval 會導致 XSS 攻擊這點,問題並不在 eval,而在資料來源。如果資料來源本身就是不可靠的,即便你不用 eval,也可能出現 XSS。
- 至於第三點,eval 程式碼的確除錯起來比較麻煩,但也不是完全沒有辦法。可以在 eval 建立的程式碼末尾新增一行 "//@ sourceURL=name" 就可以給這段程式碼命名(瀏覽器會特殊對待這種特殊形式的註釋),這樣它就會出現在 Sources 面板上,然後就可以設定斷點除錯了。
真香警告
雖然大家嘴上說不要用,但是 eval 用起來卻是真香。
筆者做過的專案中,曾經為了讓 HTML 模板(應該說是一套頁面主題)也具備動態解析內聯表示式的能力,用了 data-eval 將 js 程式碼儲存在 dom 節點,然後渲染時用 with 語句(另一個 JS “毒瘤”,現在嚴格模式下已經禁用 with 了,rip...)將 data 加到作用域鏈上,再用 eval 解析執行。實現出來的效果類似這樣:
<div data-eval="data.count = data.count + 1"> {{data.count}} </div> 複製程式碼
渲染出來的結果是 eval 計算後的值。
很多庫和框架都用了 eval 實現各種黑魔法。早期的有用 eval 解析 json 的,比如 Douglas Crockford 的 json2.js(真香!)。到後來,各種 MVVM 框架也用 new Function 這個 eval 的好基友,來實現模板內嵌表示式的計算,比如 Vue 和 avalon。要達到的效果和筆者上面介紹的例子大致相同,不同的是這些 MVVM 框架還需要先解析模板,基於正則表示式提取出 new Function 的引數。
甚至不能用 eval 的時候,也要自己造一個 eval 出來。比如小程式上就不能使用 eval 和 new Function,那麼如果想動態注入並執行程式碼的話,需要繞一個大彎,從編譯原理出發,自行實現一個 JS parser。