本文是對上一篇隨筆 湊湊熱鬧,給eval做個科普. 的擴展閱讀.
如果您沒看過上一篇,我建議您先去看看,然后再看此篇. thx.
此篇,大量引入ES5 的概念以及名詞. 實屬無奈,因為直接調用概念來自ES5. 如果你覺得閱讀這些東西有些浪費時間. 我也嘗試給一個白話文的解釋.
ES5設計直接調用的目的就是, 讓eval 有改變eval動態執行代碼的scope 為global object的這一能力. 但是很不好的是.他們通過直接調用來實現這種,很抽象的概念來實現這個能力. 而后面那些官方咒語般的描述的本質,其實是想說明一層意思 . 就是 , 如果你的語句里 ,eval 是一個看起來獨立調用, 不轉借它人, 對 eval的使用,好像一個關鍵字來用的那種感覺.就算作直接調用,那么eval內的動態執行的代碼,就如同是執行在eval被調用的函數內,而對應的非直接調用則如果global code一樣.(直接體現作用域的影響).但是這種含義,太難描寫了. 所以 ES5蛋疼的出現了下面的那種定義... 好吧. 如果你覺得看到這里就解決了你的困惑, 我就十分贊成,你放棄后面部分的閱讀...直接離開了. 如果你非想問個為什么...那就繼續看吧...
eval 直接調用,原文描述 :
A direct call to the eval function is one that is expressed as a CallExpression that meets the following two conditions:
1. The Reference that is the result of evaluating the MemberExpression in the CallExpression has an environment record as its base value and its referenced name is "eval".
2. The result of calling the abstract operation GetValue with that Reference as the argument is the standard built-in function defined in 15.1.2.1(內置方法eval 的定義章節).
解釋前的准備:
在描述前要簡單介紹下原文中提到的兩個詞法非終結符.MemberExpression , CallExpression.
CallExpression , 你姑且認為 fn('Franky') 就是一個CallExpression,但其實 fn('Franky') 只是符合CallExpression的一種情況,即 MemberExpression Arguments 這一個語法產生式,你姑且看成fn對應MemberExpression ,('Franky')對應Arguments.
詞法、語法相關知識不是本文重點....就此打住.
簡單來說, ES5定義的 eval的調用 要滿足下面倆條件,則被視為 直接調用:
1.解釋執行CallExpression中MemberExpression的結果,必須是一個,其base值為一個環境記錄(environment record),且 referenced name為 "eval" 的引用類型.
2.把步驟1的結果作為參數,進行GetValue抽象運算的結果,必須就是內置方法 eval.
名詞解釋:
environment record(環境記錄):
Reference Type(引用類型):
ES5 :
由 base value, referenced name(同edition3的 property name), 以及一個叫做 strict reference 的布爾值(用於嚴格模式).構成的一個內部對象.這貨其實就是描述, 某個對象的 叫做xxx的屬性. 對其進行 GetValue 運算,就會調用該對象的內部方法[[Get]] 來獲取該屬性的值.那么 base有可能是一個對象,一個字符串,一個數字,一個布爾值,或undefined, 還有一個就是我們關心的被ES5 稱為 environment record(環境記錄,對應ES3的變量對象)的東西
別奇怪為啥base可以是字符串什么的(這是ES5與ES3的一個區別之處)
參考 - 'Franky'.length 語句按照ES5的的描述,其解釋執行過程大概是這樣子:
1. 先把'Franky'.length 轉換為 'Franky'['length']
2. 解釋執行 'Franky'
3. 對 步驟2的結果,進行GetValue運算(對解釋執行的結果求值的抽象運算,運算元可以是一個原始值,或一個引用類型,好吧又繞回去Reference Type了)
4. 解釋執行'length'
5. 對步驟4的結果進行GetValueu運算
6. 對步驟3的結果進行 ToObject運算(嘗試進行裝箱操作.參考new String('Franky'))
7. 對步驟5進行ToString運算(因為'length' 這部分可能是一個表達式,所以要先解釋執行,然后求值,然后再嘗試轉為字符串.才可能符合屬性訪問器的語法)
8. 返回一個 base值 為步驟5的結果(ES3則是步驟6的,這也是為啥ES5 的base,可以是各種奇葩原始值的原因之一..). referenced name 為步驟7結果, strict reference 為代表當前執行環境是否為嚴格模式的布爾值. 的引用類型
好了,后面如何返回字符串Franky的字符數的,不屬於本文討論范圍(相信你見到new String('Franky')的裝箱過程,應該想得到...).
而上面這個解釋執行過程,ES中被稱為屬性訪問器( Property Accessors) .這個屬性訪問環節,總是要返回一個reference type.然后再進行后面的操作.
列出這個過程,只是想讓不熟悉ES的朋友,對ES的 Reference Type,有個直觀的感受.
GetValue:
你姑且認為他就是對一個引用類型 求值的過程.因為引用類型實際作用是對 某對象上某個屬性的一種描述.所以本質是對就是獲取該對象某個屬性值過程(涉及到對象的內部方法[[Get]],不再詳細介紹). 當然內部過程不是如此簡單. 比如reference中,base 值為
undefined,123,等等.
而當base值為一個環境記錄時,意味着這個引用類型其實是一個被稱為 Identifier Reference的東西. 比如 對 var abc = 123; 中 abc 進行解釋執行的過程,會得到一個標識符引用.就是這個意思.
有了上面這些基礎后,我們再回過頭來看 上一篇 湊湊熱鬧,給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 }());
語句 eval('typeof a') :
符合兩個條件- eval 作為callExpression中的MemberExpression部分,本質上是一個標識符解析,返回一個Identifier Reference(標識符引用). 其base value 即是充當 environment record 的 global object. 而 referenced name顯然就"eval".
語句 (eval)('typeof a') :
符合兩個條件- 原因上一篇提到過,( xxx ) 中分組運算符,在生成抽象語法樹時,只是影響語法樹生成過程,或者說是影響其他元素在語法樹中的位置和順序(從程序運行角度來說,本質是對運算優先級的影響),但不會作為節點保留在語法樹中.也就是在運行時,不會 有額外的與其有關的運算.所以之前我會說它在語法分析期.盡到了義務后,就被消除了.(此處羅嗦的解釋一下,只因為,昨天有朋友表示之前的簡單描述,無法解決他的困擾.). 那么這里的情況其實就和第一個demo是一樣的了.
語句 (1,eval)('typeof a')
不符合條件1中,結果為 Reference Type這一要素. (1,eval) 中分組運算符,雖然同demo中一樣,被消掉. 但是 1,eval 這個表達式 實際的含義是 "," 逗號運算, 即 1和 eval 都是逗號運算符的運算元. 其運算結果為對,eval解釋執行后獲得的reference 進行GetValue的結果. 顯然因為多了一次GetValue 導致結果不符合這個條件了.
語句 fn('typeof a')
不符合條件1 中referenced name為 "eval" ....
下面我要額外說的是一個 特殊的東西.
語句 window.eval('typeof a') //undefined .
為什么要額外補充這個呢,因為其比較特殊. 首先,它 不符合的是 其base值為一個環境記錄(environment record) .這一點,對於熟悉ES5的朋友,可能會比較困惑..因為:
1. window,在ES層面等價global (雖然絕大多數瀏覽器的實現,並非如此)
參考 :http://www.cnblogs.com/_franky/archive/2010/12/20/1911328.html
2. global object 在某些情況下,是作為environment record 存在的.
3. window.eval , 這種被稱為 Property Accessors 的東西.解釋執行的結果,就總是一個 base value 為 GetValue(evalute(window)) ,referenced name 為 "eval" 的一個 引用. 而對這個引用進行GetValue運算得到的結果又一定是 內置方法 eval (除非有人劫持了他們).
那么看起來window.eval似乎符合 兩個條件啊.. 其實不然. 設計上, environment record 對外是不可見的. 從window訪問global時, 顯然算做可見的訪問. 而不是通過詞法環境(Lexical Environments, 好吧又一個新名詞.對應ES3 的作用域鏈的實現機制.前面提到的environment record就是掛在Lexical Environments下面)來找的. 只有發生類似作用域鏈中查找標識符情況.找到global時它才是environment record. 才會滿足條件. 這也是為啥 在任何執行環境(很深的內層函數嵌套中) eval('xxx') 最終找到global.eval.但是卻算作直接調用的原因.
注: 應該注意那些,按照ES3標准實現的引擎的行為. 與本篇描述不符. 比如悲劇的IE8- 的jscript引擎.和其他主流瀏覽器的部分早期版本.
好吧.到了此篇該結束的時候了. 最后送上一個小demo .你能不測試就說出結果么?
;(function (eval) { var a = 'franky'; eval('alert(typeof a)'); })(eval);