iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 5
3
Modern Web

JavaScript基本功修煉系列 第 5

JavaScript基本功修練:Day5 - 宣告變數 - let、const、var

ES6開始就引入了letconst替代var去宣告變數。今天就來整理一下自己學習letconst時碰到的概念。

let、const解決了var的什麼問題?

開發時用var宣告變數會容易導致意外汙染全域變數的問題,例如是區域變數覆蓋全域變數。

var food = 'apple';
function func(){
    var result = 'I eat ' + food
    console.log(result)

}
func(); //I eat apple

這個例子很簡單,在func函式裏用到全域變數food,組合字串及回傳。但如果程式碼變得更複雜時,又或者另一個開發者沒注意到food已經在第一行宣告過了,就可能會出現以下的問題:

var food = 'apple';

// 200行code之後

function func(){
    var result = 'I eat ' + food
    console.log(result) // I eat undefined

    // 100行code之後,我忘了之前已經宣告過food
    var food = 'banana';
}
func(); 

func這個函式裹的最後一行程式碼,我們再次宣告food這個變數及重新賦值,根據hoisting(提升)的概念,在函式裹的var food會提升至函式作用域裹的最高處,在「提升之後(var food)」到「賦值之前food = 'banana'」,這段期間food的值會是undefined。我們可以把整個過程想像如下圖:

var food = 'apple';

function func(){
    var food 
    // 提升之後 
    var result = 'I eat ' + food
    console.log(result) // I eat undefined

    // 賦值之前
    food = 'banana';
}
func(); 

所以在函式裹第二行的food會變成undefined

澄清一點,food變成了undefined這個問題,並不是因為重複宣告變數。我在函式內再次宣告在函式外的變數是不會報錯的,如下面的做法:

var food = 'apple';

// 200行code之後

function func(){
    // 100行code之後,我忘了之前已經宣告過food
    var food = 'banana';
    var result = 'I eat ' + food
    console.log(result) // I eat banana
}
func(); 

以上的例子,我把var food = 'banana'放在var result = 'I eat ' + food前面,這裹會成功回傳I eat banana。因為我是在變數food被賦值'banana'之後(用=去賦予),才提取food這個變數,這時候food已經是'banana'。總括來說,這個問題出現與否是取決於你在什麼時候提取這個變數。重複一次,如果你在「提升之後(var food)」到「賦值之前food = 'banana'」提取變數,就會變成undefined

更安全的做法就是,JS不讓開發者在「提升之後」到「賦值之前」的這段期間提取變數。如果開發者要這樣做,JS就直接報錯,而不是拋出一個undefeind照讓你過關。letconst就能正正做到這一點。下文會再重用這個例子去講解更多。

另外再舉個很多人都知道的for迴圈邪惡例子,我們預期印出0,1,2,3,4:

for (var i=0; i<5; i++){
    setTimeout(function(){
        console.log(i) //一秒後console顯示五次5
    }, 1000);
}

但最後console是回傳五次5這個數值:

因為for迴圈的工作就是跑5次setTimeoutsetTimeout裏面的事不干for迴圈的事,當它極速跑完迴圈,1秒後setTimeout才會執行,但這時候的i已經是5了,所以五次的迴圈會回傳5。

但這不是我們想要的,我們要怎樣才可以顯示沒有因為跑迴圈而被汙染的變數?我們先暫時放下這問題,之後再解決。

let、const特性

  • 區塊作用域
  • 變量會提升,但若未宣告該變數時(const甚至要求你必需賦值),就去提取該變數便會報錯,而非回傳undefined
  • 不允許重複宣告
  • 全域變數不會成為window的屬性

1. 區塊作用域

區塊作用域是指大括號{}之間的區域。這時候變數只存活在{}這個括號裏,{}外就不能調用。就像下面例子一樣:

{
    const x = 10;
}
console.log(x) //Uncaught ReferenceError: x is not defined
{
    let y = 20;
}
console.log(y) //Uncaught ReferenceError: y is not defined
{
    var z = 30;
}
console.log(z) //30

再舉個例子:

let applePrice = 5

if(applePrice < 10){
    let message = 'I will buy an apple'
    console.log(message) //I will buy an apple
}

console.log(message) //message is not defined

回到剛剛for 迴圈沒有印出0,1,2,3,4的例子,如果改成let去宣告i,就能把每次的迴圈次數數字帶進去了。這裏的i不再是全域變數,所以不會顯示五次5:

for (let i=0; i<5; i++){
    setTimeout(function(){
        console.log(i)
    }, 1000*i); 
}

2. 變量會提升,但若未宣告該變數,會回傳錯誤,而非undefined

letconstvar來宣告變數,都會提升至作用域的最高處。不同的是,如果我們沒有宣告就直接使用該變數時,var會回傳undefined,但constlet會報錯。

第一點要注意的是,letconstvar一樣,都是會提升至作用域的最高處,例如有開發者曾經舉出以下例子:

var a = 10
function test(){
  console.log(a) //Uncaught ReferenceError: Cannot access 'a' before initialization
  let a
}
test()

如果let沒有提升,那麼console應該會顯示10。但這裹卻是ReferenceError

const也一樣:

var a = 10
function test(){
  console.log(a) //Uncaught ReferenceError: Cannot access 'a' before initialization
  const a = 5;
}
test()

注意,因為const在宣告時一定要賦值,所以我寫了const a = 5,這一點之後會再提及。

