###### tags: `JavaScript` `Variable`
# [week 16] JavaScript 進階 - 關於變數與資料型態
> 本篇為 [[JS201] 進階 JavaScript:那些你一直搞不懂的地方](https://lidemy.com/p/js201-javascript) 這門課程的學習筆記。如有錯誤歡迎指正!
```
學習目標:
瞭解 JavaScript 有哪些的資料型態
原始型態與物件型態在變數宣告賦值上的差異
```
## JavaScript 資料型態
在第二週 [[JS101] 用 JavaScript 一步步打造程式基礎](https://lidemy.com/p/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(可變的)。這裡指的不可變動不是「賦值」,而是不能改變原本的記憶體位置。
也就是說,若對其有任何變更(例如:新增、修改、刪除),就會回傳一個新值。以下列程式碼為範例:
- 原始型態:不改變原本的值
```javascript=
var str = 'hello'
var newStr = str.toUpperCase()
console.log(str, newStr)
// 印出 hello HELLO
```
- 物件型態:改變原本的值
```javascript=
var arr = [1]
arr.push(2)
console.log(arr)
// 印出 [1, 2]
```
## `typeof <value>`:用來判斷變數型態
我們可使用 `typeof` 來判斷變數的資料型態,輸入結果會回傳一個字串,語法範例如下:
```javascript=
console.log('typeof true', typeof true)
//輸出 typeof true boolean
```
結果得到 true 的資料型態是 boolean。
接著我們再看看其他範例結果:
![](https://i.imgur.com/rT6DR1N.png)
由結果可知,array 和 null 也屬於 object 型態,但前面不是說 null 的屬於原始型態嗎?這其實是 JavaScript 的歷史 bug,詳細內容可查閱下方參考資料:
> null 使用 typeof 運算子,回傳的結果會是字串 "object",這指出 null 可被認為是象徵「無物件」(no object)的一種特殊物件值。(參考資料:[犀牛書](https://www.tenlong.com.tw/products/9789862764411))
> 這其實是 JavaScript 最初發現的一個錯誤,然後被 ECMAScript 沿用至今。現在,null 被認為是物件的佔位符,從而解釋了這一矛盾。
> (參考資料:[你懂JavaScript 嗎?#4 型別(Types)](https://ithelp.ithome.com.tw/articles/10200841))
以下是在 [MDN 網站](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/typeof) 列出 typeof 的可能回傳值:
![](https://4.bp.blogspot.com/-dBndQxibqJ8/V3xngW8lK5I/AAAAAAAAnDU/L5PVWe-8j-gL3vzcA4xMznzsgqq4AcnkACLcB/s1600/1.png)
### 利用 `typeof` 確認變數是否有使用到
我們還可以利用 `typeof` 來確認某個變數是否有使用到(是否有被宣告),以下列程式碼為例:
```javascript=
var a
console.log(typeof a)
// 宣告 a 但還沒賦值,所以結果是 undefind
```
若沒有先宣告變數 a,直接使用 `typeof` 檢查也會得到相同結果:
```javascript=
console.log(typeof a)
// undefind
```
若應用在判斷句,在有宣告變數 a 的情況:
```javascript=
var a = 10
if (typeof a !== 'undefined') {
console.log(a)
}
// a = 10,所以印出 10
```
在不宣告變數 a 的情況下,直接利用 `typeof` 進行判斷:
```javascript=
if (typeof a !== 'undefined') {
console.log(a)
}
// 因為 a 是 undefined ,不符合判斷句,不會印出任何東西
```
若直接判斷變數 a 是否等於 undefined,就會出現未定義 a 的錯誤:
```javascript=
if (a !== 'undefined') {
console.log(a)
}
// 因為 a 不存在,會印出錯誤訊息:ReferenceError: a is not defined
```
因此,若使用 typeof() 來判斷 a 是否為 undefined,就能夠避免出現錯誤,導致程式中斷。
## `Array.isArray()`:判斷變數是否為陣列
若想檢查是否為陣列,可使用函式 `Array.isArray()`,檢查傳入的值是否為一個 Array,範例如下:
```javascript=
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` 還要準確,尤其物件型態會顯示更詳細的類別。
語法範例如下:
```javascript=
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 的等號真正的理解變數](https://github.com/heidiliu2020/this-is-codediary/blob/master/week2_JavaScript%20%E5%9F%BA%E7%A4%8E.md#%E5%BE%9E-object-%E7%9A%84%E7%AD%89%E8%99%9F%E7%9C%9F%E6%AD%A3%E7%9A%84%E7%90%86%E8%A7%A3%E8%AE%8A%E6%95%B8)」中我們也曾提到相關概念。
若將變數視為一個箱子,在放入數字的情況下,兩者會相等:
```javascript=
var a = 30
console.log(a === 30)
// 印出 true,兩者相等
```
但如果在變數 obj 裡放入物件,結果卻是不相等:
```javascript=
var obj = {
a:1
}
console.log(obj === {a:1})
// 印出 false,兩者不相等
```
可想像成是「記憶體位置不同」導致的結果。儘管兩個箱子儲存的數值相同,但因記憶體位置不同,指向的元素不同,所以不會相等。
如下方示意圖:
![](https://i.imgur.com/ZAY1PrG.png)
### 關於 `=` 等號賦值
如果換成下列情形,也就是將 obj 賦值給 obj2 時:
```javascript=
var obj = {
a:1
}
var obj2 = obj
console.log(obj === obj2) // 印出 true
```
兩者理所當然會相等,此時若以 `obj2.a = 2` 更改 obj2 物件中 a 的值,會連同 obj 的值也一起更動:
```javascript=
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),也就是指向同一個物件:
![](https://i.imgur.com/cgfJMmd.png)
但如果以 `obj2 = {b:1}` 將 obj2 賦值一個新的物件,此時就會指向一個新的記憶體位置。以下方程式碼為例:
```javascript=
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 不相等,這是因為「往裡面放東西」與「改放全新的東西」是兩件完全不同的事情。後者會指向一個新的記憶體,可參考下圖理解:
![](https://i.imgur.com/JkzALWA.png)
若以陣列為例,會得到相同的結果,以下列程式碼為例:
```javascript=
var arr = []
var arr2 = arr
console.log(arr, arr2)
// 印出 [] []
arr2 = ['arr2'] // 賦值,指向新的記憶體位置
console.log(arr, arr2)
// 印出 [] ['arr2']
```
賦值後的 arr2 會指向新的記憶體位置,因此兩者的值會不相同,可想像成 `arr: 0x10` 和 `arr: 0x20`。
### `==` 與 `===` 的差別
- `=`:代表賦值
- `==` 和 `===`:均用來判斷是否相等,差別在於是否判斷值的型態。原因是 `==` 判斷過程會進行型態轉換
> 結論:盡量使用三個等號進行判斷,如此最能夠避免因型態不同而發生錯誤。
以下列程式碼為例:
```javascript=
console.log(0 == '0')
// true
console.log(0 === '0')
// false,因為數字和字串型態不同
```
再以陣列作為範例:
```javascript=
var arr = [2]
var arr2 = [2]
console.log(arr === arr2)
// false
```
之所以兩者不會相等,和前面提到的「記憶體位置不同」有關,可想像成:
```
0x01: [2]
0x02: [2]
arr: 0x01
arr2: 0x02
```
也因此,不管裡面放相同參數或均為空陣列,兩者都不會相等,一定要加上 `arr2 = arr` 才會使等號成立。
同理,當我們比較空陣列或空物件時,結果也不會相等,因為比較的是兩者的記憶體位置:
```javascript=
console.log([] === [])
// false
console.log({} === {})
// false
```
### 特例:NaN
- NaN:Not a Number(無效的數字),型態為 Number
在什麼樣的情況下會產生 NaN 呢?以「將字串轉換成 Number」為例,因無法轉換所以會得到 NaN:
```javascript=
var a = Number('hello')
console.log(a)
// NaN
console.log(typeof a)
// number
```
這時如果再以等號進行判斷,結果會是:
```javascript=
var a = Number('hello')
console.log(a === a)
// false
```
> 什麼~~~~~?!同一個變數結果竟然會不相等?!(震驚ing)
為何會發生自己不等於自己的情況呢?這是因為 `NaN === NaN` 判斷結果不相等造成,屬於特殊案例。
至於要如何檢視變數是否為 NaN,可使用函式 `isNaN()`:
```javascript=
var a = Number('hello')
console.log(isNaN(a))
// true
```
- 參考資料:[JS Comparison Table](https://dorey.github.io/JavaScript-Equality-Table/)
### let 與 const
接著談到宣告變數的方式,除了習慣使用的 var(variable 變數),在 ES6 還引入了 let 和 const(constant 常數) 兩種宣告方式。這三者之間最大差別,主要在於作用域不同,在之後的 Scope 章節會再詳細說明。
#### const
- 宣告時就要給初始值
- 宣告後就不能再改變
但我們可以去改變物件 obj 裡面的值,如以下範例:
```javascript=
const obj = {
number: 1
}
obj.number = 2
```
如果直接賦予 obj 新的值,就會出現錯誤:
```javascript=
const obj = {
number: 1
}
obj = {number: 2}
// TypeError: Assignment to constant variable.
```
這和前面提到的記憶體位置觀念相同,const 說的不能改變,其實是不能改變「該記憶體位置」。obj 是存記憶體位置,number 則是存 value。也因此賦值給 obj 就代表改變記憶體位置。
```javascript=
0x10: {
number: 1
}
0x20: {
number: 2
}
obj: 0x20
// 常數 obj 的記憶體位置被改變,所以出現錯誤
```
參考資料:
- [深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?](https://blog.huli.tw/2018/06/23/javascript-call-by-value-or-reference/)