我們先從阿灰 的蛋疼的例子開始. demo1:
| function test() { var x = 1; with ({x: 2}) { eval('function foo() { console.log(x); }'); eval('var bar = function() { console.log(x); }'); } foo(); bar(); } test(); |
大多數按照ES5實現的引擎(除了Opera12-)的結果. 列印的都是 兩個2 .我必須先強調下.這個結果,按照標準是錯的. 按照標準都應該是 1 , 2 . Opera12- 會列印兩個1 .這個我們就不討論了.因為錯的更離譜了. 開始正題:為什麼說兩個2不對呢. 這就要從ES5的函式宣告,以及函數運算式說起了.坦白說.寫這篇東西的時候我很抗拒.因為要說清楚這裡邊的細節.實在涉及到太多的東西了.如果每個涉及到的概念都要說到位.有些浪費對ES5很熟悉的朋友的時間.而我並不認為不熟悉ES5的朋友通過一篇詳細的介紹就能建立起正確的認識.所以還是一切從簡吧. 我在這裡假設您知道什麼是 函式宣告的概念,以及函數運算式的概念,以及二者在文法層面的區別.以及 Lexical Environments , LexicalEnvironment, VariableEnvironmet 以及之間的關係 以及 Environment Records
, Declarative Environment Records, Object Environment Records 這些東西的相關概念. ES5 中函數定義章節描述的兩種定義方式 : 1. 函式宣告 2. 函數運算式 這兩種方式定義時不僅僅是文法上的差異.還有建立函數對象時的一些細節差異. 我們要討論的就是 函式宣告使用其所在執行環境的當前的VariableEnvironmet 作為建立函數對象時提供的scope參數.而函數運算式則使用 LexicalEnvironment 作為scope參數.這就導致我們上面例子中的最直接的問題了. 我們知道,在非strict 模式下,ES5是允許使用with(
expression)
Statement 文法的. 但是我們要先明確幾件事. 1.
Statement 文法產生式中.是不能出現
FunctionDeclaration
的. 相對於我們類似的 catch從句的
block 文法產生式也是如此.(顯然所有瀏覽器都沒有按標準實現這部分規範,也就是說我所見過的所有javascript引擎的都允許在with,catch裡 甚至if
statement中等等位置使用函式宣告) 2. 我們的例子和第一條並不衝突. 因為我們前面例子中的函式宣告是放在eval中. 而eval code是
Program .是允許進行函式宣告的. 3. with會在其作用範圍內 建立一個新的LexicalEnvironment .並把其所在執行環境相關聯的LexicalEnvironment 設定為這個新建立的LexicalEnvironment .(也就是說with改變了當前執行環境的LexicalEnvironment) 4. with並沒有改變VariableEnvironmet.在改變LexicalEnvironment 前, LexicalEnvironment 和 VariableEnvironmet指向相同的引用.(with強迫他倆各玩各的了) 5. with 還會為這個LexicalEnvironment 建立一個object environment record .目的自然是 with(
expression) 中的ToObject(
expression)的結果作為標識符搜尋的整個責任鏈的頂端. 6. 非strict 模式下的eval內部的代碼被執行時.會使用eval的調用環境LexicalEnvironment,以及VariableEnvironmet 作為其內部代碼的執行環境的對應的 Lexical Environments(注意我這裡是複數.且單詞間有空格,希望你理解這裡的意思.不詳細解釋了) 現在問題來了.你應該注意到按照我們的已知條件. foo是函式宣告.那麼它的標識符尋找的下一個 Lexical Environment 顯然是原來的 VariableEnvironmet. 而 bar 的則是with建立的新的 LexicalEnvironment . 那麼顯然整個例子的結果就應該是1,2 . 但遺憾的是,我們還沒有找到一款已知的javascript引擎,在此處的實現是符合標準的.... 但是知道這些,並不是我們今天討論的重點.我們的重點是.為啥標準會這樣定義. 以下部分為我個人猜測. demo2:
function test(){ var x = 1; with({x:2}){ function foo(){ console.log(x); } var bar = function(){ console.log(x); }; } foo(); bar();} test(); |
如果我的對標準的解讀沒有錯誤的話。 我就可以斷定此處規範的制定者,自己陷入混亂. 他似乎就是在考慮類似demo2情形是.思路是這樣的: foo的函式宣告,所定義的函數對象,不應該受到with的影響. 但是他似乎忘記了兩件事. 1. 函式宣告是在 控制器一進入執行環境,進行變數初始化階段進行的. 而with 語句的執行,卻是在解釋執行期進行的. 顯然foo的函數定義是早於 with執行的.當with執行時篡改當前執行環境相關聯的 LexicalEnvironment 時. 所有函式宣告早就處理結束了.foo.[[Scope]]早就引用了最初的LexicalEnvironment 了.根本就不應該受到with的影響. 所以這個設計完全是多餘的.. 2. 設計者忘了
Statement 以及
blok 兩個文法產生式中根本就不存在
FunctionDeclaration
.那還折騰什麼呢? 所以從標準角度.也許根本就不應該生生的把Lexical Environments 拆分成 LexicalEnvironment 和 VariableEnvironmet . 要對付with和catch前插的責任鏈節點. 只需要概念描述下即可.但可能是為了描述精準吧?畢竟他要描述with本身特有的那種特性.總之這個屁股並沒有在ES5裡被擦乾淨. ps: 雖然所有已知引擎都沒有遵守第二條. 但我們討論的畢竟是標準.而不是實現的差異. ps: catch從句有類似的建立並篡改當前執行環境LexicalEnvironment 的能力.不過它對應的產生式是 block.且catch並不會使用object environment record ps: SpiderMoneky的早期版本 包括中間的各種跟猴子的分支.都支援
Block
FunctionDeclaration .這種被稱為語句塊函式宣告的特殊的,非標準文法.它會導致上面的demo2的結果有差異.不屬於本文討論範圍 簡單來說就是:
| typeof fn;// undefined { function fn(){} } |
最後. 似乎ES6草案中 函式宣告也不再使用VariableEnvironmet 作為其[[Scope]]了. 不知道標準委員會中,是否有人意識到了這個問題.而做出了修正. 暫時沒精力繼續去問了. 我對ES6的態度還處於觀望狀態.什麼時候release狀態了.再去跟進好了.. 好吧.就這樣吧. 感謝阿灰寫出了這個蛋疼的例子. 感謝 Bosn 和 kenny 還有abcd同學花時間大家一起討論它. 最後.真心話.相比ES5 取消了 scope chain 的概念而用 內外層詞法環境來代替. 我更喜歡ES3的描述.因為那種描述直接是資料結構上的描述.更適合碼農啊顯然.
最後,附加一點東西, 因為前陣子有看到,有朋友認為 function declaration 不能被當做statment那樣出現在 block中,是ES5strict 模式的一個限制. 下面來說說這個問題 .
關於 函式宣告出現在block 或 其他statement應該出現的地方的問題, ES5 12.0 章節末尾有這樣一段描述:
Several widely used implementations of ECMAScript are known to support the use of FunctionDeclaration as a Statement. However there are significant and irreconcilable variations among the implementations in the semantics applied to such FunctionDeclarations. Because of these irreconcilable differences, the use of a FunctionDeclaration as a Statement results in code that is not reliably portable among implementations. It is recommended that ECMAScript implementations either disallow this usage of FunctionDeclaration or issue a warning when such a usage is encountered. Future editions of ECMAScript may define alternative portable means for declaring functions in a Statement context.
基本上就是說,已知的絕大多數ES引擎,都支援把函式宣告,作為一個語句來處理的這種實現. 而實際上這帶來了語意衝突. ES5的建議是禁止這種行為, 或者發出一個警告.. 並且在將來的ES版本中 基於這兩種建議中的一個.來作為標準的行為…
也就是說,更加強調ES引擎,不應該讓函式宣告 出現在block 等地方… 所以部分引擎(比如v8和猴子)在實現ES5 strict 模式時, 順便也明確禁止了這種行為(但應該明白, 這個strict 模式限制,並不來自ES5 strict 模式的官方限制.而是引擎實現者自己出於某種考慮而實現的)… 但我個人認為. 這個約束是多餘的. 因為本身我們觀察產生式就會發現. 本來就不應該出現這種支援的. 也就是說, 非strict 模式變體 , 也不應該出現這種使用函式宣告的方式… 何必多此一舉的在樣模式中強調呢? .. 哦 當然, 出於對事實現狀的尊重,如果粗暴進位非strict 模式變體代碼也拋異常的話, 老代碼可能就會出問題….所以 這也許是 部分引擎實現者的彌補措施吧.
至於ES6如何規定這裡, 我們拭目以待吧. 坐等ES6 release .
另附ES5strict 模式 一篇連結 : http://www.cnblogs.com/_franky/articles/2184461.html