JavaScript代碼是怎么執(zhí)行的?

前言

眾所周知,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ù)等狭魂。

  1. 在編譯階段
  • JavaScript引擎會將var變量聲明和函數(shù)聲明等的變量提升內容放在變量環(huán)境中。
  • 接下來JavaScript引擎會把聲明以外的代碼編譯為字節(jié)碼--可執(zhí)行代碼党觅。
  1. 執(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í)行上下文的三種情況:

  1. 全局執(zhí)行上下文:JS引擎在編譯全局代碼時,創(chuàng)建全局執(zhí)行上下文电谣。在當前頁面中秽梅,全局執(zhí)行上下文僅有一個。
  2. ** 函數(shù)執(zhí)行上下文**:在調用一個函數(shù)時剿牺,JS引擎會創(chuàng)建一個函數(shù)執(zhí)行上下文企垦。一般情況下,當函數(shù)執(zhí)行完畢后就會銷毀此函數(shù)執(zhí)行上下文晒来。
  3. 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()

步驟如下:

  1. 創(chuàng)建全局執(zhí)行上下文,并將其壓入棧底邓梅。

  2. 執(zhí)行全局代碼:bar()脱盲。調用bar函數(shù)時,JS引擎會編譯bar函數(shù)震放,并為其創(chuàng)建一個函數(shù)執(zhí)行上下文宾毒。最后將其執(zhí)行上下文壓入棧中,并且將變量b賦予默認值undefined殿遂。
    在這里插入圖片描述
  3. 執(zhí)行bar函數(shù)內部的代碼诈铛。先執(zhí)行b = 1的賦值操作此叠,然后調用foo函數(shù)垦藏。JS引擎編譯foo函數(shù),并為其創(chuàng)建一個函數(shù)執(zhí)行上下文伊佃。最后將其執(zhí)行上下文壓入棧中恩静,并且將變量a賦予默認值undefined焕毫。
    在這里插入圖片描述
  4. 執(zhí)行foo內部的代碼蹲坷。執(zhí)行a = 1賦值操作,然后輸出a的值邑飒。foo函數(shù)執(zhí)行完畢后循签,調用棧就將其執(zhí)行上下文從棧頂彈出。接著執(zhí)行bar函數(shù)疙咸。

  5. 執(zhí)行完bar函數(shù)后县匠,調用棧就將其執(zhí)行上下文從棧頂彈出。剩下全局執(zhí)行上下文

整個JavaScript流程執(zhí)行就到此結束了撒轮。

調用棧是JS引擎追蹤函數(shù)執(zhí)行的一個機制乞旦,當一次有多個函數(shù)被調用時,通過調用棧就能夠追蹤到哪個函數(shù)正在被執(zhí)行以及各函數(shù)之間的調用關系题山。

var缺陷與塊級作用域

變量提升帶來的問題

  1. 變量被覆蓋
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洲劣。
  1. 變量沒被銷毀
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()

步驟如下:

  1. 第一步創(chuàng)建全局執(zhí)行上下文
  2. 執(zhí)行foo()母市,創(chuàng)建foo函數(shù)的執(zhí)行上下文
    在這里插入圖片描述
  • 在函數(shù)內部使用var聲明的變量都放在變量環(huán)境中,并賦予一個默認值undefined损趋。
  • 在函數(shù)內部使用let聲明的變量被放在詞法環(huán)境中患久,沒有賦予一個默認值。
  • 在函數(shù)內部中的{}內部使用let聲明的變量沒有放在詞法環(huán)境中浑槽。
  1. 執(zhí)行foo函數(shù)內部的{}塊蒋失,此時a和b的已經(jīng)初始化了,并且進入作用域塊時括荡,作用域塊中通過let聲明的變量高镐,會被存放在詞法環(huán)境的一個單獨的區(qū)域中,這個區(qū)域中的變量并不影響作用域塊外面的變量畸冲。
    在這里插入圖片描述

在詞法環(huán)境內部維護了一個棧結構嫉髓,棧底是函數(shù)最外層的變量观腊,進入一個作用域塊后,就會把該作用域塊內部的變量壓入棧中算行;當作用域執(zhí)行完成之后梧油,該作用域的let和const聲明的變量就會從棧頂彈出。

  1. 作用域塊執(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聲明的變量為何在聲明之前無法被訪問胚股。

總結

  1. JavaScript代碼是先編譯再執(zhí)行的。
  2. 執(zhí)行是按順序一段一段執(zhí)行的裙秋,一段代碼是指一個執(zhí)行上下文琅拌。
  3. 執(zhí)行上下文有三種情況:
  • 全局執(zhí)行上下文
  • 函數(shù)執(zhí)行上下文
  • eval執(zhí)行上下文
  1. let和const支持塊級作用域

作者:zhangwinwin
鏈接:JavaScript代碼是怎么執(zhí)行的?
來源:github

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末摘刑,一起剝皮案震驚了整個濱河市进宝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌枷恕,老刑警劉巖党晋,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異活尊,居然都是意外死亡隶校,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門蛹锰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來深胳,“玉大人,你說我怎么就攤上這事铜犬∥柚眨” “怎么了?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵癣猾,是天一觀的道長敛劝。 經(jīng)常有香客問我,道長纷宇,這世上最難降的妖魔是什么夸盟? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮像捶,結果婚禮上上陕,老公的妹妹穿的比我還像新娘桩砰。我一直安慰自己,他們只是感情好释簿,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布亚隅。 她就那樣靜靜地躺著,像睡著了一般庶溶。 火紅的嫁衣襯著肌膚如雪煮纵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天偏螺,我揣著相機與錄音行疏,去河邊找鬼。 笑死套像,一個胖子當著我的面吹牛隘擎,可吹牛的內容都是我干的。 我是一名探鬼主播凉夯,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼采幌!你這毒婦竟也來了劲够?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤休傍,失蹤者是張志新(化名)和其女友劉穎征绎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磨取,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡人柿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了忙厌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凫岖。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖逢净,靈堂內的尸體忽然破棺而出哥放,到底是詐尸還是另有隱情,我是刑警寧澤爹土,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布甥雕,位于F島的核電站,受9級特大地震影響胀茵,放射性物質發(fā)生泄漏社露。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一琼娘、第九天 我趴在偏房一處隱蔽的房頂上張望峭弟。 院中可真熱鬧附鸽,春花似錦、人聲如沸孟害。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挨务。三九已至击你,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谎柄,已是汗流浹背丁侄。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留朝巫,地道東北人鸿摇。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像劈猿,于是被迫代替她去往敵國和親拙吉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354