從這兩個例子可見,letconst所宣告的變數是有提升至最高函式的作用域。

第二點是,letconst雖然和var一樣有提升的作用,但letconst不會預設有undefined這個值。

看看以下MDN的說法:

Unlike variables declared with var, which will start with the value undefined, let variables are not initialized until their definition is evaluated. Accessing the variable before the initialization results in a ReferenceError. The variable is in a "temporal dead zone" from the start of the block until the initialization is processed.

意思是用var宣告的變數,它的初始值預設是undefined,但let不會有這個預設。當執行let變數的宣告語句時,let才會被初始化和能夠被訪問。

如果我們這樣去宣告x,就可以得出undefined

let x = 10
function func(){
    let x
    console.log(x); //undefined
}
func();

因為以上例子中,宣告x的語句先被執行,這時候x這個變數已被初始化和能夠被訪問。

const不能只宣告但不賦值,因為ES6規定用const宣告變數時,一定要指定一個值給它:

const x = 10
function func(){
    const x //Uncaught SyntaxError: Missing initializer in const declaration
    console.log(x); 
}
func();

看看MDN的對於const的解釋:

An initializer for a constant is required. You must specify its value in the same statement in which it's declared. (This makes sense, given that it can't be changed later.)

再來多一個例子,我們重用文章開頭,回傳I eat undefined結果的例子去改寫:

let food = 'apple';

// 200行code之後

function func(){
    let result = 'I eat ' + food //Uncaught ReferenceError: Cannot access 'food' before initialization
    console.log(result)

    // 100行code之後,我忘了之前已經宣告過food
    let food = 'banana'; 
}
func();

console會回傳Uncaught ReferenceError: Cannot access 'food' before initialization。直接說明我不能在food這個變數被初始化之前就去訪問,換言之,就是未執行let food這行宣告語法前,food是不能被訪問。

變數food會提升到函式作用域的最高處,而在「變數food」後,「執行let food這行宣告語法」前,這段時間我們會稱為暫時性死區(TDZ),在TDZ中,我們不能訪問food這個變數。如下:

let food = 'apple';

// 200行code之後

function func(){
    //food提升到這裹,TDZ開始
    let result = 'I eat ' + food //Uncaught ReferenceError: Cannot access 'food' before initialization
    console.log(result)

    //宣告food,TDZ結束
    let food = 'banana'; 
}
func();

3. 不允許重複宣告

如果該變數已經被宣告過,就不能再宣告。letconst都一樣。
例子:

let food = 'banana';
let food = 'apple'; //Uncaught SyntaxError: Identifier 'food' has already been declared

4. 全域變數不會成為window的屬性

var宣告全域變數,會成為window的屬性,但letconst不會。

何時用let,何時用const?

很多人都知道不要用var,要改用letconst。如果該變數會變,就用let,不變就用const

根據Google的JavaScript style guide,說明我們應該默認用const,如果該變數需要重新被賦值才用let,永遠不用var

Declare all local variables with either const or let. Use const by default, unless a variable needs to be reassigned. The var keyword must not be used

但我在剛習慣用letconst去開發時,有時會搞不清什麼時候用letconst,到底怎樣去介定那個變數會變還是不變?例如以下例子,我會修改陣列和物件裏面的資料,這樣算是變嗎?

let x = [];
x.push(1,2,3,4)
let y = {name: 'Mary', age: 30}
y.name = 'Peter'

更改不是指值的更改,而是記憶體地址的更改

後來在google找到一些解答,原來要決定那個變數會變,還是不變,是指那個變數的記憶體地址有沒有變,而不是它的值。

簡單重溫一下記憶體存放變數的原則。記憶體存放變數的值時,會看變數的值是哪個資料型別:

  • 基本型別值:
    包括字串、數值、字串、undefinednull、symbol(ES6加入)。
    不能更改它的值,只能重新賦值,重新賦值時會更改記憶體地址。
  • 引用值:
    包括物件、陣列、函式等等。
    能修改它裏面的值,這樣不會更改記憶體地址,但如果重新賦予一個新的值,就會更改記憶體地址。

回到剛才提及的例子,我只是修改了該陣列和該物件裏的值,而它們在記憶體的地址都沒有被改變,所以它們並沒有變,也因為它們沒有變,所以我應該用const去宣告,而非let

如何才算變呢?重用這兩個例子,如果我做以下的事,就是更改了它們的記憶體地址,也就是變了:

const x = [];
x = [1,2,3,4] //Uncaught TypeError: Assignment to constant variable.
const y = {name: 'Mary', age: 30}
y = {name: 'Peter'}; //Uncaught TypeError: Assignment to constant variable.

參考資料

JavaScript’s Memory Model
一次說清楚 JavaScript 中宣告的各種提升行為(var、function、let/const)
JavaScript: var, let, const 差異
我知道你懂 hoisting,可是你了解到多深?
Day26 var 與 ES6 let const 差異
Var, Let, and Const – What's the Difference?


上一篇
JavaScript基本功修練:Day4 - 基本型別(II) - 字串、布林、null、undefined、Symbol
下一篇
JavaScript基本功修練:Day6 - 傳址、傳值
系列文
JavaScript基本功修煉31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
ChungKaiLu
iT邦新手 5 級 ‧ 2021-12-29 09:10:04

感謝,程式碼實例都很清楚易懂。列上的Reference也很有參考價值

我要留言

立即登入留言