1. 程式人生 > >簡談JS的原型鏈和作用域鏈

簡談JS的原型鏈和作用域鏈

談起js的原型鏈和作用域鏈,我覺得還是和圖結合起來說比較明白,手繪了一些圖片,

原型鏈

建立一個函式及呼叫建構函式建立例項的背後

當我們建立了一個新函式時,都會自動為該函式建立一個prototype屬性,這個prototype屬性是一個指標,指向這個函式的原型物件。而這個原型物件會自動建立一個constructor屬性,constructor也是一個指標,指向prototype屬性所在的那個函式,即我們建立的那個新函式。
當我們以new形式呼叫我們剛剛建立的那個函式時(即是以建構函式呼叫,可以把我們剛剛建立的那個函式理解為類,儘管js本身沒有類的概念),我們建立了一個例項,這個例項內部有一個[[Prototype]]指標,指向建構函式的原型物件,即constructor所在的物件。(我們可以通過proto

屬性訪問這個[[Prototype]],也可以通過Object.getPrototypeOf()來訪問)

這裡寫圖片描述

重寫原型物件

重寫函式的原型物件後,函式的prototype指向新的物件,而之前建立的例項的[[prototype]]指向的物件沒有改變,儘管這個物件的constructor仍然指向函式,但是當前函式的原型物件和例項之間已經沒有聯絡了。

這裡寫圖片描述

繼承中的原型鏈及預設的原型物件

直接上圖就好了,,,

這裡寫圖片描述

作用域鏈

每個執行環境都有一個與之關聯的變數物件,環境中所有的變數和函式都儲存在這個變數物件裡面。這個變數物件雖然我們無法訪問,但是直譯器會在後臺使用。
當代碼在一個環境中執行時,會建立變數物件的一個作用域鏈,作用域鏈的前端始終是當前執行的程式碼所在環境的變數物件,下一個變數物件來自外部包含環境,一直延伸到全域性執行環境。
每個函式都有自己的執行環境,當執行到一個函式時,該函式的環境被推入一個環境棧,函式執行後將其彈出。

作用域鏈與閉包有著密切的關係

作用域鏈實質上是一個指向變數物件的指標列表。
閉包,簡單說就是有權訪問另一個函式作用域中變數的函式(一整句話感覺還是覺得比較繞,分開說就是:閉包是一個函式,它不僅能夠訪問自己作用域中的變數,還能訪問另一個函式作用域中的變數)。閉包常見的建立方式是在一個函式中返回另一個函式。

function Outer() {
    var a = 5;
    return function() {
        console.log(a);
    }
}
var fun = Outer();
fun();

如上,即使這個內部函式被返回了,而且是在全域性呼叫的,但是它仍然可以訪問變數a。這是因為內部函式的作用域鏈中包含了外部函式Outer()的作用域。

函式被呼叫時發生了什麼?
1. 建立一個執行環境
2. 建立作用域鏈
3. 使用arguments及其他命名引數初始化活動物件

舉個例子:

function add(a, b) {
    return a + b;
}
var value = add(1, 2);

以這個簡單的函式為例子,畫出執行環境及作用域鏈圖:

這裡寫圖片描述

建立add函式時,會建立一個預先包含全域性變數物件的作用域鏈,這個作用域鏈儲存在函式內部的[[Scope]]屬性中。
當呼叫add()時,會為函式建立一個執行環境,然後複製函式[[Scope]]屬性中的物件構建執行環境的作用域鏈,之後函式的活動物件被建立並推入執行環境作用域的前端。

正常情況下,函式執行完畢後,區域性活動物件被銷燬,記憶體中僅儲存全域性執行環境的變數物件,但是閉包的情況有所不同。
在一個函式內部建立的另一個函式,會將外部函式的活動物件新增到它的作用域鏈中,以我們第一段程式碼為例子。Outer()中定義並返回了一個匿名函式,這個匿名函式的會將Outer()的活動物件新增到自己的作用域鏈中。當匿名函式被返回後,它的作用域鏈包含Outer()函式的活動物件和全域性變數物件,所以這個匿名函式可以訪問Outer()中的變數,即使他是在全域性呼叫的。Outer()在執行完畢後,其執行環境的作用域鏈會被銷燬,但是它的活動物件不會被銷燬,因為匿名函式的作用域鏈仍在引用這個活動物件。

這裡寫圖片描述