Day18 | JS 關於 let、 const、var

ES6 的 letconst 是為了改變 var 在宣告變數上的一些問題。

❐ var 宣告可能會產生的問題

  • var 具函式作用域,所以不受區塊限制,但會受到函式範圍限制。
  • var 也是一種宣告變數的方式,可覆寫值,與 let 類似但相對較不嚴謹,但目前已經很少使用 var 因為較容易發生奇怪的問題 ( var 會汙染全域變數,容易造成不可預期的錯誤 )。

範例 1

  1. 程式量大的時候你可能會忘記取過什麼變數,所以會出現重複宣告的情況,蓋掉之前寫的變數的值。

    1
    2
    3
    var name = 'abc';
    var name = 'ccc';
    // var 是允許重複宣告變數的,且 console 不會丟任何錯誤提示給你,而 let 和 const 會。
  2. var 在函式外宣告屬於 global variables(全域變數,意即 JS 任何地方都可以使用),在函式內則為該函式整個區域都可以使用 → 這是屬於 scope(作用域的範疇)。主要都是圍繞在 Redeclartion(重新宣告)、Scope(作用域)、Hoisiting(提升)、TDZ 這幾個主題,有興趣的話也可以查查這些關鍵字。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function sayHi() {
    var name = "andy";
    for (var i = 1; i<=3; i++) {
    var num = 0;
    if (i = 3) {
    num = i;
    }
    }
    console.log(`${name}的座號是 ${num} 號`);
    // 可以取得 for 迴圈的變數 num,若是改用 let 宣告 num,它是取不到 for 迴圈內的 num 的,會報錯
    }
    sayHi();

範例 2. for 迴圈

1
2
3
4
for (var i = 0; i < 10; i++){
console.log(`for 迴圈內 ${i}`)
}
console.log(`for 迴圈外 ${i}`)

for 迴圈使用 var 宣告 i 會污染全域:

  • for 迴圈中,var i = 0; 中的 i 因為是用 var 宣告,所以為全域變數,在 for 迴圈外也取得到,所以如果想要把 i 控制在 for 迴圈內就會產生一些問題。
    • 這邊可透過 window.i 來查看並找到 i 的值。

範例 2. 出處:六角學院 JavaScript 核心篇 / Let, Const 基本概念

範例 3. 判斷式

1
2
3
4
5
6
var answer = true;
if (answer) {
var myFeedback = '同意';
console.log(`判斷是內:${myFeedback}`);
}
console.log(`判斷是外:${myFeedback}`);

除了上方的 for 迴圈,在判斷式也會有相同問題 → 污染全域

❐ let

  • let 宣告的變數可重新賦予新的值。
  • 可取出 let 宣告過的變數,並重新賦予新的值。但不可使用 let 再重新宣告相同的變數,會出現錯誤訊息 Uncaught SyntaxError: Identifier 'myName' has already been declared,這樣就可以避免同一個作用域下使用 let 做重覆宣告。
1
2
3
4
5
6
7
8
9
// 正確用法:宣告過的變數重新賦予新的值
let ricePrice = 100;
ricePrice = 150;

console.log(ricePice); // 150

// 錯誤用法,使用let 重新宣告相同的變數
let myName = 'Carrie';
let myName = 'CarrieT';

❐ const

  • const 是宣告一個常數,所以基本上使用 const 宣告的變數是沒辦法被調整的。( cosnt 在原始型別難以被覆寫 )

  • 隨時需要做調整的變數值的話可使用 let,不會去更改值的話可使用 const

  • const 在 object {} 與 Array [] 中使用是可被修改的。除非使用 Object.freeze(變數); 會凍結裡面的內容無法做修改。
    在 object `{}` 與 Array `[]` 中使用是可被修改的

    使用 `Object.freeze(變數);` 會凍結裡面的內容無法做修改

