iT邦幫忙

2022 iThome 鐵人賽

DAY 12
0
Modern Web

就是要搞懂 JavaScript 啦!系列 第 12

Day12 提升:到底是在提什麼升什麼啦!

  • 分享至 

  • xImage
  •  

變數的提升

我們已經知道,變數依賴著作用域存活,也就是遵循著一系列規則被存取。

而在變數的宣告和賦值上,有一個微妙的細節。

先前已經提過,進行變數查詢時如果找不到識別字的參考,就會回報 ReferenceError

console.log(a);
// ReferenceError: a is not defined

加上宣告與賦值後才能正常取得

let a;
a = 1;
console.log(a); // 1

甚至不賦值的話,JS 也會給予預設值 undefined

let a;
console.log(a); // undefined

但如果在宣告之前存取:

console.log(a);
// ReferenceError: Cannot access 'a' before initialization
let a;

可以看到同樣出現了 ReferenceError,但奇怪,這裡不是寫 a is not defined,而是出現 Cannot access 'a' before initialization

照理來說,在宣告之前使用,不是應該跟 a is not defined 一樣,找不到識別字對應的參考嗎?

為什麼顯示的錯誤卻表示 a 尚未初始化?這兩者到底有什麼不同?

解開這個答案之前,來看看更神奇的事情:

console.log(a); // undefined
var a;

在上面的程式碼中,程式打印了一個在此之前未宣告過的 a 變數,隨後才在下一行宣告它,結果程式......毫無錯誤地印出了 undefined !?

要解答這兩個謎題,就需要來探討,JS 實際上都做了些什麼。


解開祕密的鑰匙

所以說,為什麼以下的結果會是 undefined

console.log(a); // undefined
var a;

問題的解答,藏在 JS 的編譯時期,在程式執行之前就已經發生了某些事,因此等執行到 console.log(a) 的時候,才會出現上面那樣的情況。

同一段的程式碼,對人類來說會直覺式地從上往下讀,並對其賦予「時間」的概念,上面的程式碼先執行,下面的程式碼後執行。

這個概念對電腦基本上來說也是一樣的,只是每個階段處理的內容不同,所以最後的結果有時會在人腦的預期之外。

先前已經提過,JS 包含編譯這個過程,在這個時期,JS 編譯器就已經完成全部的「宣告」作業,並將識別字存入記憶體當中,同時處理好所有作用域存取規則。

因此在執行 console.log(a); 之前,var a; 早在編譯時期便處理完畢,並給予預設值 undefined ,因此來到 console.log(a); 時,就能毫無障礙地進行 RHS,然後順利把值傳給 console.log 執行。

以上整個過程所產生的結果,就像把 var a; 這行程式碼搬移到最前面一樣,因此這樣的現象被稱為「提升(Hoisting)」。

// var a; 早在編譯期處理完畢,像是被放到了最上面一行
console.log(a); // undefined
var a; // 被提升到第一行了

與此同時,由於變數的存取遵守作用域規則,因此所有宣告「看起來」都被提升到了所屬作用域的最前面,畢竟再上去,也就是更外層的作用域並無法存取這個變數。

// 這裡無法取得 a
function foo(){
  // var a; 被提升到此處,是 a 最早能夠被取得的地方
  console.log(a) // undefined
  var a;
}
foo();

但賦值的部分則是在執行時期處理,並沒有提升的行為:

// var a 宣告被提升到最頂端
function foo() {
  // var b 被提升到 foo 作用域的最頂端
  console.log(b); // undefined
  b = "B"; // 於執行時期才處理,留在原地
  var b; // 於編譯時期就處理完畢,因此已經被提升到上面了
  console.log(b); // B
  
  // foo 執行的時候,a 完成宣告但尚未賦值
  console.log("a in foo:", a); // a in foo: undefined
}

foo();
var a = "A"; // 直到這裡才賦值給 a
console.log("a in global:", a); // a in global: A

總結來說,由於 JS 會在編譯時期處理所有宣告,結果就是程式內部的宣告「看上去」被往前移到了所屬作用域的最頂端,這就是所謂的「提升(Hoisting)」。


