你不知道的 eval

前言

eval() 是 JavaScript 中一個很是有用的函數,它能夠一段代碼字符串動態執行。然而各類編碼規範和最佳實踐都強烈抵制 eval,幾乎將 eval 打入了死牢,大牛 Douglas Crockford 也在《JavaScript 語言精粹》一書中將 eval 視爲 JavaScript 中糟粕。這篇文章將帶你們從新認識這個函數,知道爲何不用它,以及爲何不得不用它。node

eval 是什麼

在分析 eval 的利弊前,首先來認識一下它。在不清楚一項技術的狀況下,就對它作出武斷地評價,是有失公允的。git

eval 是全局對象上的一個函數,會把傳入的字符串當作 JavaScript 代碼執行。若是傳入的參數不是字符串,它會原封不動地將其返回。eval 分爲直接調用和間接調用兩種,一般間接調用的性能會好於直接調用。github

直接調用時,eval 運行於其調用函數的做用域下;正則表達式

var context = 'outside';
(function(){
  var context = 'inside';
  return eval('context');
})();

// return 'inside'
複製代碼

而間接調用時,eval 運行於全局做用域。json

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 的緣由主要是如下幾個緣由:瀏覽器

  1. 下降性能。具體緣由上文已經提到了,網上一些文章甚至說 eval() 會拖慢性能 10 倍。
  2. 安全問題。由於它的動態執行特性,給被求值的字符串賦予了太大的權力,因而你們擔憂可能所以致使 XSS 等攻擊。
  3. 調試困難。eval 就像一個黑盒,其執行的代碼很難進行斷點調試。

鑑於以上各類緣由,不少人說 eval 是 evil(魔鬼)。另外,eval 還有一些難兄難弟,好比 new Function, setTimeout, setInterval。它們也具有執行一段代碼字符串的能力。 究其本質緣由,仍是由於 JS 賦予這個方法的權限太大了,做爲新手很難駕馭它,若是對 eval 沒有很好地理解,很容易寫出問題來。這有點像 C 語言中 goto 語句,一樣是由於權限太大而被封殺的典範。安全

被誤解的 eval

事實上,eval 一直在被誤解,它多是最強大的一個 JavaScript 函數,但卻由於一些人的誤用,而被開發者們打入了冷宮。接下來,我來根據上述被質疑最多的幾個點,給出一點本身的見解。bash

  1. 關於 eval 會拖慢性能 10 倍這個點,出自 Mozila 工程師的演講 「Know Your Engines - How to make your JavaScript Fast」
    這是一個發佈於 2011 年的演講,時至今日,JS 引擎已作了各類優化。咱們來測試如今的 JS 引擎中,eval 的實際性能。依然使用上圖做爲測試用例,測試環境爲 node v8.11.1,設 N 的值爲 10000。
    image.png
    Benchmark 跑出的數據來看,當 N = 10000 時,用了 eval 的 function 執行性能,相比沒有 eval 的狀況,慢了 3 倍多。 將 N 的值設爲 1000000,eval 的性能降低到 8 倍。
    image.png

從測試結果可知,eval 的確會拖慢函數執行性能,並且隨着函數規模增大,性能也越慢。可是在通常狀況下(N < 1000000),性能差別並無 10 倍那麼誇張。框架

  1. 關於 eval 會致使 XSS 攻擊這點,問題並不在 eval,而在數據源。若是數據源自己就是不可靠的,即使你不用 eval,也可能出現 XSS。
  2. 至於第三點,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。

總結

關於 eval,筆者我的的見解是,你能夠不去用它,但要去了解它。寫這篇文章的目的也不是爲了推薦你們使用 eval。就平時的業務開發而言,eval 幾乎沒有用武之地。但在一些特殊場合,eval 就像一枚核彈,無往不利。


參考連接:

  1. Global eval. What are the options?
  2. Knockout, Vue 和 AvalonJS 等 MVVM 框架實現中是否用到 eval 或 Function?
  3. eval() isn’t evil, just misunderstood
  4. A new V8 is coming, Node.js performance is changing.
  5. V8: Behind the Scenes (February Edition feat. A tale of TurboFan)
相關文章
相關標籤/搜索