在昨天我們稍微提到了 react 裡宣告變數的方式,也就是 const、let、var。
不過在了解宣告變數之前,先了解作用域(Scope)會讓你上手更快。
基本上 Scope 可以分為 global 和 local 兩種,依照定義的位置決定你的作用域,跟投胎有點像啦,讓我們先來看看各自的定義。
定義在全域的變數就代表在任何地方都能夠自由存取,舉例來說:
var fruit ='apple';
console.log(fruit) //apple
function fruitName(){
console.log(fruit)
}
fruitName() //apple
區域有兩種情況 Function Scope 和 Block Scope
定義在函式內就是 Function Scope,舉例來說:
function fruitName() {
var fruit = "apple";
console.log(fruit);
}
fruitName() //apple
console.log(fruit); //not defined
通常定義在 Function Scope 的變數會用 var 來宣告,這是為了避免造成全域命名汙染。
註:全域命名汙染的大致意思就像是你創腳色的時候,你好不容易想到一個超級猛的名字,像是: "xx金城武",但卻被提示說名字已被使用那種感覺,但在 JS 裡並不會提示你這個,如果你的 local 變數已經被 global 變數定義過,他們會共用同一個儲存空間,後果會怎樣,想想都覺得可怕。
定義在 try catch、if esls、while 等邏輯迴圈內就是 Block Scope,其實只要是{}就是一個區塊,舉例來說:
function fruitName() {
if(true){
let fruit1 ="banana"
let fruit2 ="orange"
console.log(fruit); //undefined
console.log(fruit1); //banana
console.log(fruit2); //orange
}
var fruit = "apple";
console.log(fruit); //apple
console.log(fruit1); //error : not defined
console.log(fruit2); //error : not defined
}
fruitName()
Block Scope 內定義的變數只能作用在那個區塊內,出了那個區塊就會不存在。
可是其實看完以上這些式例可能會引出幾個疑問:
1.為什麼定義在 function scope 內的都用 var ?用 let 不好嗎?const 呢?怎麼沒出現?
2.為什麼在 block scope 內不用 var 定義,是有甚麼原因嗎?
3.let 和 var 差在哪裡?
4.為什麼在 block scope 內可以接到 fruit ,他不是還沒被定義嗎?
5.為什麼之前用 const 宣告 arrow function 而不是用 function 宣告,這有差別嗎?
這邊給個提示,可以查查看 hosting 和 lexical scope 還有想一下 function scope、 block scope的差別。
為了解決上面這些疑惑,就必須來好好講解一下 var、let、const 有甚麼差別。
其實在 ES6 之前,所有的變數宣告都是靠 var 去獨攬大權的,並且只有 function 可以做為 scope,區塊(block)是沒有的,這就引起了很多問題,像是造成全域命名汙染、循環變數洩漏或是區域變數互相影響,直到 ES6 出現,讓 block 也可以作為 scope,並新增了 let/const 兩種宣告方式,讓我們可以用更嚴謹的方式去保護變數,現在我們就來看看到底有甚麼差別吧。
const 就是常數(constant)的意思,一開始就需要給值,且不可被重複定值,舉個例子來說:
function fruitName() {
const fruit; //Parsing error: Unexpected token
const fruit1='apple' //
const fruit1='banana' //Parsing error: Identifier 'fruit1' has already been declared
console.log(fruit); //error
console.log(fruit1); //apple
}
console.log(fruit1) //error:not defined
fruitName()
雖然說這樣跑一定會出錯 XD,不過我就讓大家看一下會出現怎樣的結果。
上面可以看到 const 有相對來說最嚴格的命名條件,同時也是保護性最高的。
註:就保護性來說 const > let> var。
其實 const 也可以用來定義 陣列和 arrow function,就像我們前一篇的 function 一樣。
const setfruit = () =>{}
const fruit =[]
雖然說不能直接改變 const 宣告的變數,但是可以改變用 const 宣告變數裡面的值,意思就像是這樣:
const fruit =[]
console.log(fruit) //[]
fruit[0]=1
console.log(fruit) //[1]
這樣是可以滴,同理,用在 arrow function 也是一樣,這樣更加確保了 code 的穩固性,也可以在 hook 裡看到 使用 const 來宣告 useState。
let 跟 const 一樣,只會存在定義的 block 裡面,可以避免函數被不當存取或影響,我們可以看下面的例子:
function fruitName() {
const fruit='apple'
console.log(fruit);
}
fruitName() //apple
console.log(fruit); //error:not defined
因為這個侷限的特性,let 常被用來定義在 for loop、if else、while 等邏輯迴圈內,舉個例子來說:
const fruits = ["apple", "orange", "banana"];
function fruitName(fruit) {
for (let i = 0; i < fruit.length; i++) {
setTimeout(function(){
console.log(fruit[i])},1000);
}
}
fruitName(fruits); // apple orange banana
我們希望使用 setTimeout 去調用作為 callback function 的匿名涵式(Anonymous )來回傳每個水果,這時就可以用 let 來定義變數 i,那為什麼不用 var 去定義呢?
註:匿名涵式(Anonymous ),也就是不具名的函式,至於為什要用呢,方便啦!因為不用想名稱,通常用在重複執行的邏輯內,但這也會造成維護困難,畢竟每個東西都用匿名的方式,就不知道 error 從哪邊跑出來的了,我們將會在未來做更詳細的解說。
const fruits = ["apple", "orange", "banana"];
function fruitName(fruit) {
for (var i = 0; i < fruit.length; i++) {
setTimeout(function(){
console.log(fruit[i-1])},1000);
}
}
fruitName(fruits); // banana banana banana
他就只會輸出 i=3 時的那個值。
這是因為 var 的特性可以一直被重複定義,每次迴圈賦值時都是指定到同一個變數 i,代表他的值會一直不斷地被覆蓋,所以永遠都只有一個變數 i,加上我們使用 setTimeout 讓變數 i 沒辦法及時輸出,當 for loop 跑完時,i 已經變成 3 了,也就造成了每次出來的結果都會是同一個,這就很像我們前面講的全域命名汙染。
那為什麼使用 let 就會正確呢,這是因為 let 定義的變數,只能在那個區塊內被存取以及改變,所以每次 for loop 時的流程就會像這樣:
{
i=0
setTimeout(function(){
console.log(fruit[0])},1000);
}
},
{
i=1
setTimeout(function(){
console.log(fruit[i])},1000);
}
}
{
i=2
setTimeout(function(){
console.log(fruit[2])},1000);
}
}
因為 let 所定義的 i 在各自的區塊{}內,而每一層迴圈都是各自的區塊{},let 所定義的變數只能在區塊內被存取、改變,所以各個變數 i 彼此不會互相影響,所以會有多個變數 i,因此會跑出不同的結果。
至於為什麼要搞得這麼複雜呢,不能直接 console.log 嗎,這樣你們才會看出區別阿。
主要是因為 js 是非同步的,有時候你的資料沒有這麼即時傳回來,這時執行的話就會出錯,當然也有很多種方式可以改善,像是立即執行函式(IIFEs)、閉包(closure)等等的方式,但這樣的話可讀性就不高了,只是改個宣告方式就可以改善,不用嗎。
註:立即執行函式(IIFEs)、閉包(closure)等等的就不多做贅述了,篇幅已經太長了嗚嗚,我們會在未來的幾篇內做詳細解釋。
就如同上面講的一樣,var 是保護性最弱的宣告方式,但是他有 let 和 const 都沒有的特性提升(hosting),舉例來說:
console.log(a) // undefined
var a='123'
也就是他可以在還沒被宣告之前就先呼叫它,在 js 的理解裡它是長這樣:
var a
console.log(a) // undefined
a='123'
這邊需要注意一點,hosting 做的只有提前幫你定義你的變數,給值並不會提前。
而let 或 const 這樣做的話就會出現 ReferenceError
console.log(a,b) //ReferenceError
let a
const b='b'
其實這蠻合理的,因為 const 和 let 的命名規則相對 var 來說嚴格很多,這也可以防止前面一直講的全域變數汙染,所以當你使用一個還沒被宣告的變數時,請記得一定要將他用 var 宣告,不然就慘啦。
今天這篇蠻長的,如果有人看到最後,你很棒。
我是 Chris,在看完今天這篇之後你可以翻上去看一下上面提到的 5 個問題,因該有不少都有解答了,明天我們將會詳細的介紹這些問題,順便帶到一些其他的觀念,我們明天見!