前端的小伙伴大概都知道,js
中的var
變量存在變量提升阵幸,在es6
以后隨著let
變量的出現(xiàn)辕翰,變量提升問(wèn)題得以解決缴挖。那么變量提升的原理是什么洪唐?es6
又是怎么解決變量提升問(wèn)題的蒿柳?下面我們來(lái)共同探尋答案:
我們首先來(lái)了解幾個(gè)概念客扎,執(zhí)行上下文饺律、變量環(huán)境胧瓜、詞法環(huán)境矢棚。(本文不涉及閉包、this
指向等問(wèn)題)
執(zhí)行上下文
當(dāng)一段js
代碼被執(zhí)行時(shí)府喳,js
引擎會(huì)先對(duì)其進(jìn)行編譯蒲肋,并創(chuàng)建執(zhí)行上下文。執(zhí)行上下文分為三種:全局執(zhí)行上下文钝满、函數(shù)執(zhí)行上下文兜粘、eval
執(zhí)行上下文
全局執(zhí)行上下文
在js
執(zhí)行全局代碼時(shí),js
引擎會(huì)創(chuàng)建一個(gè)全局的執(zhí)行上下文弯蚜,全局執(zhí)行上下文在頁(yè)面的生命周期內(nèi)只有一份孔轴。即每個(gè)js
文件,只有一個(gè)全局上下文碎捺。函數(shù)執(zhí)行上下文
當(dāng)執(zhí)行一個(gè)js
函數(shù)時(shí)路鹰,js
引擎會(huì)創(chuàng)建一個(gè)函數(shù)執(zhí)行上下文,當(dāng)函數(shù)執(zhí)行結(jié)束之后收厨,函數(shù)的執(zhí)行上下文會(huì)被銷(xiāo)毀晋柱。一個(gè)函數(shù)被多次調(diào)用,會(huì)創(chuàng)建多個(gè)執(zhí)行上下文诵叁。eval
執(zhí)行上下文
使用eval
函數(shù)執(zhí)行一段js
代碼時(shí)雁竞,會(huì)創(chuàng)建一個(gè)eval
的執(zhí)行上下文。
當(dāng)js
文件執(zhí)行時(shí)黎休,首先會(huì)創(chuàng)建全局執(zhí)行上下文浓领,并壓入調(diào)用棧玉凯,當(dāng)調(diào)用js
函數(shù)時(shí),會(huì)創(chuàng)建函數(shù)執(zhí)行上下文联贩,并壓入調(diào)用棧漫仆。當(dāng)函數(shù)執(zhí)行完之后,函數(shù)執(zhí)行上下文便會(huì)從棧中移出泪幌。如以下代碼的執(zhí)行:
var a = "123"
function func1() {
var b = "123"
console.log(b)
func2()
}
funcgion func2() {
const c = "456"
console.log(c)
}
func1()
執(zhí)行上下文中其實(shí)還包含了另外兩個(gè)對(duì)象盲厌,一個(gè)變量環(huán)境對(duì)象和一個(gè)詞法環(huán)境對(duì)象。那么接下來(lái)我們來(lái)看一下什么是變量環(huán)境和詞法環(huán)境
變量環(huán)境
變量環(huán)境存在于執(zhí)行上下文中祸泪,其本質(zhì)是一個(gè)對(duì)象吗浩,變量環(huán)境中存儲(chǔ)的是此作用域內(nèi)定義的變量、函數(shù)信息等信息没隘。如全局執(zhí)行上下文中的變量環(huán)境存儲(chǔ)的是全局的變量和函數(shù)信息懂扼。函數(shù)執(zhí)行上下文中的變量環(huán)境則存放的是函數(shù)的參數(shù)、局部變量等信息右蒲。
其實(shí)阀湿,js
的代碼在執(zhí)行前還有一個(gè)編譯的過(guò)程,在編譯過(guò)程中瑰妄,var
變量和function
函數(shù)部分會(huì)被js
引擎放入到變量環(huán)境中陷嘴,并且變量會(huì)被默認(rèn)設(shè)置為undefined
。在執(zhí)行階段间坐,js
引擎會(huì)在變量環(huán)境中查找聲明的變量和函數(shù)灾挨。這就是我們所說(shuō)的“變量提升”,這也是為什么函數(shù)可以在函數(shù)的實(shí)現(xiàn)之前調(diào)用竹宋。
例:
console.log(a)
var a = "123"
function func1() {
console.log(a)
}
func1()
以上代碼的執(zhí)行順序是:
js引擎先進(jìn)行編譯劳澄,并把
a
變量和func1
放入到變量環(huán)境中,并把a
變量設(shè)置為undefined
進(jìn)入執(zhí)行階段蜈七,執(zhí)行第一行代碼
console.log(a)
浴骂,此時(shí)從變量環(huán)境中取出a
的值為undefined
,所以打印結(jié)果為undefined
宪潮。執(zhí)行第二行代碼
var a = "123"
溯警,將變量環(huán)境中的a
變量賦值為字符串123
。執(zhí)行最后一行代碼
func1()
狡相,js引擎從變量環(huán)境中找出對(duì)應(yīng)的func1
梯轻,并執(zhí)行里面的代碼console.log(a)
,打印結(jié)果為123
所以以上代碼輸出結(jié)果為
undefined
123
雖然在a
聲明之前打印a
變量尽棕,但是卻并沒(méi)有報(bào)錯(cuò)喳挑。
詞法環(huán)境
在ES6
之前,js
中只支持全局作用域和函數(shù)作用域,并不支持塊級(jí)作用域伊诵。ES6
之后单绑,js
引入了let
和const
關(guān)鍵字,從而解決了變量提升問(wèn)題并使js支持了塊級(jí)作用域曹宴。
其實(shí)說(shuō)let
和const
沒(méi)有變量提升并不準(zhǔn)確搂橙,當(dāng)js
代碼被編譯時(shí),let
和const
變量代碼會(huì)被存放在詞法環(huán)境中笛坦。此時(shí)let
和const
變量已經(jīng)被提升了区转,但是只是創(chuàng)建被提升,初始化和賦值并沒(méi)有被提升版扩,如果在賦值之前去讀寫(xiě)該變量废离,便會(huì)報(bào)錯(cuò),這就是我們所說(shuō)的“暫時(shí)性死區(qū)”礁芦。
那實(shí)現(xiàn)塊級(jí)作用域的原理是什么呢蜻韭?其實(shí)在詞法環(huán)境中,維護(hù)了一個(gè)作用域棧柿扣,棧底是函數(shù)的最外層變量(let
和const
聲明的變量)湘捎,進(jìn)入一個(gè)作用域塊后,就會(huì)把該作用域中的變量入棧窄刘;當(dāng)作用域中的代碼執(zhí)行完成之后,該作用域的信息就會(huì)從棧頂彈出舷胜。
我們舉個(gè)以下例子來(lái)說(shuō)明
例:
function fun()
{
let a = 1
{
let a = 2
let b = 3
console.log(a)
console.log(b)
}
console.log(a)
console.log(b)
}
fun()
如圖:
- 當(dāng)
fun
函數(shù)被編譯時(shí)娩践,外層的a
變量首先被創(chuàng)建,并存放至詞法環(huán)境作用域棧中烹骨,此時(shí)函數(shù)內(nèi)部的塊級(jí)作用域中的變量不會(huì)被創(chuàng)建翻伺。 - 當(dāng)函數(shù)執(zhí)行至作用域塊時(shí),
let a
和let b
也被創(chuàng)建并入棧存放至棧頂沮焕。并將a
賦值為2
吨岭,將賦值為3
。 - 當(dāng)執(zhí)行至
console.log(a)
和console.log(b)
時(shí)峦树,js引擎首先從棧頂找到a
和b
的值并打印出2
和3
辣辫。 - 當(dāng)作用域塊執(zhí)行完成之后,作用域塊中的變量信息從棧中彈出魁巩。
- 接著執(zhí)行
console.log(a)
找到的是棧底的a
變量急灭,并打印出1
。接著執(zhí)行console.log(b)
谷遂,由于在詞法環(huán)境和變量環(huán)境中都找不到b
變量葬馋,所以便會(huì)報(bào)錯(cuò)b is not defined
。
如果同一個(gè)函數(shù)中不同作用域存在相同的變量(如上面例子的a),那么變量的查找順序是怎樣的呢畴嘶?
- 首先在詞法環(huán)境作用域棧的棧頂?shù)淖兞啃畔⒅虚_(kāi)始查找
- 如果找到該變量蛋逾,則直接返回該變量在此作用域塊中的值,如果沒(méi)有找到則從棧頂往下依次查找窗悯。
- 如果從詞法環(huán)境中的棧頂?shù)綏5锥紱](méi)有找到区匣,則從變量環(huán)境中查找。
總結(jié):
講到這里蟀瞧,我想應(yīng)該可以回答一下文章開(kāi)始所提的兩個(gè)問(wèn)題:
變量提升的原理是什么沉颂?
在js代碼編譯階段,var
變量和function
函數(shù)會(huì)被js引擎放入到變量環(huán)境中悦污,并且var
變量會(huì)被默認(rèn)設(shè)置為undefined
铸屉。需要注意的是,var
變量只有創(chuàng)建和初始化被提升切端,賦值并沒(méi)有被提升彻坛;而function
的創(chuàng)建、初始化和賦值均會(huì)被提升踏枣。所以在變量的聲明之前昌屉,該變量的值是undefined
,而函數(shù)則可以在聲明之前正常調(diào)用茵瀑。
let
间驮、const
是怎么解決變量提升問(wèn)題的?
在js代碼編譯階段,let
和const
變量會(huì)被js引擎放入到詞法環(huán)境中马昨。與var
一樣竞帽,let
和const
變量也被提升了。但只是創(chuàng)建被提升鸿捧,變量的初始化和賦值并未被提升屹篓,如果在賦值之前讀寫(xiě)該變量,就會(huì)形成暫時(shí)性死區(qū)匙奴。