js深入(三)作用域鏈與閉包

在之前我們根絕對象的原型說過了js的原型鏈,那麼同樣的js 萬物皆對象,函數也同樣存在這麼一個鏈式的關係,就是函數的作用域鏈

作用域鏈

首先先來回顧一下之前講到的原型鏈的尋找機制,就是實例會先從本身開始找,沒有的話會一級一級的網上翻,直到頂端沒有就會報一個undefined

同樣的js的機制就是這樣的,函數在執行的時候會先函數本身的上下文的變量對象中查找,沒有的話,也會從這個函數被創建的時候的父級的執行上下文的變量對象中去找(詞法環境),一直找到全局上下文的變量對象(比如客戶端的window對象),這個多層的執行上下文的鏈式關係就是函數的作用域鏈

盜一張圖

作用域被創建的時機

大家可以看到,我在控制檯聲明瞭一個函數,並且打印了他,這個a函數的裏邊有一個[[scope]]屬性,
這是一個內部屬性,當一個函數被創建的時候,會保存所有的父級的變量對象(詞法環境)到這個裏邊,比如說上圖中 就有一個global 屬性展開後,往下找你會發現很多我們常見的屬性和方法,比如alert等等

如圖

函數作用域的生命週期

姑且叫他生命週期,我是這麼理解的,當進入一個函數的上下文,經歷了創建階段之後,就會把函數的作用域鏈創建出來,直到銷燬這個上下文,這個作用域鏈也是存在的

先來一個正經的例子

function a(){
    var aaa = 'aaa';
    return aaa;
}
checkscope();

這個函數的生命週期是這樣的

  • 首先函數被創建,先把函數的作用域鏈保存到函數的[[scope]]屬性上邊
a.[[scope]] = [
    globalContext.VO//這個也就是我們上邊圖片裏邊看的golbal
];
globalContext 全局上下文 VO 這個之前沒有介紹 是Variable object的簡稱,也就是之前經常提到的變量對象
還有一個AO ,這個AO指的是函數被激活的時候(被執行)得活動對象
  • 創建完成之後,執行到a函數,創建了a函數得執行上下文,並壓入執行棧裏邊

現在執行棧裏邊已經有了兩個執行上下文一個globalContext還有一個aContext

  • 到了a函數之後,首先會做一些列得準備工作,就是之前講到得函數得arguments,this等等

首先第一步複製之前得[[scope]]屬性,創建作用域鏈

aContext = {
    Scope: a.[[scope]],
}

然後開始初始化活動變量 argments對象 形參,函數聲明,變量聲明等等

最後把把活動變量也塞到作用域鏈中去

以上,一個函數得準備工作就算是做完了,然後下一步就是函數得執行階段

  • 之前講過,在之後階段得時候函數會根據代碼給之前得活動對象賦值,然後執行裏邊得代碼,直到執行完畢
  • 最後,函數執行完畢,函數得上下文被從上下文棧中彈出銷燬

在彈出得最後時候,a函數得結構大概長成這個樣子

aContext = {
    AO: {
        arguments: {
            length: 0
        },
    },
    Scope: [AO, [[Scope]]]
}

接下來我們在舉一個不正經得例子,就是爲了證明一下作用域鏈即使在函數被銷燬後,也會存在這麼一個事實

閉包

首先什麼是閉包,閉包是指在一個函數內部能夠訪問不是函數得參數,也不是局部變量得函數,所以廣義得講我們用的所有得函數都是可算作是閉包,都能訪問全局變量。。。

不過工作中不是這樣子得,說正題,給上邊得問題舉個例子


var item = '1'
function a(){
    var item = '2'
    function b(){
        return item
    }
    return b;
}

var foo = a();
foo();

試着猜想一下這段代碼得執行過程

還是來一步一步得解釋一下

  • 首先不用多想,進入全局代碼,創建全局執行上下文,推入執行棧,全局上下文得一系列初始化
  • 然後創建a , 創建上下文,推入執行棧,一些列得初始化
在執行a得時候創建了b函數,這個時候,還記得上邊之前說過得把,作用域鏈是在被創建得時候確定得

這個時候得b函數得作用域鏈應該是這個樣子的

bContext = {
    Scope: [AO, aContext.AO, globalContext.VO],
}

這個是重點,我們先把執行過程說完

  • 在a函數執行完畢之後,a的上下文棧被彈出
  • 然後在後邊執行b函數,然後一樣的套路,進上下文壓入棧
  • 進棧一些列的初始化
  • 執行完畢b的上下文被彈出

上邊已經把順序說的很清楚了對吧, 執行過程是a進棧出棧,b進棧出棧,但是你打印這段代碼的時候
會打印出一個2,就是因爲雖然說a的上下文被銷燬了,但是b的作用域鏈裏邊還是有a的活動對象的
,在b的上下文裏邊可以找到這個item

這也就是我們之前所說的閉包,也符合閉包的定義

  • 創建他的函數的上下文被銷燬,但是他依然存在
  • 在代碼中引用了不是自身的參數或者局部變量

最後放一個網上很常見的面試題

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

從作用域鏈的角度思考一下會打印出什麼結果,爲什麼會打印出這個結果

以上是我對js的作用域鏈和閉包的一些認識,有不足之處,希望批評指正

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