新手秒懂 - 做用域 & 做用域鏈

前言

根據上篇關於 新手秒懂 - 高逼格解釋變量提高 的文章中說明了,在生成執行上下文的建立階段,生成變量對象後會創建做用域鏈。那咱們接下里就看看做用域和做用域鏈究竟是個啥子玩意。javascript

做用域

做用域是一套規則, 用於肯定在何處以及如何查找變量(標識符)。(說白了就是你寫代碼的那塊旮旯裏,來肯定你以後怎麼查找變量,簡單粗暴。。)前端

詞法做用域 & 動態做用域

  • 詞法做用域: 函數的做用域在函數定義的時候就決定了。(javascript 採用的是靜態做用域)
  • 動態做用域: 函數的做用域是在函數調用的時候才決定的。

簡單的例子表述一下:java

var value = 1;

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

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

bar(); // 1
複製代碼

我將以最簡單的大白話告訴您發生了啥: foo函數執行 -> 查詢value值(沒有) -> 向上查找(var value = 1), so, 打印 1es6

靜態做用域,只看定義時的位置,就像你跟別人作了鄰居,哪天你老婆吵架了跑出去了,不在家裏。你只要去外面找就好了,別人家就算也有老婆,但確定不是你要找的老婆啊對不對??函數

函數做用域 & 塊級做用域

  • 函數做用域: 已聲明函數的形式, 將內部代碼"隱藏"起來。從而造成函數做用域
  • 塊級做用域: 從 ES3 開始,try/catch 結構在 catch 分句中具備塊做用域。在 ES6 中引入了 let/const 關鍵字( var 關鍵字的表親), 用來在任意代碼塊中聲明變量。 if(..) { let a = 2; } 會聲明一個劫持了 if{ .. } 塊的變量,而且將變量添加到這個塊 中。以下例所示:
var foo = true;
if (foo) {
    let bar = foo * 2;
    bar = something( bar ); 
    console.log( bar );
 }
console.log( bar ); // ReferenceError
複製代碼

匿名函數表達式 & IIFE

之前剛入門的時候被人問到一個問題:請問,當即執行函數表達式的做用是什麼??post

白癡的我居然把匿名函數和 IIFE(當即執行函數表達式) 認爲是同一個東西。ui

  • 匿名函數表達式: 顧名思義,就是沒有名字標識的函數表達式(注意: 函數聲明則不能夠省略函數名)
  • IIFE: 最多見的用法其實就是使用了匿名函數表達式並最後加入(),讓它當即執行。
var a = 2;
(function () {
  var a = 3;
  console.log( a ); // 3
  // 匿名函數表達式內及是塊級做用域
})();
console.log( a ); // 2
複製代碼

而它的做用主要包括幾點:spa

  1. 避免命名衝突
  2. 減小內存佔用
  3. 產生塊級做用域,形成做用域隔離。關於爲啥要存在塊級做用域,請參照《ECMAScript 6 入門》

做用域鏈

定義

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

形象一點就是:對象

做用域鏈就像一棟樓,當前做用域在一樓,全局做用域在頂樓,就是一直往上找你要用的變量。

而編譯器的查找方式有兩種:

  • 若是目的是獲取變量的值,就會使用 RHS 查詢, 不成功的RHS引用會致使拋出 ReferenceError 異常。 // console.log(a) --> VM130:1 Uncaught ReferenceError: a is not defined
  • 賦值操做符會致使 LHS 查詢, 不成功的 LHS 引用會致使自動隱式地建立一個全局變量(非嚴格模式下),該變量使用 LHS 引用的目標做爲標識符,或者拋 出 ReferenceError 異常(嚴格模式下)。 // a = 1

深刻理解

由於javascript 是靜態做用域,函數的做用域在函數定義的時候就決定了。

這是由於函數有一個內部屬性 [[scope]],當函數建立的時候,就會保存全部父變量對象到其中,你能夠理解 [[scope]] 就是全部父變量對象的層級鏈,可是注意:[[scope]] 並不表明完整的做用域鏈!(意思就是在函數建立時就能夠拿到父級的變量對象VO)

文字比較難理解不要緊,我們以一個例子說明

function foo() {
    function bar() {
    ...
    }
}
複製代碼

函數建立時, 各自的[[scope]]爲:

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

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
]; // 父級對象的 AO/VO(表示變量對象),俺的上篇文章提到過
複製代碼

而以後函數激活, 變量對象就會添加到做用鏈的前端

做用域鏈 = [AO].concat([[Scope]]);
複製代碼

下面咱們結合示例具體說說實現過程:

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();
複製代碼

執行過程以下:

1.checkscope 函數被建立,保存做用域鏈到 內部屬性 [[scope]]

checkscope.[[scope]] = [
    globalContext.VO // 建立時就能夠獲取父變量對象(靜態做用域)
];
複製代碼

2.執行 checkscope 函數,建立 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧

執行上下文棧:當執行一個函數的時候,就會建立一個執行上下文,而且壓入執行上下文棧,當函數執行完畢的時候,就會將函數的執行上下文從棧中彈出。

ECStack = [ // 執行上下文棧
    checkscopeContext, // checkscope上下文
    globalContext // 全局上下文
];
複製代碼

3.checkscope 函數啓動(不執行函數內的請求)。開始作準備工做,第一步:複製函數[[scope]]屬性建立做用域鏈

checkscopeContext = {
    做用域鏈: checkscope.[[scope]], //上面提到的建立時生成的[[scope]]
}
複製代碼

4.第二步:用 arguments 建立活動對象,隨後初始化活動對象,加入形參、函數聲明、變量聲明(生成變量對象的的幾個過程)

checkscopeContext = {
    VO: { // 變量對象
        arguments: {
            length: 0
        },
        scope2: undefined
    }
}
複製代碼

5.第三步:將活動對象壓入 checkscope 做用域鏈頂端

checkscopeContext = {
    VO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    做用域鏈: [VO, [[Scope]]]
}
複製代碼

6.準備工做作完,開始執行函數,隨着函數的執行,修改 AO (活動變量,變量對象的執行階段)的屬性值

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope' // 函數執行後 scope2 獲取到值
    },
    做用域鏈: [AO, [[Scope]]]
}
複製代碼

7.查找到 scope2 的值,返回後函數執行完畢,函數上下文從執行上下文棧中彈出

ECStack = [
    globalContext
];
複製代碼

結尾

這篇主要分享的是做用域相關知識,感受大體瞭解就差很少了,寫的都是我本身的淺薄理解,有錯誤的地方歡迎指出,對於變量對象不瞭解的小夥伴請參照個人上篇文章 新手秒懂 - 高逼格解釋變量提高,仍是一句話,努力,奮鬥💪💪

參考文獻

《你不知道的JavaScript(上卷)》

JavaScript深刻之做用域鏈

相關文章
相關標籤/搜索