❐ var、let、const 作用域

  • 所謂作用域即「變數有效的作用範圍」,最大為全域作用域範圍,指變數有效範圍是全部範圍;區塊作用域指的是 {} 大括號的範圍。
  • ES6 前,沒有區塊作用域概念 ( block funciton ),僅有全域 ( global scope ) 與函式作用域 ( function scope ),var 宣告的變數具有函式作用域的特性,代表**切分變數有效範圍的最小單位是 function**。
  • ES6 後,新增區塊作用域概念 ( block funciton ),let / const 宣告的變數才具有區塊作用域的特性,切分變數最小單位的有效範圍式是 {} block

➊ var 具函式作用域

var 具函式作用域,所以不受區塊 {} 限制,但會受到函式 function 範圍限制。

因為 var 具函式作用域,所以上方「var 可能會產生的問題 」中範例 1、2、3 方式都會污染全域,除非用 function 包住。可見 function 外就讀取不到 i 的變數 ( i is not defined )。

範例1.

1
2
3
4
5
6
7
8
// 這邊使用立即函式包覆
// 也可移除立即函式把 var 調整為 let
(function(){
for (var i = 0; i < 10; i++){
console.log(`for 迴圈內 ${i}`)
}
})()
console.log(`for 迴圈外 ${i}`)

範例 2.

var 具函式作用域,所以不受區塊限制,但會受到函式範圍限制

1
2
3
4
5
6
7
8
9
10
11
12
13
// --- var不受區塊限制,但會受到函式範圍限制
// --範例一
function call() {
var isCall = 'Carrie';
}
console.log(isCall); // Uncaught ReferenceError: isCall is not defined

// --範例二
{
var isCall = 'Carrie';
}

console.log(isCall ); //Carrie

➋ const、let 具區塊作用域

範例 1.

constlet 具區塊作用域,所以有效作用域範圍會被限制在該區域中

1
2
3
4
5
6
7
8
9
10
11
12
// --範例一
{
let isCall = 'Carrie';
}
console.log(isCall );
// --範例二
function call(){
const isCall = 'Carrie';
}
console.log(isCall );

// 皆印出 Uncaught ReferenceError: isCall is not defined

❐ let 、const 實作技巧

實作 1. for 迴圈使用 var 宣告

1
2
3
4
5
6
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(`這執行第 ${i} 次`);
}, 0);
}
console.log(i);

解析:

for 迴圈中使用 var 宣告變數 i 是全域變數,所以 for 迴圈外 console.log(i); 中的 i 會是執行到最後的結果 10 ( 為 0 到 9 個執行一次的狀態 )。

  • setTimeout 為非同步的程式, JS 會放到事件緒列內,等到所有程式都執行完才回來執行這個非同步程式。所以 setTimeout 中的 i 會是全域變數的 i 並不是 for 迴圈內的 i

答案:

  • 所以無法如預期中依依印出 0 到 9
  • console.log(i); 會印出 10setTimeout 會印出 10 次 這執行第 10 次

實作 2. for 迴圈使用 let 宣告

1
2
3
4
5
6
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(`這執行第 ${i} 次`);
}, 0);
}
console.log(i);

解析:

  • for 迴圈中使用 let 宣告變數 i 就不會是全域變數,因為 let 為區塊作用域只會在區塊 {} 內產生作用

答案:

  • setTimeout 會依序印出 這執行第 0 次這執行第 9 次
  • console.log(i); 會印出 Uncaught ReferenceError: i is not defined
    • 因為使用 let 宣告的關係,所以 i 並非全域變數。

Let 有沒有 Hoisting?暫時性死區介紹

Hoisting 分創造與執行兩階段,下方實作範例中來看看 varlet 宣告會有什麼不同處。

實作1. var

1
2
console.log(Ming);
var Ming = '小明';

解析:

Hoisting 分創造與執行兩階段,以上方程式碼來說會拆分為下面形式:

  • 宣告的變數會先被移至創造階段 → var Ming;
  • 執行階段再賦予值 → Ming = '小明';
  • 在創在階段 var Ming;undefined,如果在賦予值前就先 console.log(Ming); 要去取 Ming 的值就會印出 undefined
