var let cost

一尤花島樹鳴發表於2020-10-07

var 宣告提升
使用 var 時,下面的程式碼不會報錯。這是因為使用這個關鍵字宣告的變數會自動提升到函式作用域
頂部:

function foo() { 
 console.log(age); 
 var age = 26; 
} 
foo(); // undefined 

之所以不會報錯,是因為 ECMAScript 執行時把它看成等價於如下程式碼:


`function foo() { 
 var age; 
 console.log(age); 
 age = 26; 
} 
foo(); // undefined 

這就是所謂的“提升”(hoist),也就是把所有變數宣告都拉到函式作用域的頂部。此外,反覆多次
使用 var 宣告同一個變數也沒有問題:

function foo() { 
 var age = 16; 
 var age = 26; 
 var age = 36; 
 console.log(age); 
} 
foo(); // 36

let 宣告
let 跟 var 的作用差不多,但有著非常重要的區別。最明顯的區別是,let 宣告的範圍是塊作用域,而 var 宣告的範圍是函式作用域。

if (true) { 
 var name = 'Matt'; 
 console.log(name); // Matt 
} 
console.log(name); // Matt

if (true) { 
 let age = 26; 
 console.log(age); // 26 
} 
console.log(age); // ReferenceError: age 沒有定義

let 也不允許同一個塊作用域中出現冗餘宣告。這樣會導致報錯:

var name; 
var name; 
let age; 
let age; // SyntaxError;識別符號 age 已經宣告過了

對宣告冗餘報錯不會因混用 let 和 var 而受影響。這兩個關鍵字宣告的並不是不同型別的變數, 它們只是指出變數在相關作用域如何存在。

var name; 
let name; // SyntaxError 
let age; 
var age; // SyntaxError

let 與 var 的另一個重要的區別,就是 let 宣告的變數不會在作用域中被提升。
在解析程式碼時,JavaScript 引擎也會注意出現在塊後面的 let 宣告,只不過在此之前不能以任何方式來引用未宣告的變數。在 let 宣告之前的執行瞬間被稱為“暫時性死區”(temporal dead zone),在此階段引用任何後面才宣告的變數都會丟擲 ReferenceError。

// name 會被提升
console.log(name); // undefined 
var name = 'Matt'; 
// age 不會被提升
console.log(age); // ReferenceError:age 沒有定義
let age = 26;

全域性宣告
與 var 關鍵字不同,使用 let 在全域性作用域中宣告的變數不會成為 window 物件的屬性(var 宣告的變數則會)。

var name = 'Matt'; 
console.log(window.name); // 'Matt' 
let age = 26; 
console.log(window.age); // undefined

條件宣告
對於 let 這個新的 ES6 宣告關鍵字,不能依賴條件宣告模式

<script> 
 let name = 'Nicholas'; 
 let age = 36; 
</script> 
<script> 
 // 假設指令碼不確定頁面中是否已經宣告瞭同名變數
 // 那它可以假設還沒有宣告過
 if (typeof name === 'undefined') { 
 let name; 
 } 
 // name 被限制在 if {} 塊的作用域內
 // 因此這個賦值形同全域性賦值
 name = 'Matt'; 
 try { 
 console.log(age); // 如果 age 沒有宣告過,則會報錯
 } 
 catch(error) { 
 let age;
  } 
 // age 被限制在 catch {}塊的作用域內
 // 因此這個賦值形同全域性賦值
 age = 26; 
</script>

for 迴圈中的 let 宣告

let 出現之前,for 迴圈定義的迭代變數會滲透到迴圈體外部:

for (var i = 0; i < 5; ++i) { 
 // 迴圈邏輯 
} 
console.log(i); // 5

改成使用 let 之後,這個問題就消失了,因為迭代變數的作用域僅限於 for 迴圈塊內部:

for (let i = 0; i < 5; ++i) { 
 // 迴圈邏輯
} 
console.log(i); // ReferenceError: i 沒有定義

在使用 var 的時候,最常見的問題就是對迭代變數的奇特宣告和修改:

for (var i = 0; i < 5; ++i) { 
 setTimeout(() => console.log(i), 0) 
} 
// 你可能以為會輸出 0、1、2、3、4 
// 實際上會輸出 5、5、5、5、5

之所以會這樣,是因為在退出迴圈時,迭代變數儲存的是導致迴圈退出的值:5。在之後執行超時邏輯時,所有的 i 都是同一個變數,因而輸出的都是同一個最終值。

而在使用 let 宣告迭代變數時,JavaScript 引擎在後臺會為每個迭代迴圈宣告一個新的迭代變數。每個 setTimeout 引用的都是不同的變數例項,所以 console.log 輸出的是我們期望的值,也就是迴圈執行過程中每個迭代變數的值。

for (let i = 0; i < 5; ++i) { 
 setTimeout(() => console.log(i), 0) 
} 
// 會輸出 0、1、2、3、4

因為對於 let 來說,他會建立一個塊級作用域,相當於

{ // 形成塊級作用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, 0 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}

const 的行為與 let 基本相同,唯一一個重要的區別是用它宣告變數時必須同時初始化變數,且嘗試修改 const 宣告的變數會導致執行時錯誤。

const age = 26; 
age = 36; // TypeError: 給常量賦值
// const 也不允許重複宣告
const name = 'Matt'; 
const name = 'Nicholas'; // SyntaxError 
// const 宣告的作用域也是塊
const name = 'Matt'; 
if (true) { 
 const name = 'Nicholas'; 
} 
console.log(name); // Matt

const 宣告的限制只適用於它指向的變數的引用。換句話說,如果 const 變數引用的是一個物件,
那麼修改這個物件內部的屬性並不違反 const 的限制。

const person = {}; 
person.name = 'Matt'; // ok 

JavaScript 引擎會為 for 迴圈中的 let 宣告分別建立獨立的變數例項,雖然 const 變數跟 let 變
量很相似,但是不能用 const 來宣告迭代變數(因為迭代變數會自增):

for (const i = 0; i < 10; ++i) {} // TypeError:給常量賦值

相關文章