前言
眾所周知,JavaScript是單線程語言雳窟。所以JavaScript是按順序執(zhí)行的!
先編譯再執(zhí)行
變量提升
請看下面的例子:
console.log(cat)
catName("Chloe");
var cat = 'Chloe'
function catName(name) {
console.log("我的貓名叫 " + name);
}
按照得出的結論:"JavaScript是按順序執(zhí)行的"來看匣屡,步驟如下:
- 執(zhí)行第一句的時候封救,cat并沒有定義,結果應該是拋出一個錯誤耸采,然后結束執(zhí)行兴泥。
Uncaught ReferenceError: cat is not defined
但實際的執(zhí)行結果并不是這樣:
不僅可以執(zhí)行,catName()執(zhí)行結果也輸出了虾宇。這種現(xiàn)象就是: 變量提升
從概念的字面意義上說搓彻,“變量提升”就是把變量和函數(shù)的聲明移動到代碼的最前面,變量被提升后嘱朽,會給變量設置默認值--undefined旭贬。
調整之后的執(zhí)行順序如下:
- 首先執(zhí)行var cat = undefined和function catName(){}
- 然后執(zhí)行console.log(cat) // undefined
- 接著調用catName()
- 最后給cat賦值cat = 'Chloe'
移動一詞容易造成誤解。實際在物理層面上代碼的位置并沒有改變搪泳。JavaScript是解析執(zhí)行的語言稀轨,在執(zhí)行前會先經(jīng)過編譯階段。造成這種現(xiàn)象的原因是:JavaScript引擎在編譯階段中將變量和函數(shù)的聲明放在了內存中岸军。
執(zhí)行上下文
變量提升(Hoisting)被認為是奋刽, Javascript中執(zhí)行上下文 (特別是創(chuàng)建和執(zhí)行階段)工作方式的一種認識
在編譯階段,JavaScript會為上述代碼創(chuàng)建一個執(zhí)行上下文和可執(zhí)行代碼艰赞。
執(zhí)行上下文是JavaScript執(zhí)行一段代碼時的運行環(huán)境佣谐,包含this、變量方妖、對象以及函數(shù)等狭魂。
- 在編譯階段
- JavaScript引擎會將var變量聲明和函數(shù)聲明等的變量提升內容放在變量環(huán)境中。
- 接下來JavaScript引擎會把聲明以外的代碼編譯為字節(jié)碼--可執(zhí)行代碼党觅。
- 執(zhí)行階段
- 執(zhí)行到console.log(cat)時雌澄,JavaScript引擎在變量環(huán)境中查找cat這個變量,由于變量環(huán)境存在cat變量杯瞻,并且其值為undefined镐牺,所以這時候就輸出undefined。
- 當執(zhí)行到catName函數(shù)時魁莉,引擎在變量環(huán)境中查找該函數(shù)任柜,由于變量環(huán)境中存在該函數(shù)的引用卒废,所以引擎執(zhí)行該函數(shù),并輸出執(zhí)行結果宙地。
- 執(zhí)行cat賦值,引擎在變量環(huán)境查找到cat變量逆皮,并進行賦值宅粥。
創(chuàng)建執(zhí)行上下文的三種情況:
- 全局執(zhí)行上下文:JS引擎在編譯全局代碼時,創(chuàng)建全局執(zhí)行上下文电谣。在當前頁面中秽梅,全局執(zhí)行上下文僅有一個。
- ** 函數(shù)執(zhí)行上下文**:在調用一個函數(shù)時剿牺,JS引擎會創(chuàng)建一個函數(shù)執(zhí)行上下文企垦。一般情況下,當函數(shù)執(zhí)行完畢后就會銷毀此函數(shù)執(zhí)行上下文晒来。
- eval函數(shù)執(zhí)行上下文:執(zhí)行eval函數(shù)時钞诡,也會創(chuàng)建一個執(zhí)行上下文。
調用棧
JS引擎通過棧的數(shù)據(jù)結構來管理多個執(zhí)行上下文湃崩。
棧是計算機科學中的一種抽象數(shù)據(jù)類型荧降,只允許在有序的線性數(shù)據(jù)集合的一端(稱為堆棧頂端,英語:top)進行加入數(shù)據(jù)(英語:push)和移除數(shù)據(jù)(英語:pop)的運算攒读。因而按照后進先出(LIFO, Last In First Out)的原理運作
在一個執(zhí)行上下文創(chuàng)建好后朵诫,JS引擎就會它壓進棧中。管理執(zhí)行上下文的棧結構就稱為調用棧薄扁,或者執(zhí)行上下文棧剪返。
請看下面例子:
function foo() {
var a = 0
console.log(a)
}
function bar() {
var b = 1
foo()
console.log(b)
}
bar()
步驟如下:
創(chuàng)建全局執(zhí)行上下文,并將其壓入棧底邓梅。
-
執(zhí)行全局代碼:bar()脱盲。調用bar函數(shù)時,JS引擎會編譯bar函數(shù)震放,并為其創(chuàng)建一個函數(shù)執(zhí)行上下文宾毒。最后將其執(zhí)行上下文壓入棧中,并且將變量b賦予默認值undefined殿遂。
-
執(zhí)行bar函數(shù)內部的代碼诈铛。先執(zhí)行b = 1的賦值操作此叠,然后調用foo函數(shù)垦藏。JS引擎編譯foo函數(shù),并為其創(chuàng)建一個函數(shù)執(zhí)行上下文伊佃。最后將其執(zhí)行上下文壓入棧中恩静,并且將變量a賦予默認值undefined焕毫。
執(zhí)行foo內部的代碼蹲坷。執(zhí)行a = 1賦值操作,然后輸出a的值邑飒。foo函數(shù)執(zhí)行完畢后循签,調用棧就將其執(zhí)行上下文從棧頂彈出。接著執(zhí)行bar函數(shù)疙咸。
執(zhí)行完bar函數(shù)后县匠,調用棧就將其執(zhí)行上下文從棧頂彈出。剩下全局執(zhí)行上下文
整個JavaScript流程執(zhí)行就到此結束了撒轮。
調用棧是JS引擎追蹤函數(shù)執(zhí)行的一個機制乞旦,當一次有多個函數(shù)被調用時,通過調用棧就能夠追蹤到哪個函數(shù)正在被執(zhí)行以及各函數(shù)之間的調用關系题山。
var缺陷與塊級作用域
變量提升帶來的問題
- 變量被覆蓋
var cat = "foo"
function catName(){
console.log(cat);
if(false){
var cat = "bar"
}
console.log(cat);
}
catName()
調用catName時兰粉,調用棧如下圖所示:
- 創(chuàng)建catName執(zhí)行上下文時,JavaScript引擎會將var變量聲明cat提升內容放在變量環(huán)境中顶瞳,賦予默認值undefined玖姑。
- 執(zhí)行到catName內部的console.log(cat)時,在catName執(zhí)行上下文中的變量環(huán)境找到了cat的值浊仆,輸出undefined客峭。
- if判斷為false,不執(zhí)行抡柿。
- 執(zhí)行console.log(cat)舔琅,參照第二步,輸出undefined洲劣。
- 變量沒被銷毀
function foo () {
for (var i=0; i<10; i++){}
console.log(i)
}
foo()
直觀的來說备蚓,會以為for循環(huán)結束后,i會被銷毀囱稽。結果并非如此郊尝,console.log(i)輸出10。
原因也是變量提升战惊,在創(chuàng)建foo執(zhí)行上下文時流昏,i被提升了。所以for循環(huán)結束后吞获,i并沒有被銷毀况凉。
塊級作用域
存儲變量中的值以及對這個值進行訪問或修改,是編程語言的基本功能各拷。而 作用域 則是如何存儲變量以及如何訪問這些變量的規(guī)則刁绒。
在ES6前,JavaScript只支持兩種方法創(chuàng)建作用域:
- 全局作用域
- 函數(shù)作用域
而其他編程語言則都普遍支持塊級作用域烤黍。
塊級作用域 就是使用一對大括號包裹的一段代碼知市,比如函數(shù)傻盟、判斷語句、循環(huán)語句嫂丙,甚至單獨的一個{}都可以被看作是一個塊級作用域娘赴。
簡單來講,在塊級作用域內部定義的變量在其塊級作用域外部是訪問不到的奢入,并且等該內部代碼執(zhí)行完成之后筝闹,其定義的變量會被銷毀。
由于JavaScript不支持塊級作用域腥光,所以才會有變量提升帶來的問題。
幸好糊秆,ES6改變了現(xiàn)狀武福,引入了新的let和const關鍵字,提供了除var以外的另一種變量聲明方式痘番。
let和const關鍵字可以將變量綁定到所在的任意作用域中(通常是{}內部)捉片。換句話說,let為其聲明的變量創(chuàng)建了塊作用域汞舱。
塊級作用域的作用伍纫,請看下面例子:
var cat = "foo"
function catName(){
if(true){
var cat = "bar"
console.log(cat);
}
console.log(cat);
}
catName()
在這段代碼中,有兩處聲明了cat變量昂芜,一處在全局作用域莹规,一處在catName函數(shù)作用域中的if語句里面。
在執(zhí)行if語句內部時泌神,調用棧如下圖所示:從圖中可看出兩處console.log(cat)都輸出bar良漱。
使用let改寫上面代碼
var cat = "foo"
function catName(){
if(true){
let cat = "bar"
console.log(cat);
}
console.log(cat);
}
catName()
if語句執(zhí)行結束后,let聲明的cat變量就會被銷毀欢际,第二處的console.log(cat)就會輸出foo JavaScript內部實現(xiàn)塊級作用域
請看下面的例子
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
步驟如下:
- 第一步創(chuàng)建全局執(zhí)行上下文
-
執(zhí)行foo()母市,創(chuàng)建foo函數(shù)的執(zhí)行上下文
- 在函數(shù)內部使用var聲明的變量都放在變量環(huán)境中,并賦予一個默認值undefined损趋。
- 在函數(shù)內部使用let聲明的變量被放在詞法環(huán)境中患久,沒有賦予一個默認值。
- 在函數(shù)內部中的{}內部使用let聲明的變量沒有放在詞法環(huán)境中浑槽。
-
執(zhí)行foo函數(shù)內部的{}塊蒋失,此時a和b的已經(jīng)初始化了,并且進入作用域塊時括荡,作用域塊中通過let聲明的變量高镐,會被存放在詞法環(huán)境的一個單獨的區(qū)域中,這個區(qū)域中的變量并不影響作用域塊外面的變量畸冲。
在詞法環(huán)境內部維護了一個棧結構嫉髓,棧底是函數(shù)最外層的變量观腊,進入一個作用域塊后,就會把該作用域塊內部的變量壓入棧中算行;當作用域執(zhí)行完成之后梧油,該作用域的let和const聲明的變量就會從棧頂彈出。
-
作用域塊執(zhí)行結束后州邢,詞法環(huán)境的棧結構就把其信息從棧頂彈出儡陨。
使用let或const聲明的變量,在達到聲明處之前都是無法訪問的量淌,試圖訪問會導致一個 引用錯誤骗村,即使在通常是安全的操作時(例如使用typeof運算符)也是如此。示例如 下:
if (true) {
console.log(typeof value); // 引用錯誤
let value = 'blue'
}
因為value位于暫時性死區(qū)(temporal dead zone, TDZ)的區(qū)域內--該名稱并沒有在ECMAScript規(guī)范中被明確命名呀枢,但經(jīng)常被用于描述let或const聲明的變量為何在聲明之前無法被訪問胚股。
總結
- JavaScript代碼是先編譯再執(zhí)行的。
- 執(zhí)行是按順序一段一段執(zhí)行的裙秋,一段代碼是指一個執(zhí)行上下文琅拌。
- 執(zhí)行上下文有三種情況:
- 全局執(zhí)行上下文
- 函數(shù)執(zhí)行上下文
- eval執(zhí)行上下文
- let和const支持塊級作用域
作者:zhangwinwin
鏈接:JavaScript代碼是怎么執(zhí)行的?
來源:github