標籤:
上一篇(JavaScript中你所不知道的Object(一))說到,Object對象有大量的內部屬性,而其中多數和外部屬性的操作有關。最後留了個懸念,就是Boolean、Date、Number、String、Function等有更多的內部屬性,而它們分別是什麼呢?
這些內部屬性不能像Object的內部屬性一樣一言以蔽之,因為它們各有各的用處和特點。其中核心的部分自然是最特殊的對象,Function對象。我們先從簡單的開始:
- [[PrimitiveValue]]: 值的類型是基礎資料類型。所以所有的封裝類比如Boolean、Number、String都有此內部屬性,其中Date也有,用來儲存時間戳記。
- [[Construct]]: 值的類型是方法。傳入參數列表,返回Object。Function對象特有,當使用new func()調用的就是這個內部屬性。所以擁有這個屬性的方法也可以叫做構造器。注意區分構造器的prototype對象下的constructor屬性。
- [[Call]]: 值的類型是方法。傳入參數列表,返回任意資料類型。Function對象特有,當使用func()調用的就是這個內部屬性。
- [[HasInstance]]: 值的類型是方法。傳入任意值,返回Boolean。檢測這個參數的原型鏈上是否有此函數的prototype,即檢測參數及其原型中是否有對象是此函數作為構造器時建立的。
- [[Scope]]: Function對象特有,每當函數執行時使用此內部屬性建立範圍鏈加入到新的執行環境中。
- [[FormalParameters]]: 形參列表。Function對象特有。
- [[Code]]: JS代碼。Function對象特有。
- [[TargetFunction]]: 如下
- [[BoundThis]]: 如下
- [[BoundArguments]]: 如下
以上三個為Function.prototype.bind建立的函數特有的內部屬性。
var arr1 = [1, 4, 5], arr2 = [2, 3];var func = Array.prototype.splice.bind(arr1, 1, 0);func.apply(arr1, arr2);console.log(arr1); //[1, 2, 3, 4, 5]
例子中,func是通過bind建立的函數,其內部屬性[[TargetFunction]]對應Array.prototype.splice,[[BoundThis]]對應arr1,[[BoundArguments]]對應1,0組成的列表。
上面列出的屬性乍看一下好像都能理解,但細思恐極,比如標紅的範圍和執行環境,都是很抽象的概念,我們現在就來完整的剖析下這些概念。
首先引入的概念就是可執行代碼。ES規定三種可執行代碼:全域代碼、Eval代碼、函數代碼。當執行到這三種代碼的時候,解譯器會建立並進入新的執行環境,當代碼運行完畢的時候,解譯器會退出並銷毀當前執行環境,並回到前一個執行環境。
到現在為止執行環境還是一個抽象的概念,那執行環境的具體實現是怎樣的呢?首先,它有三個要素:詞法環境、變數環境、this綁定。this綁定我們都比較熟悉,那詞法環境和變數環境分別是幹什麼用的呢?簡單而不太嚴謹的說,詞法環境就是範圍鏈,用來取變數的值;而變數環境暫時理解為當前範圍吧,用來賦變數的值。我們舉個栗子吧:
var a = 0;function test0() { console.log(a); //0 console.log(window.a); //0}function test1() { a = 1; console.log(a); //1 console.log(window.a); //1}function test2() { var a = 2; console.log(a); //2 console.log(window.a); //1}test0();test1();test2();
這裡涉及4個執行環境:全域執行環境、test0-2的執行環境。
進入test0的執行環境以後,要列印出a,當前的範圍中沒有定義這個變數,於是沿著範圍鏈找,找到了全域執行環境中定義的a,於是列印出來0,相信這裡沒什麼問題。
進入test1的執行環境後,這時已經退出了test0的執行環境。這時候給a賦了一個值,可能就有人立即想到,賦值?ok,變數環境,作用於當前範圍,所以a應該是1,window.a應該還是0,但結果卻不是這樣的。我們把a = 1拆開來看,首先是取a這個變數,當前範圍是沒有這個變數的,所以a這個變數指向了全域執行環境中的a,然後才是賦值,1自然賦給了全域執行環境中的a。那麼怎麼讓當前範圍中有這個變數呢?聲明!即test2中的var a = 2,我們同樣拆開來看,先是聲明了a,所以在當前範圍中綁定了a這個變數,即在變數環境中添加了a,然後取a這個變數,從範圍鏈中的當前範圍就找到了a,最後把2賦給a。
所以我們來規範一下上面的定義:詞法環境用於尋找變數,可以理解為範圍鏈。變數環境呢,不能再理解為範圍了,它是一個用來儲存當前執行環境和變數之間的綁定資訊的對象。注意這裡隱藏了一個關鍵點:取變數是以範圍為單位尋找的,而聲明變數是以執行環境為單位儲存的。不理解沒關係,繼續往下走。
例子中細心的話可能察覺到一絲不對勁:為什麼我在全域執行環境中聲明的變數可以通過全域對象訪問呢,那麼我在test2中聲明的a可以通過test2.a訪問嗎?
當然是不行的。因為範圍分為兩類:一種是聲明式的,一種是對象式的。function產生的範圍是聲明式的,而全域執行環境對應的範圍是對象式的。對象式範圍中聲明的所有變數都可以通過此對象的屬性進行訪問,而聲明式則可以定義不可以被修改的變數,你在strict 模式下修改function中的arguments試試。
相信看到這裡基本已經陷入混亂了。我來整理幾個問題:範圍和執行環境到底什麼關係?聲明式和對象式範圍只對應function和全域嗎?
首先,一個執行環境中是可以產生多個範圍的,但都有一個基礎範圍。然後其他範圍怎麼產生呢?聲明式的範圍還有一種方式產生,就是catch語句,我們在catch(e){}的語句中可以通過e訪問到錯誤對象,就是產生了一個聲明式的範圍,並在這個範圍裡添加了變數e。而對象式的範圍也同樣還有一種方式產生,就是with語句。
var a = { name: ‘tarol‘},name = ‘okal‘;with(a) { console.log(name); //tarol console.log(window.name); //okal name = ‘ctarol‘; console.log(name); //ctarol console.log(window.name); //okal var age = 18;}console.log(a.age); //undefinedconsole.log(window.age); //18
例子中,進入with語句後,產生了一個新的對象式範圍,並添加到了範圍鏈的頭部,所以在語句中對變數name的訪問是取a.name的值,對變數name的賦值也是對a.name的賦值。疑痛點在var age = 18後,age這個屬性沒有賦給a,而是賦給了window。就像小夥a看上了姑娘age,說好了也訂了婚,最後姑娘嫁給了a的老大window(怎麼有種曹操和關羽的既視感)。其實原因就在上面,with語句修改了執行環境的詞法環境,所以把訪問變數的規則改了,但是沒有修改變數環境,所以聲明的變數統統都給了全域執行環境中的基礎範圍(a:我怎麼把這茬給忘了?)。
另外把var age = 18中的var去掉,結果還是一樣的。因為娶變數的時候,媒婆找啊找,找到最後一個範圍了都沒找到,於是就停到那裡了,突然看到有個值送上門來,也懶得換地方了,當場就在這個範圍把這個值打扮成了個變數。所以前一個例子中去掉var a = 0也不影響test1()的結果。
注意!注意!注意!對整個範圍鏈中未定義的變數賦值,這個變數會綁定到範圍的尾部,而給這個原型鏈中未定義的屬性賦值,這個屬性會綁到原型鏈的頭部即當前的對象中。一次給兩個栗子:
var a = {};function test() { with(a) { age = 19; }}test();console.log(window.age); //19
var a = {}, b;b = Object.create(a);b.age = 18;console.log(a.age); //undefined
好了,說了那麼多,回到之前的內部屬性[[Scope]]。它是在建立函數產生,值是建立時的範圍鏈;並在執行函數時取用,產生新執行環境中的範圍鏈。
注意這個[[Scope]]是建立函數是產生!
注意這個[[Scope]]是建立函數是產生!
注意這個[[Scope]]是建立函數是產生!所以無論在哪裡執行這個函數範圍鏈都是一樣的。
給個栗子:
var a = { age: 18},age = 19;with(a) { var test0 = function() { console.log(age); }; function test1() { console.log(age); } test0(); //18 test1(); //19}!function() { var age = 20; function test2() { console.log(age); } test0(); //18 test1(); //19 test2(); //20}()test0(); //18test1(); //19
需要注意的是,在with語句中建立函數,如果此函數的範圍鏈中想插入a,要用test0的聲明方式,而不是test1。至於為什嗎?那是變數聲明和函式宣告的時序問題,在進入執行環境後,會先遍曆所有的變數聲明和函式宣告,會產生函數對象,但不會對變數賦值。這也是為什麼JS中函數可以先調用後聲明,因為你隨便寫在哪裡,一進入執行環境就函數就產生了。而test1函數產生的時候還沒有run到with語句,[[Scope]]自然是全域執行環境的基礎範圍鏈。test0開始只聲明個undefined的變數,到with語句才進行賦值,所以這個函數的[[Scope]]中加入了a。
寫的比較亂,而且為了便於理解,並沒有正常化一些概念,比如Environment Records變成了範圍,Lexical Environments變成了範圍鏈。以後再行整理,這個系列的下一篇(如果有的話)會是RegExp類型的內部屬性和arguments的內部屬性。
JavaScript中你所不知道的Object(二)--Function篇