1. 程式人生 > >漫談JavaScript中的作用域(scope)

漫談JavaScript中的作用域(scope)

什麼是作用域

程式的執行,離不開作用域,也必須在作用域中才能將程式碼正確的執行。

所以作用域到底是什麼,通俗的說,可以這樣理解:作用域就是定義變數的位置,是變數和函式的可訪問範圍,控制著變數和函式的可見性和生命週期。

而JavaScript中的作用域,在ES6之前和ES6之後,有兩種不同的情況。

ES6之前,JavaScript作用域有兩種:函式作用域和全域性作用域。

ES6之後,JavaScript新增了塊級作用域。

作用域的特性

在JavaScript變數提升的討論中,我們其實是缺少了一個作用域的概念的,變數提升其實也是針對在同一作用域中的程式碼來說的。

對編譯器的瞭解,讓我們明白,對於一段程式碼【var a = 10】變數的賦值操作,其實是包含了兩個過程:

1、變數的宣告和隱式賦值(var a = undefined),這個階段在編譯時

2、變數的賦值(a = 10),這個階段在執行時

先看一下如下程式碼:

var flag = true;

if(flag) {
    var someStr = 'flag is true';
}

function doSomething() {
    var someStr = 'in doSomething';
    var otherStr = 'some other string';
    console.log(someStr);
    console.log(flag);
}

doSomething();

for(var i = 0; i < 10; i++) {
    console.log(i);
}

console.log(i);

{
    var place = 'i do not want to be visited';
}

  

那麼這一些程式碼在編譯之後,執行之前,根據變數提升的機制,我們可以知道應該是下面這個樣子:

function doSomething() { // 函式優先提升
    // 提升隱式賦值
    var someStr = undefined; 
    var otherStr = undefined; 

    someStr = 'in doSomething';
    otherStr = 'some other string';

    console.log(someStr);
    console.log(flag);
}

// 隱式賦值和提升
var flag = undefined; 
var someStr = undefined;
var i = undefined;
var place = undefined;

flag = true;

if(flag) {
    someStr = 'flag is true';
}

for(i = 0; i < 10; i++) {
    console.log(i);
}

doSomething();

console.log(i);

{
    place = 'i do not want to be visited';
}

  

因為變數的提升特性,以及無塊級作用域的概念,所以程式碼中在同一個作用域中變數和函式的定義,在編譯階段都會提升到頂部。

通過上述程式碼,我們大體上可以得出作用域的特性:

第一、內部作用域和外部作用域是巢狀關係。外部作用域完全包含內部作用域。

第二、內部作用域可訪問外部作用域的變數,但是外部作用域不能訪問內部作用域的變數,(鏈式繼承,向上部作用域查詢)。

第三、變數提升是在同一個作用域內部出現的。

第四、作用域用於編譯器在編譯程式碼時候,確定變數和函式宣告的位置。

塊級作用域

上述程式碼,在ES6+的環境中執行,也是和ES6之前是相同的結果,但是ES6不是引用了塊級作用域嗎,為什麼大括號塊內的程式碼還是會出現和之前一樣的編譯方式呢?

那麼,ES6中的塊級作用域到底是什麼?

let & const

利用var定義的變數,具有提升的性質,可能會影響程式碼的執行結果。

這是var定義變數的缺陷,那麼如何規避這種缺陷呢?在ES6中,設計出來了let和const來重新定變數。

但是,由於JavaScript標準定義的非常早,1995年5月JavaScript方案定義,1996年微軟提供了JavaScript解決方案JScript。而網景公司為了同微軟競爭,神情了JavaScript標準,於是,1997年6月第一個國際標準ECMA-262便頒佈了。

C語言標準化的過程卻是將近二十年後才頒佈。

所以,我們以後設計的語言既要相容var也要有自己的塊級作用域,讓var和let以及const在引擎做到相容。

所以,我們定義塊級作用域的標準,只能從定義變數的方式入手,而不是直接一個{}塊就可以解決。

先讓我們看一下下面程式碼:

var name = 'someName';

function doSomething(){
    console.log(name);
    if(true) {
        var name = 'otherName';
    }
}

doSomething();

結果:undefined

  

產生這個結果的原因是我們函式內部的變數提升,覆蓋了外部作用域的變數,也就是說,其實打印出來的值是doSomething函式中的變數宣告的值。

但是這樣卻並不符合塊級作用域的預期,如果有許多類似程式碼,理解起來也會相當困難。如果將程式碼用ES6方式改寫:

let name = 'someName';

function doSomething(){
    console.log(name);
    if(true) {
        let name = 'otherName';
    }
}

doSomething();

結果:'someName'

  

從執行結果看,我們真正的做到了塊級作用域應該有的效果,那麼let和const又是如何支援塊作用域的呢?

執行上下文

先想想一下JavaScript中的一個作用域兩個執行上下文中的編譯過程中的環境:

變數環境:編譯階段var宣告存放的位置(一個大物件)。

詞法環境:我們程式碼書寫的位置,也是let和const的初始化位置(程式碼按詞法環境順序執行,按照{}劃分的棧結構)。

而在編譯階段,我們將var定義的變數全都在編譯過程在變數環境初始化為undefined,但是用let和const定義的變數,其實他們並未在變數環境初始化,而是在詞法環境初始化,也就是執行程式碼位置初始化。

詞法環境的特點:按照{}劃分的一個棧結構。

變數查詢方式

JavaScript中變數查詢的方式:沿著詞法環境的棧頂向下查詢,找不到的變數去變數環境中查詢,這樣就形成了先查詢程式碼塊中的變數,再查詢提升之後的變數環境,這樣就形成了塊級作用域的概念。

上面的程式碼形成兩種環境的情況如下:

一、全域性環境的執行上下文

變數環境:函式宣告function doSomething() { ... }

詞法環境棧:執行到let name = 'someName';讓語句name = 'someName'入棧。

二、doSomething的執行上下文(被全域性環境包裹)

變數環境:無

詞法環境棧情況:執行到let name = 'otherName',語句的時候,name = 'other'才會入棧;

JavaScript程式碼執行方式

執行doSomething的時候,還未執行let name = 'otherName',所以,此時doSomething的詞法環境中並未有name = 'otherName',這個時候查詢,只能向外部作用域查詢(全域性作用域)

此時查詢到全域性作用域name = 'someName'所以此時就列印了someName

程式碼接著執行到了if語句內部,才會將name  = 'otherName'入棧,但是此時因為語句已經執行完畢,所以也就無關痛癢了。

JavaScript也就通過這種方式,實現了塊級別作用域。

總結

JavaScript中的作用域總的來說,分為塊級作用域、函式作用域、全域性作用域。

而每個作用域都會建立自身的執行上下文,每一個執行上下文又分為了變數環境和詞法環境兩部分。

塊級作用域的實現,其實是根據定義的let和const宣告的變數放置在詞法環境棧中這一特性來實現。

這一特性被社群的人叫做‘暫時性死區’,但是在JavaScript標準中並未有這個概念。

只有理解了作用域的概念,才能真正明白JavaScript的執行機制,才能減少我們因為變數定義等發生的錯誤。

我的部落格:http://www.gaoyunjiao.fun/?p