[JS] 認識變數:let、const、var


Posted by Genos on 2021-11-16

💎 變數的演進

JavaScript 過去只有 var 一種變數宣告方式,直到 2015 年發表的 ECMAScript 2015(ES2015),第 6 版,又被稱為 ECMAScript 6(通常簡稱為 ES6 ),才有 letconst 兩種新的變數宣告方式,這個版本有相當多的革新,現在常用的箭頭函式、樣板字串...等都包含在其中。

參考資料:
Wiki - ECMAScript

💎 Can I Use?

雖然 2015 年距今已經超過 6 年了,但是仍有些企業或公司在使用 IE 這類過時的瀏覽器,letconst 在 IE 11 只有部分支援、 IE 10 以下版本則是完全不支援,使用前需要確認用戶端環境是否支援。

參考資料:
Can I Use

💎 變數特性

var的特色是就是沒有規矩,因此需要小心使用避免踩坑,letconst 有了進一步的規範,可以針對不同需求場景運用,使用上較為安全穩定,接下來對各種特性逐一說明。

🔸 重複宣告

  • var 可以重複宣告同一個變數名稱,後者會覆蓋前者,而且程式不會報錯。

      var a = 1;
      var a = 2;
      console.log(a); // 2
    
  • letconst 則不允許重複宣告(會報錯),可以避免變數名稱衝突導致 debug 困難的情況。

      let b = 1;
      let b = 2;
      // Uncaught SyntaxError: Identifier 'b' has already been declared
    

🔸 重新賦值

摘錄維基百科:
常數,又稱定數(Constant),是指一個數值固定不變的常量,例如圓周率,與之相反的是變數。

參考資料:
Wiki - 常數

  • 過去只有 var 時,如果想要宣告一個不能被變更的常數,通常會用全部大寫,加上 _ 連接單字來標示,例如:

      var DO_NOT_TOUCH_ME = 'X';
    
  • 但是名稱只是幫助辨識,不會真的具備常數的特性。
      var DO_NOT_TOUCH_ME = 'X';
      DO_NOT_TOUCH_ME = '我偏要改';
      console.log(DO_NOT_TOUCH_ME); // 我偏要改
    
  • const 可以宣告『真正』的常數,有了 const 之後也比較不會特別要求使用大寫格式的變數名稱來區別(視團隊開發時的規範而定)。
     const doNotTouchMe = 'X';
     doNotTouchMe = '我偏要碰';
     // Uncaught TypeError: Assignment to constant variable.
    
  • const 的使用時機
    對 JS 新手來說,大概只知道圓周率、太陽...等明確固定數值要用常數,但是下列的資料都應盡量使用 const 宣告:
    • DOM:使用 querySelector 這類方法取得的 DOM 通常不會再改變。
    • 陣列:使用陣列方法來操作,避免被 = 砍掉重練。
    • 物件:使用物件方法來操作,避免被 = 砍掉重練。
    • 函式:避免匿名函式被蓋掉,功能全失。

🔸 初始值

  • 常數(const)除了無法變更外還有另一個特性,宣告的時候一定要賦予值,varlet 則無此限制。
      const a;
      // Uncaught SyntaxError: Missing initializer in const declaration
    
  • 既然是後續不能更動的值,在一開始就設定好是很合理的事情吧!

🔸 作用域

作用域(Scope)是變數可以被使用的區域,也代表著在相同的作用域才會有重複命名的衝突問題。

📗 全域作用域(Global Scope)

當我們在一個 JS 檔案直接撰寫變數宣告時,變數存在於最外層,無論是 var、let、const 都屬於全域變數,無論寫了多少層的 {}函式,都可以向外找到並使用全域變數。

```
let s;

function t() {
  if(true){{{{{{s=666}}}}}}
};

t();

console.log(s); // 666
```

不同的變數宣告方式,在運作機制上會有所不同,當變數跑到 window 底下會被視為一種『污染』,應盡量避免這種狀況。

  • var 宣告全域變數時會成為 window 物件底下的一個屬性
  • let、const 不會被放到 window 底下
  • 直接用變數名稱宣告(不加前綴詞),無論放在 {}函式 內都會成為 window 物件底下的一個屬性。

    dirtyA = 1;
    console.log(window.dirtyA); // 1
    
    var dirtyB = 2;
    console.log(window.dirtyB); // 2
    
    let c = 3;
    console.log(window.c); // undefined
    
    const d = 4;
    console.log(window.d); // undefined
    
    function dirtyVariable() {
      dirtyE = 5;
    }
    dirtyVariable();
    console.log(window.dirtyE); // 5
    
  • 直接宣告在 window 底下的變數可以使用 delete 物件方法刪除;而使用 var 宣告的全域變數無法使用 delete 刪除(介於屬性與非屬性之間)。

    x = 1;
    delete window.x; // true
    console.log(x); // x is not defined
    
    var y = 2;
    delete window.y; // false
    console.log(y); // 2
    
  • 在 ES5 時可以透過'use strict'(嚴格模式)來避免這些不好的操作,使用 ES6 時應只用 letconst 就好。

    延伸閱讀:
    1、深入JavaScript系列(一):詞法環境
    2、Javascript 的嚴格模式 (Strict Mode)

