本篇為 [JS201] 進階 JavaScript:那些你一直搞不懂的地方 這門課程的學習筆記。如有錯誤歡迎指正!
學習目標:
瞭解 JavaScript 有哪些的資料型態
原始型態與物件型態在變數宣告賦值上的差異
JavaScript 資料型態
在第二週 [JS101] 用 JavaScript 一步步打造程式基礎 學習JavaScript 基礎時,我們就曾提及關於值的型態,以及該如何判斷資料型態。
而資料型態的不同,可能會造成一些操作結果與想像不符,這部分我們後面會進行討論。
關於值的型態,大致可分為原始型態和物件型態兩種:
原始型態(Primitive types)
- boolean(真偽值):ture 和 false
- number(數字):例如 1、3.14159、NaN(無效的數字)
- string(字串):例如
'Hello World'
- symbol(ES6):例如 Sym
- null:沒有值存在(no value)
- undefined:值不存在(absence)
其他都屬於物件型態(Object types)
- object(物件):例如 {name: heidi, number: 99}
- array(陣列):例如 [1, 2, 3]
- function(函式)
- date…etc
Immutable 與 Mutable
其中原始型態具有 Immutable(不可變動)的特性,相對於物件型態是 Mutable(可變的)。這裡指的不可變動不是「賦值」,而是不能改變原本的記憶體位置。
也就是說,若對其有任何變更(例如:新增、修改、刪除),就會回傳一個新值。以下列程式碼為範例:
- 原始型態:不改變原本的值
var str = 'hello'
var newStr = str.toUpperCase()
console.log(str, newStr)
// 印出 hello HELLO
- 物件型態:改變原本的值
var arr = [1]
arr.push(2)
console.log(arr)
// 印出 [1, 2]
typeof <value>
:用來判斷變數型態
我們可使用 typeof
來判斷變數的資料型態,輸入結果會回傳一個字串,語法範例如下:
console.log('typeof true', typeof true)
//輸出 typeof true boolean
結果得到 true 的資料型態是 boolean。
接著我們再看看其他範例結果:
由結果可知,array 和 null 也屬於 object 型態,但前面不是說 null 的屬於原始型態嗎?這其實是 JavaScript 的歷史 bug,詳細內容可查閱下方參考資料:
null 使用 typeof 運算子,回傳的結果會是字串 “object”,這指出 null 可被認為是象徵「無物件」(no object)的一種特殊物件值。(參考資料:犀牛書)
這其實是 JavaScript 最初發現的一個錯誤,然後被 ECMAScript 沿用至今。現在,null 被認為是物件的佔位符,從而解釋了這一矛盾。
(參考資料:你懂JavaScript 嗎?#4 型別(Types))
以下是在 MDN 網站 列出 typeof 的可能回傳值:
利用 typeof
確認變數是否有使用到
我們還可以利用 typeof
來確認某個變數是否有使用到(是否有被宣告),以下列程式碼為例:
var a
console.log(typeof a)
// 宣告 a 但還沒賦值,所以結果是 undefind
若沒有先宣告變數 a,直接使用 typeof
檢查也會得到相同結果:
console.log(typeof a)
// undefind
若應用在判斷句,在有宣告變數 a 的情況:
var a = 10
if (typeof a !== 'undefined') {
console.log(a)
}
// a = 10,所以印出 10
在不宣告變數 a 的情況下,直接利用 typeof
進行判斷:
if (typeof a !== 'undefined') {
console.log(a)
}
// 因為 a 是 undefined ,不符合判斷句,不會印出任何東西
若直接判斷變數 a 是否等於 undefined,就會出現未定義 a 的錯誤:
if (a !== 'undefined') {
console.log(a)
}
// 因為 a 不存在,會印出錯誤訊息:ReferenceError: a is not defined
因此,若使用 typeof() 來判斷 a 是否為 undefined,就能夠避免出現錯誤,導致程式中斷。
Array.isArray()
:判斷變數是否為陣列
若想檢查是否為陣列,可使用函式 Array.isArray()
,檢查傳入的值是否為一個 Array,範例如下:
var a = [1, 2, 3];
console.log(Array.isArray(a))
// true
console.log(Array.isArray([]))
// true
但使用時須注意,一些較舊的瀏覽器並不支援 Array.isArray()
這個語法,因此更推薦的方法如下。
Object.prototype.toString
:用來判斷型態
Object.prototype.toString
是另一種判斷型態的方式,結果也會比 typeof
還要準確,尤其物件型態會顯示更詳細的類別。
語法範例如下:
console.log(Object.prototype.toString.call(null))
console.log(Object.prototype.toString.call([]))
console.log(Object.prototype.toString.call(1))
console.log(Object.prototype.toString.call(new Date))
// [object Null]
// [object Array]
// [object Number]
// [object Date]
等號賦值與記憶體位置
在課程第二週 JS101 的「從 Object 的等號真正的理解變數」中我們也曾提到相關概念。
若將變數視為一個箱子,在放入數字的情況下,兩者會相等:
var a = 30
console.log(a === 30)
// 印出 true,兩者相等
但如果在變數 obj 裡放入物件,結果卻是不相等:
var obj = {
a:1
}
console.log(obj === {a:1})
// 印出 false,兩者不相等
可想像成是「記憶體位置不同」導致的結果。儘管兩個箱子儲存的數值相同,但因記憶體位置不同,指向的元素不同,所以不會相等。
如下方示意圖:
關於 =
等號賦值
如果換成下列情形,也就是將 obj 賦值給 obj2 時:
var obj = {
a:1
}
var obj2 = obj
console.log(obj === obj2) // 印出 true
兩者理所當然會相等,此時若以 obj2.a = 2
更改 obj2 物件中 a 的值,會連同 obj 的值也一起更動:
var obj = {
a:1
}
var obj2 = obj // 賦值
obj2.a = 2
console.log('obj', obj2) // obj { a: 2 }
console.log('obj2', obj2) // obj2 { a: 2 }
console.log(obj === obj2) // 印出 true,兩者相等
之所以 obj 的值也一起被更改,是因為 obj 和 obj2 指向了相同記憶體位置(0x01),也就是指向同一個物件:
但如果以 obj2 = {b:1}
將 obj2 賦值一個新的物件,此時就會指向一個新的記憶體位置。以下方程式碼為例:
var obj = {
a:1
}
var obj2 = obj
obj2.a = 2
obj2 = {b:1}
console.log('obj', obj2) // obj { a: 2 }
console.log('obj2', obj2) // obj2 { b: 1 }
console.log(obj === obj2) // 印出 false,兩者不相等
會發現 obj2 和 obj 不相等,這是因為「往裡面放東西」與「改放全新的東西」是兩件完全不同的事情。後者會指向一個新的記憶體,可參考下圖理解:
若以陣列為例,會得到相同的結果,以下列程式碼為例:
var arr = []
var arr2 = arr
console.log(arr, arr2)
// 印出 [] []
arr2 = ['arr2'] // 賦值,指向新的記憶體位置
console.log(arr, arr2)
// 印出 [] ['arr2']
賦值後的 arr2 會指向新的記憶體位置,因此兩者的值會不相同,可想像成 arr: 0x10
和 arr: 0x20
。
==
與 ===
的差別
=
:代表賦值==
和===
:均用來判斷是否相等,差別在於是否判斷值的型態。原因是==
判斷過程會進行型態轉換
結論:盡量使用三個等號進行判斷,如此最能夠避免因型態不同而發生錯誤。
以下列程式碼為例:
console.log(0 == '0')
// true
console.log(0 === '0')
// false,因為數字和字串型態不同
再以陣列作為範例:
var arr = [2]
var arr2 = [2]
console.log(arr === arr2)
// false
之所以兩者不會相等,和前面提到的「記憶體位置不同」有關,可想像成:
0x01: [2]
0x02: [2]
arr: 0x01
arr2: 0x02
也因此,不管裡面放相同參數或均為空陣列,兩者都不會相等,一定要加上 arr2 = arr
才會使等號成立。
同理,當我們比較空陣列或空物件時,結果也不會相等,因為比較的是兩者的記憶體位置:
console.log([] === [])
// false
console.log({} === {})
// false
特例:NaN
- NaN:Not a Number(無效的數字),型態為 Number
在什麼樣的情況下會產生 NaN 呢?以「將字串轉換成 Number」為例,因無法轉換所以會得到 NaN:
var a = Number('hello')
console.log(a)
// NaN
console.log(typeof a)
// number
這時如果再以等號進行判斷,結果會是:
var a = Number('hello')
console.log(a === a)
// false
什麼~~~~~?!同一個變數結果竟然會不相等?!(震驚ing)
為何會發生自己不等於自己的情況呢?這是因為 NaN === NaN
判斷結果不相等造成,屬於特殊案例。
至於要如何檢視變數是否為 NaN,可使用函式 isNaN()
:
var a = Number('hello')
console.log(isNaN(a))
// true
- 參考資料:JS Comparison Table
let 與 const
接著談到宣告變數的方式,除了習慣使用的 var(variable 變數),在 ES6 還引入了 let 和 const(constant 常數) 兩種宣告方式。這三者之間最大差別,主要,在於作用域不同,在之後的 Scope 章節會再詳細說明。
const
- 宣告時就要給初始值
- 宣告後就不能再改變
但我們可以去改變物件 obj 裡面的值,如以下範例:
const obj = {
number: 1
}
obj.number = 2
如果直接賦予 obj 新的值,就會出現錯誤:
const obj = {
number: 1
}
obj = {number: 2}
// TypeError: Assignment to constant variable.
這和前面提到的記憶體位置觀念相同,const 說的不能改變,其實是不能改變「該記憶體位置」。obj 是存記憶體位置,number 則是存 value。也因此賦值給 obj 就代表改變記憶體位置。
0x10: {
number: 1
}
0x20: {
number: 2
}
obj: 0x20
// 常數 obj 的記憶體位置被改變,所以出現錯誤
參考資料:
- 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?