深入理解函數內部原理(1)——函數定義、調用、解析、執行

在閱讀本博客之前先閱讀:
執行環境: http://blog.csdn.net/wmaoshu/article/details/60466990
引用規範類型:http://yanhaijing.com/es5/#80
本系列博客主要說一下一個函數從定義到調用到解析到執行的過程,以便於更好的理解後續介紹的閉包、this等概念。先介紹內部原理,然後通過一個實例說明一下這個原理。然後是一些對這個原理一部分的應用技巧。



內部原理

函數定義

官方文檔:http://yanhaijing.com/es5/#237
過程一:執行函數定義
函數定義的方式有兩種,一種是函數聲明的方式,一種是函數表達式的方式,其中又分爲匿名的函數表達式和具名的函數表達式。
對於這三種定義函數的方式有許多的不同,現在先介紹一個不同之處就是在函數定義的時候不同。
無論是哪一種函數定義方式,到最後都調用創建函數對象這麼一個過程,在這個過程中傳入一個參數爲scope,這個參數的目的就是爲了賦給內部屬性[[scope]]。
(1)對於函數聲明來說傳入的scope爲函數所在的執行環境的變量環境。
(2)對於匿名的函數表達式來說傳入的scope爲函數所在的執行環境的詞法環境。
(3)對於具名的函數表達式來說與上述兩種方式不同,sope爲一個創建的詞法環境,在這個詞法環境中外部詞法環境爲所在的執行環境的詞法環境,而環境記錄項爲聲明式環境記錄項,聲明式環境記錄項中會有一個屬性,這個屬性就是這個函數表達式的名字並且與創建的這個函數對象綁定在一起。函數的遞歸就是對這個應用。

過程二:調用創建函數對象過程
在這個過程中,創建一個空對象,添加各種內部屬性比如[Class] [Put] [Get] [Prototype]等最重要的是將有一個[Scope]內部屬性賦值爲傳入的scope。然後添加一些函數特有的內部方法[Call]、[Construct]、[HasInstance]。同時還會定義一些屬性比如length爲形式參數的個數、constructor爲只想這個函數對象的指針、prototype爲一個空對象,目的是爲了實現夠當構造函數時實現原型繼承。


函數調用

官方文檔:http://yanhaijing.com/es5/#164
過程一:令 ref 爲解釋執行 MemberExpression 的結果,取值
在這裏的MemberExpression 可能有三種類型一種是標識符、一種是屬性訪問表達式、一種是一個函數表達式可能是匿名的後者也有可能是具名的。下面分別介紹這三種方式:

(1)使用函數標識符的方式
在這裏要介紹一下標識符解析的過程,不只是函數調用中的標識符,對於所有的標識符解析都試用。
官方文檔:http://lzw.me/pages/ecmascript/#144
其實標識符解析的過程就是在作用域鏈中從下到上(從內到外)匹配的過程。GetIdentifierReference 函數就是這麼做的。把當前的執行環境中的詞法環境和標識符字符串已經是否嚴格模型傳入GetIdentifierReference(lex, name, strict)內部函數中,然後開始執行:
1:如果傳入的詞法環境lex爲null,則返回一個引用{基值:undefined,引用名稱:name,嚴格引用:strict}
2:得到lex中環境記錄項的值爲env
3:經過搜索匹配env中存在這個標識符,則返回一個引用{基值:env,引用名稱:name,嚴格引用:strict}
4:不存在的話讓lex爲傳入的lex這個詞法環境的外部詞法環境,繼續調用GetIdentifierReference 。
無論這個標識符在作用域鏈中存不存在都會返回一個引用類型。通過getValue函數取得該值

<script>
function add(){
};
add();
</script>

這裏add標識符經過解析返回的是{base:window,name:add,strict:false}

(2)使用屬性訪問表達式的方式
先了解一下屬性訪問表達式內部工作原理。
官方文檔:http://lzw.me/pages/ecmascript/#162
可以看出返回的也是一個引用類型,這個引用類型的基值爲屬性訪問操作對象,引用名字爲操作字符串,嚴格引用視情況而定。通過getValue函數取得該值

<script>
var o = {
    add: function(){
    }
};
o.add();
</script>

這裏o.add屬性訪問標識符經過解析返回的是{base:o,name:add,strict:false}

(3)使用函數表達式
比如如下:

<body>
(function add(){    
})();
</body>

當解析器遇到這個函數調用的時候,回西安之行add函數表達式,然後根據定義的步驟會返回一個add函數的對象,所以函數調用操作符“()”前面的操作數不和前面兩種情況一樣是引用了,而是一個函數對象,但是用過getValue得到的值仍然是這個函數對象。

過程二:判斷是否是一個可被調用的對象
判斷函數調用操作符“()”前面的操作對象的返回值引用或者非引用經過getValue之後的到一個值,如果這個值是一個對象並且有[Call]內部函數的話,那麼就認爲這個對象可以被調用。

過程三:確定this的值
經過過程一可以得知,得到的可能是引用類型或者非引用類型,對於非引用類型,則直接將this設置爲undefined。對於引用類型,則取決於基值得值:
如果基值爲一個(Object、number、string、boolean)類型之一的話,那麼將this賦值爲基值。
如果基值是一個環境記錄項,那麼this爲ImplicitThisValue 結果,一般如果是全局對象的話由於全局對象中provideThis爲false,所以爲undefined。

過程四:調用[Call]
將上述第三過程中確定的this和參數列表傳入函數的[Call]方法中,這個方法會創建一個執行環境push邏輯棧中爲進入函數代碼做準備。


進入函數代碼

官方文檔:http://lzw.me/pages/ecmascript/#150
過程一:確定最終的this
如果說處於嚴格模式下,那麼執行環境中this屬性將直接是用過[Call]傳入this的值即使是undefined。
如果不是嚴格模式的話,如果傳入的this爲null或者undefined,會用全局對象來替代。
如果傳入的this不是對象類型,那麼就儘可能的轉化爲對象,最終在複製給執行環境中this屬性,這個執行環境中this屬性將作爲在這個執行代碼塊中this標識符的值。所以說this的值是在運行的時候確定的。

過程二:確定詞法環境和變量環境
先創建一個詞法環境,然後讓外部詞法環境這一屬性值爲該函數的[Scope]內部屬性的值。然後將這個創建的詞法環境複製給執行環境的詞法環境屬性和變量環境屬性

過程一:執行定義綁定初始化
開始預覽一邊代碼,執行如下操作:
如果有參數,則將參數和值綁定到環境記錄項中。
如果有函數聲明,則將函數與定義後創建的返回的函數對象綁定到環境記錄項中。
並且創建一個arguments屬性然後將傳入的實參作爲值列表複製給她。
如果遇到變量表達式即使是函數表達式,也是僅僅將變量名和undefined綁定到環境記錄項中,並不是與實際的值,實際的值在接下來執行代碼階段進行解析賦值。
從這裏可以看出函數聲明和函數表達式第二個不同之處:
那就是在執行直線函數聲明會將函數名和值綁定到環境記錄項中,而函數表達式僅僅是將函數名和undefined綁定到環境記錄項中,所以函數聲明的函數可以在聲明語句之前調用,這就是所謂的函數提升,而函數表達式則不能。



最終函數開始執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章