程序的執行,離不開做用域,也必須在做用域中才能將代碼正確的執行。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中的塊級做用域究竟是什麼?
利用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'纔會入棧;
執行doSomething的時候,還未執行let name = 'otherName',因此,此時doSomething的詞法環境中並未有name = 'otherName',這個時候查找,只能向外部做用域查找(全局做用域)
此時查找到全局做用域name = 'someName'因此此時就打印了someName
代碼接着執行到了if語句內部,纔會將name = 'otherName'入棧,可是此時由於語句已經執行完畢,因此也就無關痛癢了。
JavaScript也就經過這種方式,實現了塊級別做用域。
JavaScript中的做用域總的來講,分爲塊級做用域、函數做用域、全局做用域。
而每一個做用域都會建立自身的執行上下文,每個執行上下文又分爲了變量環境和詞法環境兩部分。
塊級做用域的實現,實際上是根據定義的let和const聲明的變量放置在詞法環境棧中這一特性來實現。
這一特性被社區的人叫作‘暫時性死區’,可是在JavaScript標準中並未有這個概念。
只有理解了做用域的概念,才能真正明白JavaScript的執行機制,才能減小咱們由於變量定義等發生的錯誤。