0%

[week 16] JavaScript 進階 - 關於變數與資料型態

本篇為 [JS201] 進階 JavaScript:那些你一直搞不懂的地方 這門課程的學習筆記。如有錯誤歡迎指正!

學習目標:

 瞭解 JavaScript 有哪些的資料型態
 原始型態與物件型態在變數宣告賦值上的差異

JavaScript 資料型態

在第二週 [JS101] 用 JavaScript 一步步打造程式基礎 學習JavaScript 基礎時,我們就曾提及關於值的型態,以及該如何判斷資料型態。

而資料型態的不同,可能會造成一些操作結果與想像不符,這部分我們後面會進行討論。

關於值的型態,大致可分為原始型態和物件型態兩種:

原始型態(Primitive types)

  1. boolean(真偽值):ture 和 false
  2. number(數字):例如 1、3.14159、NaN(無效的數字)
  3. string(字串):例如 'Hello World'
  4. symbol(ES6):例如 Sym
  5. null:沒有值存在(no value)
  6. undefined:值不存在(absence)

其他都屬於物件型態(Object types)

  1. object(物件):例如 {name: heidi, number: 99}
  2. array(陣列):例如 [1, 2, 3]
  3. function(函式)
  4. 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: 0x10arr: 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?