漫談JavaScript中的做用域(scope)

什麼是做用域

程序的執行,離不開做用域,也必須在做用域中才能將代碼正確的執行。javascript

因此做用域究竟是什麼,通俗的說,能夠這樣理解:做用域就是定義變量的位置,是變量和函數的可訪問範圍,控制着變量和函數的可見性和生命週期。java

而JavaScript中的做用域,在ES6以前和ES6以後,有兩種不一樣的狀況。函數

ES6以前,JavaScript做用域有兩種:函數做用域和全局做用域。設計

ES6以後,JavaScript新增了塊級做用域。對象

做用域的特性

在JavaScript變量提高的討論中,咱們實際上是缺乏了一個做用域的概念的,變量提高其實也是針對在同一做用域中的代碼來講的。blog

對編譯器的瞭解,讓咱們明白,對於一段代碼【var a = 10】變量的賦值操做,實際上是包含了兩個過程:繼承

一、變量的聲明和隱式賦值(var a = undefined),這個階段在編譯時生命週期

二、變量的賦值(a = 10),這個階段在運行時ip

先看一下以下代碼:作用域

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';
}

  

由於變量的提高特性,以及無塊級做用域的概念,因此代碼中在同一個做用域中變量和函數的定義,在編譯階段都會提高到頂部。

經過上述代碼,咱們大致上能夠得出做用域的特性:

第1、內部做用域和外部做用域是嵌套關係。外部做用域徹底包含內部做用域。

第2、內部做用域可訪問外部做用域的變量,可是外部做用域不能訪問內部做用域的變量,(鏈式繼承,向上部做用域查找)。

第3、變量提高是在同一個做用域內部出現的。

第4、做用域用於編譯器在編譯代碼時候,肯定變量和函數聲明的位置

塊級做用域

上述代碼,在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中變量查找的方式:沿着詞法環境的棧頂向下查找,找不到的變量去變量環境中查找,這樣就造成了先查找代碼塊中的變量,再查找提高以後的變量環境,這樣就造成了塊級做用域的概念。

上面的代碼造成兩種環境的狀況以下:

1、全局環境的執行上下文

變量環境:函數聲明function doSomething() { ... }

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

2、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=148

相關文章
相關標籤/搜索