此篇.本來想多寫些測試用例. 但是因為 阿灰,已經做了很多測試.所以就做個科普吧.
eval是什么.
我個人覺得eval最初的設計,就是一個內置函數.提供一個動態執行代碼的接口. 所以ES3上對他的描述就是如此簡單. 這里為了描述清楚ES3對 eval code的規范.所以我不得不拿出一大段來解釋這些東西.
ES3 :
.Eval Code :當控制器進入一個eval code 的執行環境時,前一個(eval函數調用代碼所處的)執行環境,作為調用環境(calling context,調用環境),用以決定作用域鏈.變量對象,及this關鍵字的值如果沒有調用環境,則作用域鏈、變量對象、以及this值的處理同Global Code相同. (這里原文描述很含糊,所謂沒有調用環境,即使指全局環境)
. 其作用域鏈的初始化工作,就是按相同順序,使其包含,同調用環境的作用域鏈對象所包含的相同的那些對象(活動對象、變量對象,等等等等,甚至是with和catch所添加的那些對象).. 其變量對象初始化過程,雖然使用的就是調用環境的變量對象.但Eval Code內的標識符對應屬性.不具備任何特性. (這就是為啥eval中 聲明的變量,可以被delete 運算符刪除掉的本質原因)
. this的值,與調用環境的this值相同. (此處,與edition 5所指,非直接調用的eval,視為全局調用並無沖突. 即該情況下,其calling context為global context. 則this應指向global.)
好吧,我必須承認,這一段看起來很繞. 簡單來說,ES3中的eval就是下面這樣子:
1. 它是個內置函數.
2. 它具備把一個字符串的內容,作為ES 代碼,動態執行的能力.
3. 動態執行代碼的作用域隸屬於 eval函數被執行的位置,所處的那個執行環境相關聯作用域.
4. 動態執行代碼的作用域鏈,也同樣就是eval函數被執行位置,所處的那個執行環境的作用域鏈.
5. 動態執行代碼中 聲明的變量, 我們可以 用delete 運算符刪除掉.
標准的不足 : ES3 沒有明確說明 其內部的 arguments, 以及 arguments.callee 以及 arguments.callee.caller 應該是誰. 導致各個引擎實現,差異巨大. 比如v8的實現,就很奇葩(不是重點,掠過.).
然后,我們再來看看ES5 :
ES5,很奇葩的,在ES3的基礎上,把eval這貨,搞的人不像人,鬼不像鬼,既有內置函數特征,又有關鍵字特征. 為什么這么說呢?因為ES5引入了一個,被稱為direct call(直接調用)的概念.
直接調用的概念,請猛擊鏈接: 深入剖析,什么是eval的直接調用.
下面是簡單的幾個demo :
;(function(){ var a = 1; var fn = eval; eval('typeof a'); //number (eval)('typeof a');//number (1,eval)('typeof a');//undefined fn('typeof a');//undefined }());
ps :
讀了前面的內容,可能這里唯一需要說額外明的只有 (eval) 的情況, 這里分組運算符在生成語法樹時被消除了. 所以(eval) 也是直接調用. 就如同(obj.fn()) 中this 指向obj 一樣. 而 (1,obj.fn) 中this 則為global或undefined(嚴格模式).
那么,direct call,這個設計真的合理么? 我個人覺得這是不合理的設計. 好的設計應該是:
eval('code', scope); scope 可以是布爾, 用來指定是否全局,默認為false. 即非全局,同 direct call的效果.
true則為全局. 怎么也好過這種讓人蛋疼的設計. 進一步講,這種設計, 還可以讓在將來,讓我們指定eval的scope變為可能.
當然.這個如果設計的不好,就成了第二個 with了... 也不是現在我們要關心的地方.
ES5 嚴格模式對eval的影響 :
ES5,在定義了奇葩的eval后,又搞出了新花樣,因為ES5引入了嚴格模式的概念,所以嚴格模式對 eval的影響,是我們不得不提到的.
嚴格模式下,eval代碼中的變量初始化..其外部不再可訪問,也就是說eval有了一個獨立的變量環境(參考ES3的variable object)
ES5嚴格模式科普鏈接 : http://www.cnblogs.com/_franky/articles/2184461.html
參考代碼:
'use strict'; eval('var a = 1;'); typeof a;// undefined
ps : ES3,和ES5 的差異.導致瀏覽器不同,瀏覽器版本不同,他們基於的ECMAScript標准版本不同.導致了各種實現差異.不在本文討論范圍...
以上部分,就是ECMA262對eval的一些定義和科普部分.. 后面,則會介紹一些有趣的東西.
我們一直耳熟能詳的一句話出自老道之口 : eval is evil . 請容我在此篇里,唯一想輸出的看法就是, 這句話要有前提,那就是使用不當. 我這里僅指出,可能存在的副作用. 如何使用eval,則是大家自己思考的事情. 可能我會在最后面提我自己的一些權衡.
demo1 :
<script> // eval('var a;'); </script> <script> var a = 1; delete a; alert(typeof a) </script>
這個例子中,注釋掉eval部分和不注釋的結果存在差異. 這顯然是一種副作用. 而且是后面的腳本塊中delete a;操作之前,沒辦法修正的.
導致這個問題的原因簡單解釋下: 變量初始化階段,初始化的變量,會有一些特性(ECMAScript內部用於描述屬性狀態的東西.),其中有一個特性,ES3稱為{DontDelete},ES5稱為[[Configurable]],被應用來描述其是否可以被刪除.或特性是否可被改寫. 而前面我們知道, eval內部聲明變量,ES3中,不具備{DontDelete}特性,或ES5中被描述為 其[[Configurable]] = true. 那么就導致該變量可以被delete刪除. 又因為,ES中對變量初始化過程,有嚴謹描述. 即.遇到變量聲明, 就去看當前變量對象(ES5,被稱為個環境記錄)是否有同名屬性.如果有,就什么都不做. 所以先聲明的變量,具有優先級(函數聲明和形參則不同於變量聲明,不屬於本文討論范圍). 所以結論就是,非嚴格模式下, eval內的標識符聲明.有副作用.
demo2: 上一組圖來說明.(注意,該例子,僅用於說明v8引擎.不同引擎實現會有差異.)
(1).
(2).
顯然,這兩張圖,充分說明eval ,對v8的一些優化策略,是有影響的. 當然,能起到類似影響的,不僅僅是eval ,如果這里我們return一組函數,互相之間內部引用的變量不同.同樣會導致類似的問題.即函數a中引用,而b中沒有引用.但是都會被扁平化的保存在作用域鏈的上一層中. 這里我和阿灰有少許分歧,他認為v8為了更好的GC,做了這件事, 我認為是為了減少扁平化處理作用域鏈,所帶來的額外的內存占用. 但是我們都是黑盒推測. 所以僅供參考啦.
demo3 :
參考代碼:
var fn = function (x) { return x + x ; } var test = function () { return function(x){ return fn(x); } //eval('') }(); console.time('test'); for (var i = 1000000; i--; test(1)); console.timeEnd('test'); //eval('')
demo3中注釋掉的兩個不同位置的eval,對v8性能的影響程度會不同. 但內層的eval,影響程度會是沒有eval時的20倍. 即使它出現在 return 后面, 而永遠不會執行.
此處數據有誤,開啟console,似乎會影響跑分.把console面板關閉. 直接跑,用alert輸出,則雖然eval的出現,仍然有性能損耗,但是其影響,微乎其微. 就上面的例子,有沒有eval,實際只有100ms的損耗. 而不是xxx倍.
所以v8一個簡單粗暴的做法就是. 詞法、語法分析期,發現eval,並且確認它屬於direct call直接調用. 就會不同程度的干掉一些優化策略... 這里的優化策略,類似被稱為fast property access 的原型鏈優化方式. 通過對"熱代碼"所在執行環境,創建隱藏類(hidden class) 來實現.一種內聯綁定的概念. 用這種綁定關系,代替原始代碼中的標識符或屬性查找. 顯然,這種優化,在很深的作用域鏈,或很深的對象屬性查找中,在熱代碼中,對性能有十分可觀的提升. 這種機制,類似前面提到的v8引擎,扁平化作用域鏈的實現機制. 但是很遺憾. eval('')破壞了這一切.原因是eval內部可以在當前作用域中插入額外的東西,包括函數,變量,等.因為eval的參與,會導致,實際的標識符或表達式,進行evaluate的結果發生改變.導致 優化的內聯綁定不是正確的結果.
就寫到這吧.再不睡覺,明天沒法上班了... 回頭有時間,再填充些demo,或者補下遺漏的東西吧.....
哦對,表達下我對eval的看法, 該用就用,如果確認沒有安全問題, 又沒有對系統造成性能瓶頸. 大膽的用吧.. 因為在某些需求上,它是一個很好的選擇.... 好吧,這部分,以后有時間再補吧.....挺不住了....
最后,轉帖下阿灰的,對於以上問題的更多測試,以及結果,僅供參考:
http://www.otakustay.com/eval-performance-profile/
http://www.otakustay.com/about-closure-and-gc/
補充之前沒說到的三件事:
1. eval('xxx') 本身的情況.
v8引擎中,對 eval('xxx'), Function('xxx') 的結果,會進行緩存. 多次執行相同內容的話,性能問題並不那么嚴重. 部分js模板,也采用類似的緩存 編譯結果的方式.也是出於類似的考慮.
2. Function, 以及 非direct call的 eval ,在demo2中,不會有副作用.因為他們的calling context被視為同 global code中的eval.
3. 權衡.
這一點考慮再三還是寫一下,我個人的看法. 我認為eval在必要時,是完全可以用的. 比如@老趙的 wind.js的場景. 事實上,在眾多國內框架中,老趙的wind.js真是我十分喜愛的一個東西. 創新,特定場景需求.生產力. 都是他的價值. 只有當你被 各種深度嵌套的異步回調,多次強奸的時候.你才會試圖去改善開發方式. 也許你試過promise pattern 的各種框架. 但在我看來,從生產力的角度,完全無法和windjs 相媲美.
另外說說我實際項目中遇到的一個情況. 我們有個特定的序列化,反序列化數據的需求. 但是為了同時對數據有校驗性,我們把一些token作用的東西,並入到 序列化反序列化的規則中去. 這時候,我針對性的寫了一個,JSON文法的子集約束(並不是真子集,而是子集基礎上,額外又增加了一些語法限制). 通過巧妙的規則和約束上的設計.我砍掉了需要語法分析才能處理的情況. 這樣就只需要實現一個狀態機.只做詞法分析,就能完成反序列化工作 以及 token校驗. 我甚至為了提升性能, 違背狀態機設計的一些原則. 比如,把 undefined 的9個狀態.放在一個狀態中,以避免狀態遷移帶來的性能損耗. 所以我的ll1 詞法分析器是一個投機取巧,違背設計原則的產物.但是,它實現了反序列化時,時間復雜度O(n) ,所以它帶來了一定行的性能. 但是最終這個方案被廢棄. 而改用正則校驗+eval. 原因是eval更快. .這時候性能問題主要集中在正則上(不在本文討論范圍). 所以eval的慢.要看和誰比.不是么? 而且.一但反序列化的數據有重復出現的情況,還可能在高級瀏覽器中,命中緩存... 當然,其實在你不需要依賴作用域鏈的場景. Function 來動態執行,可能是更好的方案. 但是總有些場景你離不開他. 那就用吧.
最后,只有你真正認為eval的使用,導致內存開銷和性能損耗 在某些高性能需求方面,無法接受. 但是又舍不得windjs帶來的生產力.@老趙不是也早就想到了么? 那就是預編譯... 這個所帶來的額外的成本,僅僅是個習慣問題.你把該做的都自動化以后. 一切都不是問題啦.