📗 函式作用域(Function Scope)

var 的作用域除了最外層的全域,還會在函式 (function) 內區隔。

  var a = 1;
  function foo() {
    var a = 2;
    var b = 3;
  }
  foo();
  console.log(a); // 1
  console.log(b); // b is not defined

上面的範例可以看到宣告在函式內的兩個變數,是無法從全域取得的。

  for (var i = 1; i < 5; i++) {
    setTimeout(()=>
    console.log(i), 500);
  }
  // 5
  // 5
  // 5
  // 5

function 的範圍較大,撰寫時很難把程式切分成一堆 function 來區隔作用域,上面的程式碼在處理非同步時就出現不如預期的結果。

延伸閱讀
1、JavaScript 核心觀念(36)-函式以及 This 的運作-立即函式
2、閉包

📗 區塊作用域(Block Scope)

letconst 的作用域除了最外層的全域,會在大括號 {} 內區隔:

  {
    const a = 1;
    {
      let b = 2;
      var c = 3;
    }
    console.log(a); // 1
    console.log(b); // b is not defined
    console.log(c); // 3   
  }
  console.log(a); // a is not defined
  console.log(b); // b is not defined
  console.log(c); // 3

上面的範例可以看到 var 無論在哪個大括號內都可以取用,但 letconst 無法在自己存在的大括號外被取用。

常用的 if-elsefor等方法都會使用大括號,相對於 function,可以把作用域區分的更細。

前個段落 setTimeout 範例內的 var 改成 let,就能如預期的執行了。

  for (let i = 1; i < 5; i++) {
    setTimeout(()=>
    console.log(i), 500);
  }
  // 1
  // 2
  // 3
  // 4

參考資料
JS 原力覺醒 Day04 - Function Scope / Block Scope

🔸 提昇(Hoisting)

JavaScript 是一種直譯式語言,程式是由上到下逐行執行,理所當然,還沒宣告變數時沒辦法取得值:

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

但是在執行前實際上還有一個執行環境建置的階段,會把程式碼中所有變數名稱、函式先完成宣告,再逐行執行,因此,下面的程式碼中並不會出現任何錯誤:

console.log(a); // undefined
console.log(foo); // foo(){...}
var a;
function foo() {
  return 'Hello';
}

實際執行順序如下:

var a; // 提昇
function foo() { // 提昇
  return 'Hello';
}
console.log(a); // undefined
console.log(foo); // foo(){...}

再看下面這段程式碼,想想看結果是什麼:

var b = 1;
var b;
console.log(b);

你可能會認為答案是 undefined,因為重新宣告但是沒有賦予值,但答案其實是 1,經過提昇後實際執行順序如下:

var b;
var b;
b = 1;
console.log(b);

這樣的機制似乎讓這個語言又多了一個特性要記下來,為了讓事情變得簡單,letconst 有不一樣的機制,稱為 Temporal Dead Zone(TDZ),暫時性死區。

使用 letconst 宣告的變數名稱從建立到賦值(包含賦予 undefined)的期間會無法被取用,這段期間就是 TDZ;換句話說,變數提昇後會被鎖定,等到程式碼執行到他實際宣告的那一行才可取用。

這種把變數名稱給保留起來的方法,讓 JS 撰寫上更加嚴謹、有保障。

console.log(x); // x is not defined
let x = 1;
console.log(x);

參考文章
1、我知道你懂 hoisting,可是你了解到多深?
2、理解ES6中的暫時死區(TDZ)

💎 總結

前面一共針對變數的5種特性說明,整理後的列表如下:

var let const
重複宣告
重新賦值
強制賦予初始值
作用域 function { } { }
提升機制 可被取用 TDZ TDZ

以上是我對變數型別的一點認知,如有錯誤或是補充的知識點,也歡迎大家不吝指教,謝謝!

參考資料
六角學院-JavaScript 那個 let, const, var 到底差在哪?


#javascript







Related Posts

Linkedin Java 檢定題庫 try-catch

Linkedin Java 檢定題庫 try-catch

MTR04_0821

MTR04_0821

[IIS] 32位元應用程式,無故異常錯誤合輯

[IIS] 32位元應用程式,無故異常錯誤合輯


Comments