JavaScript作用域及作用域鏈

本文摘抄自冴羽的博客 JavaScript深入系列15篇正式完結](https://github.com/mqyqingfeng/Blog)

本文將先從 JavaScript作用域 聊起,然后從執行上下文的創建過程分析 JavaScript 作用域鏈 以及 相關的活動變量。

靜態作用域

作用域
作用域規定了如何查找變量,也就是確定當前執行代碼對變量的訪問權限。
JavaScript 采用詞法作用域(lexical scoping),也就是靜態作用域
因為 JavaScript 采用的是詞法作用域,函數的作用域在函數定義的時候就決定了。
而與詞法作用域相對的是動態作用域,函數的作用域是在函數調用的時候才決定的。

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 結果是 ???

總之一句話:

函數的作用域在函數定義的時候就決定了

函數會沿著作用域鏈去查找變量。

那么為什么 JavaScript 是靜態作用域呢?JavaScript 引擎是怎么處理函數的呢?
以下將進行詳解。

執行上下文棧

如果要問到 JavaScript 代碼執行順序的話,想必寫過 JavaScript 的開發者都會有個直觀的印象,那就是順序執行,畢竟:

var foo = function () {

    console.log('foo1');

}

foo();  // foo1

var foo = function () {

    console.log('foo2');

}

foo(); // foo2

然而去看這段代碼:

function foo() {

    console.log('foo1');

}

foo();  // foo2

function foo() {

    console.log('foo2');

}

foo(); // foo2

打印的結果卻是兩個 foo2。

刷過面試題的都知道這是因為 JavaScript 引擎并非一行一行地分析和執行程序,而是一段一段地分析執行。當執行一段代碼的時候,會進行一個“準備工作”,比如第一個例子中的變量提升,和第二個例子中的函數提升。

但是本文真正想讓大家思考的是:這個“一段一段”中的“段”究竟是怎么劃分的呢?

其實是以函數為段。
每當 JavaScript引擎 執行到一個函數的時候,就會創建一個執行空間(執行上下文)。

接下來問題來了,我們寫的函數多了去了,如何管理創建的那么多執行上下文呢?

所以 JavaScript 引擎 創建了執行上下文棧(Execution context stack,ECS)來管理執行上下文

試想當 JavaScript 開始要解釋執行代碼的時候,最先遇到的就是全局代碼,所以初始化的時候首先就會向執行上下文棧壓入一個全局執行上下文。當執行一個函數的時候,就會創建一個執行上下文,并且壓入執行上下文棧,當函數執行完畢的時候,就會將函數的執行上下文從棧中彈出。

那么 執行上下文 有什么用?

執行上下文

執行代碼計算時,總需要得知 變量 的來源及值,那么怎么獲取該變量?當然是要從代碼執行的上下文查找。

對于每個執行上下文,都有三個重要屬性:

  • 變量對象(Variable object,VO)
  • 作用域鏈(Scope chain)
  • this

JavaScript 引擎會沿著作用域鏈去查找變量。

作用域鏈是如何產生?我們應該先了解 JavaScript 引擎 在創建 執行上下文 的具體處理過程。

作用域鏈

具體分析產生

我們分析第一段代碼:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

執行過程如下:

1.執行全局代碼,創建全局執行上下文,全局上下文被壓入執行上下文棧

    ECStack = [
        globalContext
    ];

2.全局上下文初始化

    globalContext = {
        VO: [global, scope, checkscope],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }

2.初始化的同時,checkscope 函數被創建,保存作用域鏈到函數的內部屬性[[scope]]。
每個函數有一個內部屬性 [[scope]],當函數創建的時候,就會保存所有父變量對象到其中,你可以理解 [[scope]] 就是所有父變量對象的層級鏈,但是注意:[[scope]] 并不代表完整的作用域鏈!

    checkscope.[[scope]] = [
      globalContext.VO
    ];

3.執行 checkscope 函數,創建 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧

ECStack = [
    checkscopeContext,
    globalContext
];

4.checkscope 函數執行上下文初始化:

  1. 復制函數 [[scope]] 屬性創建作用域鏈,
  2. 用 arguments 創建活動對象,
  3. 初始化活動對象,即加入形參、函數聲明、變量聲明,
  4. 將活動對象壓入 checkscope 作用域鏈頂端(執行至此,該函數的作用域鏈才完整)。
  5. 同時 f 函數被創建,保存作用域鏈到 f 函數的內部屬性[[scope]]
    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }

5.執行 f 函數,創建 f 函數執行上下文,f 函數執行上下文被壓入執行上下文棧

    ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];

6.f 函數執行上下文初始化, 以下跟第 4 步相同:

  1. 復制函數 [[scope]] 屬性創建作用域鏈
  2. 用 arguments 創建活動對象
  3. 初始化活動對象,即加入形參、函數聲明、變量聲明
  4. 將活動對象壓入 f 作用域鏈頂端
    fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }

7.f 函數執行,沿著作用域鏈查找 scope 值,返回 scope 值

8.f 函數執行完畢,f 函數上下文從執行上下文棧中彈出

    ECStack = [
        checkscopeContext,
        globalContext
    ];

9.checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出

    ECStack = [
        globalContext
    ];

總結

當查找變量的時候,會先從當前上下文的變量對象中查找,如果沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈。

由上可知,作用域鏈是由 變量對象/活動對象 構成的。

活動變量

變量對象是與執行上下文相關的數據作用域,存儲了在上下文中定義的變量和函數聲明。因為不同執行上下文下的變量對象稍有不同,全局上下文中的變量對象就是全局對象。

在函數上下文中,我們用活動對象(activation object, AO)來表示變量對象。

活動對象和變量對象其實是一個東西,只是變量對象是規范上的或者說是引擎實現上的,不可在 JavaScript 環境中訪問,只有到當進入一個執行上下文中,這個執行上下文的變量對象才會被激活,所以才叫 activation object 吶,而只有被激活的變量對象,也就是活動對象上的各種屬性才能被訪問。

活動對象是在進入函數上下文時刻被創建的,它通過函數的 arguments 屬性初始化。arguments 屬性值是 Arguments 對象。

函數執行產生活動變量過程

執行上下文的代碼會分成兩個階段進行處理:分析和執行,我們也可以叫做:

  • 進入執行上下文
  • 代碼執行

當進入執行上下文時,這時候還沒有執行代碼,

變量對象會包括:

  1. 函數的所有形參 (如果是函數上下文)

    • 由名稱和對應值組成的一個變量對象的屬性被創建
    • 沒有實參,屬性值設為 undefined
  2. 函數聲明

    • 由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建
    • 如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性
  3. 變量聲明

    • 由名稱和對應值(undefined)組成一個變量對象的屬性被創建;
    • 如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性

舉個例子:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);

在進入執行上下文后,這時候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

在代碼執行階段,會順序執行代碼,根據代碼,修改變量對象的值

還是上面的例子,當代碼執行完后,這時候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

活動變量 總結

到這里變量對象的創建過程就介紹完了,讓我們簡潔的總結我們上述所說:

  1. 全局上下文的變量對象初始化是全局對象

  2. 函數上下文的變量對象初始化只包括 Arguments 對象

  3. 在進入執行上下文時會給變量對象添加形參、函數聲明、變量聲明等初始的屬性值

  4. 在代碼執行階段,會再次修改變量對象的屬性值

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容