1
2
3
4
5
//----- 創造階段
var Ming;
//----- 執行階段
console.log(Ming);
Ming = '小明';

答案:
印出 undefined

實作 2. let

1
2
console.log(Ming);
let Ming = '小明';

解析:

  • let 在創造階段會產生「 暫時性死區 TDZ 」,在此區域是無法取得值的。
  • 所以 let 類似 Hoisting 提升的概念,只是它在提升時不會賦予變數 undefined 的值,而是出現「 暫時性死區 TDZ 」,這個暫時性死區無法存取這個變數。
1
2
3
4
5
6
//----- 創造階段
let Ming; // 暫時性死區 TDZ

//----- 執行階段
console.log(Ming);
let Ming = '小明';

答案:

會顯示錯誤訊息 Uncaught ReferenceError: Cannot access 'Ming' before initialization ( 無法在初始化前去取得此變數 )。

實作 3. let

1
2
3
4
console.log(typeof a);
console.log(typeof myName);

let myName = '';

解析:

  • let 一樣有創造階段,但 let 在創造階段會產生「 暫時性死區 TDZ 」,在此區域是無法取得值的,所以會顯示錯誤訊息 Uncaught ReferenceError: Cannot access 'Ming' before initialization

答案:

  • console.log(typeof a); 印出 undefined
  • console.log(typeof myName); 印出錯誤訊息 Uncaught ReferenceError: Cannot access 'Ming' before initialization ( 無法在初始化前去取得此變數 )。

Let 及 Const - 課後練習

課後練習 1

1
2
3
4
a();
let a = function () {
console.log('a');
}

答案:

1
2
3
4
5
6
7
//--創造
let a;
//--執行
a();
a = function () {
console.log('a');
}

印出:Uncaught ReferenceError: Cannot access 'a' before initialization

課後練習 2. 課程上我們了解到 let 會有暫時性死區問題,所以不能在變數宣告建立之前使用該變數,那 const 呢?

1
2
console.log('a');
const a = 'Casper';

答案:

印出:Uncaught ReferenceError: Cannot access 'a' before initialization

課後練習 3. 同前面幾題,如果我們接下來將 console.log() 移至後方會得什麼?

1
2
let a;
console.log(a);

答案:

undefined

1
2
3
4
//--創造
let a;
//--執行
console.log(a);

課後練習 4. 同上題,若改成 const 呢?

1
2
const a;
console.log(a);

答案:

Uncaught SyntaxError: Missing initializer in const declaration ( const 宣告缺少初始化 )

課後練習 5. 請問 console.log() 將會出現什麼?

1
2
3
const array = [];
array.push('Casper');
console.log(array);

答案:

[’Casper’]

課後練習 6. 請問 console.log() 結果是什麼?

1
2
3
4
5
6
let a = 10;
function fu() {
console.log(a);
let a = 20;
}
fu();

答案:

1
2
3
4
5
6
7
8
9
10
//創造
function fu() {
console.log(a);
let a = 20;
}
let a;

//執行
a = 10;
fu();
  • fu() 內
    • let 在創造階段為暫時性死區,

      1
      2
      3
      4
      5
      6
      7
      function fu() {
      // 創造
      let a; //暫時性死區
      // 執行
      console.log(a);
      a = 20;
      }
    • 另外 let 為區塊作用域,在 {} 執行完記憶體就會釋放掉。所以答案為 Uncaught ReferenceError: Cannot access 'a' before initialization

課後練習 7. 請問以下 console.log() 將會出現什麼?

1
2
3
4
5
6
function fu() {
}
fu.fu = 'QQ';
const a = fu;
a.fu = 'Casper';
console.log(a.fu);

答案:

  • 答案為 Casper
  • 函式為物件型別,所以是可以新增屬性的。後面的 a.fu 會覆蓋掉前面的 fu.fu

參考資訊