JS 的宣告補丁

理解提升的原理後,回頭來看看 JS 在 ES6 新增的宣告補丁,也就是 letconst 這兩個關鍵字。

前面已經說過,letconst 兩者唯一的差別,在於 const 僅能在宣告時被賦值,而 let 則能夠重複賦值無數次。

並且 letconst 能夠創造區塊作用域(block scope),但 var 不能。除此之外,它們和 var 在提升時的表現其實也有所不同。

讓我們回到這段程式碼:

console.log(a);
let a;

前面已經提過,程式在這裡會回報 ReferenceError 。但並不是顯示 a is not defined,而是 Cannot access 'a' before initialization,而這就是 JS 對 varlet 做出的區別。

let 宣告的變數同樣在編譯時期就處理完變數宣告,所以在遇到 console.log(a) 之前,記憶體當中就存在這個變數了。

但是 JS 處理 var 宣告的變數時,會同時進行初始化並賦予預設值 undefinedletconst 在編譯時期卻沒有這個步驟。

一直要等到程式開始執行,並跑到進行宣告的那行程式碼,才會對 letconst 宣告的變數進行初始化。

如果在這之前存取這個變數,就會出現 Cannot access 'a' before initialization 這樣的錯誤訊息。

JS 對此有個專有名詞,叫做「暫時性死區(TDZ, Temporal Dead Zone)」。也就是 letconst 被提升後,尚未進行初始化、還無法存取的時期。

function foo() {
   // a 被提升到這裡,但處於 TDZ 狀態中
    bar(); // 在 a 的 TDZ 期間執行
    let a = "Hello"; // 遇到 a 的宣告,TDZ 結束
    function bar(){
      console.log(a)
    }
}
foo()
// ReferenceError: Cannot access 'a' before initialization

在上面的程式碼中,調用 bar() 時,記憶體中只是保留了 a 的位置,卻尚未初始化並給予預設值,因此在執行 console.log(a) 時遇到錯誤:「無法取用尚未初始化的變數 a 」。

JS 之所以會在 ES6 新增 letconst 這兩個宣告補丁,也是由於「提升」這個特性,讓 var 宣告的變數能夠在進行宣告的該行程式碼之前讀取,這樣的特性容易引起困惑。

比方說,你在某個地方取用了變數 a,卻沒注意到你把程式碼寫在 var a = true; 的前面,按照 JS 的邏輯,此時的 a 已經被初始化,並擁有 undefined 這個預設值,所以程式完全能夠正常運行,執行結果卻和預想的相反:

if(a){
  console.log("Let's start!") // 永遠不會跑到這行來
}

var a = true;

但如果改成 let,至少程式會告訴你,似乎有哪裡不對勁:

if(a){
  console.log("Let's start!") 
}

let a = true;
// ReferenceError: Cannot access 'a' before initialization

總結

經過上面對變數提升的討論,這裡能夠得出一個結論:「在 JS 中,不論程式碼撰寫順序為何,宣告永遠早於賦值。」因為宣告的處理,早在程式編譯時期就已經完成了。

除此之外,JS 在 ES6 新增的 letconst 所擁有的特性,顯然能夠降低程式碼的錯誤率。因此這兩個關鍵字出現後,便有不少人提倡全面廢止 var,而僅僅使用 letconst 進行宣告。

那麼,關於 JS 的提升終於在本系列最長的一篇文章中結束......這怎麼可能呢!作為 JS 的魔王關卡之一,提升還有另一段故事要講。

在進入下一章之前先給你一段程式碼,猜猜看會印出什麼結果?

console.log(a);
var a;
function a(){
  console.log("JavaScript!")
}

如果一切如你所料,那這裡給你一個掌聲。若是你對答案感到困惑,就來下一篇文章找找解答吧!


參考資料


上一篇
Day11 咱們井水不犯河水──作用域的優點
下一篇
Day13 提升:為什麼要提升?留在原地不好嗎?
系列文
就是要搞懂 JavaScript 啦!73
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言