ES6開始就引入了let
、const
替代var
去宣告變數。今天就來整理一下自己學習let
和const
時碰到的概念。
開發時用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
照讓你過關。而let
和const
就能正正做到這一點。下文會再重用這個例子去講解更多。
另外再舉個很多人都知道的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次setTimeout
,setTimeout
裏面的事不干for迴圈的事,當它極速跑完迴圈,1秒後setTimeout
才會執行,但這時候的i
已經是5了,所以五次的迴圈會回傳5。
但這不是我們想要的,我們要怎樣才可以顯示沒有因為跑迴圈而被汙染的變數?我們先暫時放下這問題,之後再解決。
const
甚至要求你必需賦值),就去提取該變數便會報錯,而非回傳undefined
window
的屬性區塊作用域是指大括號{}
之間的區域。這時候變數只存活在{}
這個括號裏,{}
外就不能調用。就像下面例子一樣:
{
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);
}
let
、const
、var
來宣告變數,都會提升至作用域的最高處。不同的是,如果我們沒有宣告就直接使用該變數時,var會回傳undefined
,但const
和let
會報錯。
第一點要注意的是,let
、const
和var
一樣,都是會提升至作用域的最高處,例如有開發者曾經舉出以下例子:
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
,這一點之後會再提及。
從這兩個例子可見,let
和const
所宣告的變數是有提升至最高函式的作用域。
第二點是,let
和const
雖然和var
一樣有提升的作用,但let
和const
不會預設有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();
如果該變數已經被宣告過,就不能再宣告。let
和const
都一樣。
例子:
let food = 'banana';
let food = 'apple'; //Uncaught SyntaxError: Identifier 'food' has already been declared
window
的屬性用var
宣告全域變數,會成為window
的屬性,但let
和const
不會。
很多人都知道不要用var
,要改用let
和const
。如果該變數會變,就用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
但我在剛習慣用let
和const
去開發時,有時會搞不清什麼時候用let
和const
,到底怎樣去介定那個變數會變還是不變?例如以下例子,我會修改陣列和物件裏面的資料,這樣算是變嗎?
let x = [];
x.push(1,2,3,4)
let y = {name: 'Mary', age: 30}
y.name = 'Peter'
後來在google找到一些解答,原來要決定那個變數會變,還是不變,是指那個變數的記憶體地址有沒有變,而不是它的值。
簡單重溫一下記憶體存放變數的原則。記憶體存放變數的值時,會看變數的值是哪個資料型別:
undefined
、null
、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?