從資料型態談起
在講變數前,先來看一下在 JavaScript 中,變數的型態可能有哪幾種。
在 JavaScript 中有七種資料型態:
- null
- undefined
- string
- number
- boolean
- symbol (ES6 引進)
- object(array, function, date…)
前六種我們又可稱為原始型別(primitive type),除了此之外都是物件(object)。
要知道這個變數是哪類資料可以用 typeof
console.log(typeof 10)
會印出 number
console.log(typeof ‘123’)
會印出 string
但是 typeof 其實沒有你想的這麼簡單
console.log(typeof [])
會印出 object,這其實滿合理的,因為剛剛說除了那六種,其他都是 object
console.log(typeof function(){})
這裡卻會印出 function
console.log(typeof null)
這裡總該印出 null 了吧,結果卻印出 object,這是 JS 一開始被創造時就有的 bug,詳細緣由可以參考這篇:The history of “typeof null”
如果想要知道 typeof
會印出什麼東東,可以對照這裡
但其實 typeof 還有個常見的用法
先觀察以下程式碼
const a = 10
console.log(typeof a)
會印出 10,很合理
那如果沒賦值呢?
const a
console.log(typeof a)
此時會印出 undefined
那如果連宣告都沒宣告呢?
console.log(typeof a)
也會印出 undefined
那如果是這樣呢?
console.log(a)
就會出現錯誤
所以知道以上的規則之後,我們如果想要寫出一個,當 a 有值得時候印出,沒值則不做事的式子該怎麼寫呢?
var a = 10
if (a !== undefined){
console.log(a)
}
這樣寫乍看之下沒什麼錯,因為的確印出 10 了,但如果 a 沒宣告時,就會出現錯誤,所以通常我們都會寫成這樣
var a = 10
if (typeof a !== “undefined”){
console.log(a)
}
這樣就算 a 沒宣告或沒賦值時,也不會出現錯誤了。
從前面可以知道,typeof
可以幫我們檢視一個變數的型態,但若變數是一個陣列,只會印出 object,如果要檢視是否為 array 的話,要使用 Array.isArray([])
,是的話就會回傳 true。
你可能會覺得我只是要判斷個形態怎麼這麼難,沒有可以準確顯示的嗎?
可以用Object.prototype.toString.call(放入你要檢測的東西)
例如:console.log(Object.prototype.toString.call(null))
會印出[object Null]
,看後者即是答案。
可變 vs 不可變
講了這麼多,還記得開頭我們說到的 primitive type 嗎?
他跟 object type 之間很大的不同在於,原始型別是不可變的(immutable)
可是你會說不是啊,這樣不就變了嗎?
var a = 10
a = 20
這只是重新賦值。
這邊談的不可變是指我們在操作值所回傳的結果
let str = “hello”
str.toUpperCase()
console.log(str)
印出的還是 hello
所以換言之 object type 是可變的
var arr = [1]
arr.push(2)
console.log(arr)
會印出[1, 2],這就是可變的(mutable)。
而物件型別之所以可變也跟他記憶體儲存的方式有關
當我們宣告 var arr = [1]
,他會將 [1] 存於一個記憶體我們假設是 0x01
所以當我們宣告 var arr2 = arr
時,也就是將 arr2 指向 0x01
所以 arr2 做動作時就會針對 0x01 去改變
但若我們寫 var arr2 = [1, 2, 3]
因為是一個新值,因此會新開一個記憶體空間給他,假設是 0x02
所以我們再對 arr2 做動作時,就是對 0x02 去做動作而非 0x01 了,這點要特別注意!
這邊推薦 Huli 大大寫的一篇,看完可能會對這種模式有更多不同的想法也說不定:深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
== 和 ===
講到資料型態,感覺就不得不提到兩個等於和三個等於的差別了。
先看一個簡單的例子
console.log(2 == ‘2’) // true
console.log(2 === ‘2’) // false
當使用 ==
時,他會幫你轉換型別,所以這裡的數字 2 和字串 2 就會相等,但這其中機制有點複雜,就不另外講述。
而使用 ===
則是會比較型別,如果型別不同,答案絕對是 false
所以推薦在比較時,使用 ===
才不會出現非預期的 bug!
接著要提一個特別的東西叫 NaN(Not a Number)
var a = Number(‘123’)
console.log(a)
印出 123,Number 可以將數字的 string 轉為數字
但如果轉換的東西不是數字呢?
var a = Number(‘hihi’)
console.log(a)
則會印出 NaN
這時候我們判定一下他的型別 console.log(typeof a)
,會得到 number,從這裡我們可以知道 NaN 的型別竟然是 number,很有趣吧。
但你確定你真的認識 NaN 嗎,我們來比較一下
var a = Number(‘hihi’)
console.log(a === a)
本人跟本人比較一定一樣啊,為何要比?
結果竟然印出 false
這就是一個特例,無法解釋,你可以當作一個特殊規則記下來。
要判定是不是 NaN 可以用 isNaN()
var a = Number(‘hihi’)
console.log(isNaN(a))
答案會是 true
要看詳細的 == & === 比較可以參考這裡
let & const
撇開變數的作用域,var 和 let 的功能其實是很像的,所以我們先講 const。
Const 其實就是 constant(常數),也就是不變的數,所以顧名思義他的值無法被改變。
const a = 10
a = 20
console.log(a)
const 一旦被宣告,其值如果被嘗試更改,就會噴出 TypeError: Assignment to constant variable.
的錯誤訊息。
那看看另一個例子
const b = {
number: 10
}
b.number = 20
console.log(b)
log 出來竟然會印出 20,這是為什麼?不是說好的值不會變嗎?
其實這裡指的不會變的是記憶體位置,所以再看看這題,答案也就合理了。
另外,在使用 const 的時候要特別注意,除了不能重新賦值外,在宣告的時候就要給他值了。
ES6 前變數的作用域(scope)
let & const 在宣告時,跟 var 最大的不同就是作用域(scope)。
但在講哪裡不同之前,我們要先說明作用域這個東西。
因為 let & const 是 ES6 的東西,所以我們會先著重在 ES6 以前的宣告。
作用域其實就是變數的生存範圍
什麼意思?用一段程式碼來說明
function test() {
var a = 10
console.log(a)
}
test()
console.log(a)
會印出 10, a is not defined
因為在 ES6 以前,是以 function 作為一個作用域的單位,變數出了 function 就不存在。
所以上面這個 a 就在 test 這個 function 的 scope 裡,出了這個 scope ,a 就找不到。
-
那如果我們把 a 宣告在外面,我們稱為全域變數(global variable)
var a = 10
function test() {
console.log(a)
}
test()
console.log(a)
兩個都會印出 10
Test scope 裡的 console.log 在 function 裡面找不到 a,就會往外找。
-
再看一個例子
var a = 20
function test (){
a = 10
console.log(a)
}
test()
console.log(a)
會印出 10, 10
因為執行 function 時,沒有找到宣告的 a,就會往外找
找到全域有一個 a,那因為 function scope 內有 a = 10
所以會把 a 改成 10
當執行到 console.log(a)
時,就會印出 10
-
那如果做一點小改變,把 var a = 20
拿掉呢?
function test (){
a = 10
console.log(a)
}
test()
console.log(a)
也是印出 10, 10
因為如果直接對一個變數賦值,而沒有事先宣告,這個變數就會自動被宣告為全域變數。
所以就算 a 是在 function 裡,但因為他符合上面的條件,所以自動被宣告為全域變數,外面 console.log(a)
就會印出 10。
-
這種一層一層往上找,很像一條鏈的東西,我們稱為 scope chain,JavaScript 的 scope chain 在你寫好程式碼的時候就決定了,而非依照呼叫順序決定。
ES6 後變數的作用域
let & const 的作用域(scope)和 var 不同,剛提到 ES6 以前,是以 function 作為作用域的單位。
但在 ES6,當你使用 let or const 定義的變數,只要有{ }
,就會產生一個作用域;但如果用 var 宣告,還是會以 function 為一個 scope 的單位。
var : function scope
let, const : block scope
function test() {
if (10 > 5) {
var a = 10
}
console.log(a)
}
test()
此時會印出 10
但如果是 let
或是 const
function test() {
if (10 > 5) {
let a = 10
}
console.log(a)
}
test()
則會顯示 a is not defined