深入理解javascript系列(六):作用域與作用域鏈

Panthon發表於2018-06-13

在javascript中,作用域是用來規範變數函式可訪問範圍的一套規則

6.1  作用域

最常見的作用域有兩種:全域性作用域與函式作用域。

6.1.1 全域性作用域

全域性作用域中宣告的變數與函式可以在專案程式碼的任何地方使用。

一般來說,以下3種情況可以擁有全域性作用域

1.  全域性物件下擁有的屬性與方法。(回憶一下,我們在變數物件系列說過的全域性上下文的特殊性)

window.name
window.location
window.top
...複製程式碼

2.  在最外層宣告的變數與方法

我們知道,全域性上下文中的變數物件就是全域性物件window,所以在全域性上下文中宣告的變數與方法其實也就是window的屬性與方法(記著:javascript中宣告的所有變數都儲存在變數物件中),所以他們也擁有全域性作用域。

3.  在非嚴格模式下,函式作用域中那些未定義卻賦值的變數與方法都會自動變成window的屬性與方法。

在實踐中,無論是從避免多人協作帶來的衝突的角度考慮,還是從效能優化的角度考慮,我們都要儘可能少地自定義全域性變數和方法。

6.1.2 函式作用域

函式作用域中宣告的變數與方法,只能被下一層子作用域訪問,而不能被其它不相干的作用域訪問。

function foo() {
    var a = 20;
    var b = 30;
}
foo();

function bar() {
    return a + b;
}
bar();    //因為作用域的限制,bar中無法訪問到變數a和b,因此執行報錯複製程式碼

function foo() {
    var a = 20;
    var b = 30;

    function bar() {        return a + b;
    }
    return bar();

}foo();    //50 bar中的作用域為foo的子作用域,因此能訪問到變數a和b複製程式碼

在ES6以前(現在ES2018已出),ECMAScript沒有塊級作用域,因此使用時需要特別注意,一定是在函式環境中才能生成新的作用域,下面的情況則不會有作用域的限制。

var arr = [1,2,3,4,5];

for(var i = 0; i<arr.length; i++) {
    console.log('i',i);
}
console.log(i);   // i == 5複製程式碼

因為沒有塊級作用域,因此單獨的'{}'並不會產生新的作用域。這個時候i的值會被保留下來,在for迴圈結束後仍然能夠被訪問。因此,在ES6之前我們需要模擬塊級作用域。

6.1.3  模擬塊級作用域

如果沒有塊級作用域則會給我們的開發帶來一些困擾(初學javascript的如寫個選項卡)。例如,上面的for迴圈的例子中,i值在作用域中仍然可以被訪問,那麼這個值就會對作用域中其它同名的變數造成干擾,因此我們需要模擬一個塊級作用域。我們知道一個函式能夠生成一個作用域,因此這個時候,可以利用函式來達到我們的目的。

var arr = [1,2,3,4,5];

(function(){
    for(var i = 0; i<arr.length; i++) {    console.log('i',i);
}})()

console.log(i);   // i is not defined複製程式碼

這種方式叫做函式自執行。

通過這種方式,我們就可以限定變數i值僅僅只在for迴圈中生效,而不會對其它程式碼造成干擾。

自執行函式的寫法有很多,這裡就不做累述。

當我們使用ECMAScript5時,往往通過函式自執行的方式來實現模組化。而模組化是實際開發中需要重點掌握的開發思維(模組化相關話題我會在之後分享自己的筆記)。

6.2  作用域鏈

作用域鏈(Scope Chain)是當前執行環境與上層執行環境的一系列變數物件組成的,它保證了當前執行環境對符合訪問許可權變數和函式的有序訪問。

var a = 20;

function test() {
    var b = a + 10;

    function innerTest() {
        var c = 10;
        return b + c;
    }
    
    return innerTest();
}
test();複製程式碼

請先按照call stack的呼叫方式,在你的大腦中執行本次程式碼。

在上面的例子中,先後建立了全域性函式test和函式innerTest的執行上下文(當test(),表示其執行上下文入棧)。假設它們的變數物件分別為VO(global)、VO(test)、VO(innerTest),那麼innerTest的作用域鏈則同時包含了這三個變數物件。

所以innerTest的執行上下文可表示如下。

innerTestEC = {
    VO: {...},                                        //變數物件
    scopeChain: [VO(innerTest),VO(test),VO(global)],  //作用域鏈
    this: {}
}複製程式碼

可以用一個陣列來表示作用域鏈的有序性。陣列的第一項scopeChain[0]為作用域鏈的最前端,而陣列的最後一項則為作用域鏈的最末端。所有作用域鏈的最末端都是全域性變數物件。

很多人會用父子關係或者包含關係來理解當前作用域與上層作用域之間的關係。但我更喜歡@陽波大神的描述:以當前上下文的變數物件為起點,以全域性變數物件為終點的單方向通道。

深入理解javascript系列(六):作用域與作用域鏈

理解作用域鏈至關重要,但是更多的知識還需要結合閉包來理解。

這些都是我以往的學習筆記。如果您看到此筆記,希望您能指出我的錯誤。有這麼一個群,裡面的小夥伴互相監督,堅持每天輸出自己的學習心得,不輸出就出局。希望您能加入,我們一起終身學習。歡迎新增我的個人微訊號:Pan1005919589



相關文章