# 首部曲- 那些你一直搞不懂的地方(變數, 賦值, 作用域)
###### tags: `JavaScript`
## 變數 (Variable)
### 七種資料型態
**原始型態 (primitive type)**
1. null
2. undefined
3. string
4. number
5. boolean
6. symbol (ES6)
**其他都是物件型態 (object type)**
7. object(array, function, date)
## 辨別資料型態的三種方法
## typeof
```javascript=
// 兩種寫法都 OK
console.log(typeof a)
console.log(typeof(a))
```
如果你的引數是放一個 Array,那麼它會顯示 object,這可以理解,因為 Array 被歸類為 object
但是如果你是放的是 function,則會顯示 function
```javascript=
console.log(typeof []) // object
console.log(typeof function() {}) // function
```
官方文件很棒,也直接幫我們列好[清單](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/typeof)了

由此可見 typeof 不一定能應付各種狀況,比如它就無法明確告訴開發者,一個變數裡放的是不是 Array(只會顯示成object)
### typeof 中常見的作法
```javascript=
var a
console.log(typeof a) // undefined
console.log(typeof b) // undefined
console.log(c)// c is not defined
```
在上面這個例子,一個是已宣告但未賦值的變數,另一個是從未宣告,都會顯示 undefined, 但是在未加上typeof語句的c, 會顯示`is not defined`,這樣會造成程式碼無法往下運作
我們可以利用 typeof 判斷一個變數是否==存在且被賦值==,應用在我們的判斷式
> typeof 印出來的東西是字串,所以要改為'undefined'
```javascript=
//未定義且未賦值
if (typeof d !== 'undefined') {
console.log(d); // 不會log出任何東西
}
//定義且賦值
let d = 10
if (typeof d !== 'undefined') {
console.log(d); // 就是10
}
```
## Array.isArray()
但是有些瀏覽器不支援此用法, 所以要準確使用可以用第三種
## Object.prototype.toString.call()
```javascript=
console.log(Object.prototype.toString.call(4) // [object Number]
console.log(Object.prototype.toString.call('a') // [object String]
console.log(Object.prototype.toString.call([]) // [object Array]
console.log(Object.prototype.toString.call(Null) // [object Null]
//只要看第二個值即可
```
## 原始型態不可變
primitive type 不可變,不要和**對變數重新賦值**搞混了
```javascript=
var a = 10
a = 20
console.log(a)//20 , 這是重新賦值的概念
```
```javascript=
var a = 'aaa'
a.toUpperCase()
console.log(a) // aaa
```
我們都知道在 JavaScript 中,有些內建的方法會直接改變對象的值,比如說 .push() 會直接改變對象陣列,那現在我們知道 Array 是物件型別,物件型別是可變的
相反的,原始型態不可變,所以就上述程式碼的例子來說,toUpperCase() 並不會改變 a 的值,而是會 return 一個處理過的值,所以我們通常會用一個變數去接住它,改成下面看看:
```javascript=
var a = 'aaa'
a.toUpperCase()
console.log(a) // aaa
newA = a.toUpperCase() // 重新對 a 賦值
console.log(newA) // AAA
```
結論 :
> 原始型態不可變,而物件型態可變
## 賦值
原始型別與物件型別在賦值的部分也有所不同
```javascript=
var a = 5
b = a
console.log(a, b) // 5 5
b = 10
console.log(a, b) // 5 10
//需注意的是a不會因為b改動而變動
```
```javascript=
var objA = { a : 5 }
var objB = objA
console.log(objA.a, objB.a) // 5 5
objB.a = 10
console.log(objA.a, objB.a) // 10 10
```
這是為什麼呢?
原因是因為 objA 與 objB 本身其實是記憶體位置,更正確地說,它用來存放物件的記憶體位置。
當我們宣告 objB 的並讓 objB = objA 時,並不是各自有各自 { a : 5 },而是 objA 與 objB 都存了相同的記憶體位置 (你可以想像成是住址),而這個地址都指向同一個 { a : 5 }
如果覺得很難記,可以試試看某個前輩舉的範例:想像物件是根吸管,那麼 objA 與 objB 就是兩根不同的吸管,插在同一杯飲料上,你不管從哪一根吸管喝,都是同一杯飲料。
所以我們可以了解上述的例子:我改變了 objB.a 的值,實際上也是直接改變 objA 所指向的 { a : 5 },因為 objA 與 objB 所指向的都是一樣的 (var objB = objA)
P.S. 這樣的情況也適用於 Array
**最後來談個很重要的狀況**
上面的案例並不難理解,那請再思考一下以下這個案例:
```javascript=
var objA = { a : 5 }
var objB = objA
console.log(objA.a, objB.a) // 5 5
objB = { a : 80 }
console.log(objA.a, objB.a) // 5 80
```
這邊你可能會疑惑,為什麼第二次的 console.log(objA.a, objB.a) 是 5 與 80,這邊簡單說明一下。
當我設定 objB = { a : 80 } 的時候,實際上並非修改原本的 { a : 5 },而是創造了一個新的物件型別 { a : 80 } 再放入 objB 之中
實際上,如果你在 = 右邊放置的是一個 {} 或 [],而不是單純 number 或 String 等原始型別,那麼你就等於創建一個新的物件型別,而其當然也有一個新的記憶體位置。(因為 {} 與 [] 都是物件型別)
如果有點困惑,不如你可以這樣想想看:為什麼 objA 與 objB 是物件 ? 究其原因就是因為我們所賦的值是物件型別 {},而不是因為這個變數的名稱叫做 Obj,所以在最初 var objA = { a : 5 } 我們也做了一樣的事。
在 Week5 的總複習中,並沒有詳細說明為什麼會是一個新的物件與記憶體位置,答案如上。
那麼我們可以更明確地得到一個結論:
> 當給一個變數賦值物件型別時,你所賦予的是該物件的記憶體位置,而非物件本身。
這樣就更好理解 var objB = objA 的案例了。
## == 與 ===
看以下例子:
```javascript=
console.log(1 == '1') // true
console.log(1 === '1') // false
```
`===` 比 `==` 多比較了型態,如果僅僅用 `==`,很多狀況會是通的,比如說 `console.log([] == 0)` 為 true。
為了避免難以預期的錯誤,建議都使用 `===`
另外延續上述討論賦值的內容
```javascript=
var objA = { a : 5 }
var objB = objA
console.log(objA.a, objB.a) // 5 5
objB.a = 10
console.log(objA.a, objB.a) // 10 10
```
既然我們已經知道,若賦值給變數的這個「值」是**物件型別**,那麼賦予的將不會是物件型別本身,而是該**物件型別的記憶體位置**,所以下列例子應該就不用多作解釋了。
```javascript=
var objA = {
a: 1
}
var objB = objA
objB.number = 10
console.log(objA === objB) // true
```
陣列也是相同道理
```javascript=
var arr1 = [1]
var arr2 = [1]
console.log(arr1 === arr2) // false
```
```javascript=
var arr1 = [1]
var arr2 = arr1
console.log(arr1 === arr2) // true
```
```javascript=
console.log([] === []) // false
console.log({} === {}) // false
```
另外這邊來介紹一下 `NaN`,它的意思其實是「 Not a number」,以下列例子舉例它會出現的其中一個原因:
```javascript=
var a = Number('ajdoajwo')
console.log(a) // NaN
console.log(typeof a) // number
console.log(a === a) // false
console.log(isNaN(a))// true
```
首先,`NaN` 本身屬於 number 型態,因此如果一個變數的型態是 number,其實你沒有辦法去辨識它到底真的是數字或是 `NaN`,不過也沒有這個必要就是了 XD
如果真的有必要,你可以使用 `isNaN()` 這個函式,它會回傳 Boolean
另外可以看到 `console.log(a === a)` 為 false,這無法用邏輯解釋,這是一個特殊案例,記得就好。
> == or === ? 請參考: [JavaScript equality table](https://dorey.github.io/JavaScript-Equality-Table/)
## 宣告變數新方式:let 與 const
- let
我們在 Scope 章節會介紹
- const (constant)(常數)
意思就是不能改變的數字,意思就是你**無法對其重新賦值**
```javascript=
const a = 20
a = 50
```
node.js 執行會顯示
> Assignment to constant varible.
另外一個例子
```javascript=
const a
a = 50
```
同樣顯示
> Assignment to constant varible.
所以可以理解成用 `const` 宣告變數的話,在最初就要給予初始值,因為後面的賦值都不會奏效
另外就是延續對於物件型別的討論,看下列例子
```javascript=
const objA = { a : 5 }
objA.a = 50
console.log(objA) // 50
```
執行後是**成功**的,可以理解,因為 objA 本身根本沒有被改變 (沒有動到記憶體位置)
# 作用域 (Scope) - 變數的生存範圍
> 作用域意指變數的生存範圍,所以焦點仍是放在變數之上
## Scope
在 ES6 之前的作用域:
```javascript=
function test() {
var a = 10
console.log(a)
}
test() // 10
console.log(a) // a is not defined
```
在 ES6 以前,**只有 function 可以產生一個新的作用域**,而其實一個 .js 檔案你可以想成是一個大的 function
從這邊可以理解,程式是以**變數宣告的位置**判定該變數所屬之作用域,而 `a is not defined` 則代表對於全域而言,`a` **根本就是不存在的**
(全域: global,代表最外圍,在此宣告的變數我們稱為「全域變數」)
接著看看下列例子:
```javascript=
var a = 10
function test()
console.log(a)
}
test() // 10
console.log(a) // 10
```
`test()` 與 `console.log(a)` 都成功輸出,後者可以理解,但前者之所以可以輸出,是因為**內層的作用域可以拿到外層作用域的變數**,而全域變數因為在最外層,因此是不論是在多內層的作用域,都一定可以拿得到這個變數
但是對於 test() 而言,它是先在 function test(){} 內中找尋有無 a 這個變數,若沒有,才再往上一層找,以此類推。所以底層其實是有一個運作機制的,這個機制稱為 `Scope chain`,這邊留待後續說明。
那下面例子就很好理解為何 test() 是 50 了
```javascript=
var a = 10
function test()
var a = 50
console.log(a)
}
test() // 50
console.log(a) // 10
```
==重要==
這邊要講解一個重要的部分,那就是基於 JavaScript 的特性,如果一個作用域 (不管是否為全域),從本身一直往外找到盡頭都找不到它所要的變數,那麼 **JavaScript 就會在最盡頭的作用域(也就是全域)自動宣告一個變數為己所用**
```javascript=
// step1
function test()
a = 50
console.log(a)
}
test()
console.log(a)
```
```javascript=
// step 2
var a // 自動宣告一個
function test()
a = 50
console.log(a)
}
test() // 50
console.log(a) // 50
```
其實這樣寫並不好,因為 a 本身是全域變數,很容易被其他作用域修改
最後我們來看一個特殊的例子
```javascript=
var a = 'global'
function test(){
var a = 'haha'
inner()
}
function inner(){
console.log(a)
}
test() // global
```
會印出 global,原因是因為 inner 的外一層是全域,而非 function test(){ },那麼這一題的重點在於對於作用域的判斷,**作用域的判斷與函式在哪裏呼叫是完全無關的**,所以儘管 inner 是在 test 內被呼叫,實際上 inner 作用域往外層找也是直接找向外層的全域
所以結論是,判斷作用域外層的方法該**以函式的宣告區域為判準**,而非呼叫函式所在的位置
而與這個機制相反,也就是會因為**呼叫函式的位置不同**而產生不同結果的則是 `this`,這邊之後會討論
## let 與 const 的生存範圍
在上述有談到,作用域範圍是以 function 來劃分,只要有 function 就會產生一個新的作用域。
```javascript=
function test()
var a = 10
if (a === 10) {
var b = 20
}
console.log(b)
}
test() // 20
```
這個例子之中,由於 console.log(b) 與 var b 都存在於同一個 function,所以也成功印出了 b,這邊沒問題,繼續討論。
然而,在 ES6 之後(含 ES6),由於引進了 let 與 const,對於作用域的定義又有了新的界定,也就是`變數的宣告方式`決定了作用域範圍
- let
剛剛的例子,我們把 var 改成 let
```javascript=
function test()
var a = 10
if (a === 10) {
let b = 20
}
console.log(b)
}
test() // b is not defined
```
可以看到由 `let` 宣告的變數 b,在 `if(){}` 之外無法被拿到,原因是因為**由 let 宣告的變數,其生存範圍僅在離自己最近的 block 以內**,而`const` 也適用於這套規則
關於 block 的定義,包括了 `function(){ ... }`,`for(){ }`,`if(){ }` 等等都是,下例以 `for(){ }` 做例子
```javascript=
function test()
for( var i = 0; i < 10; i++) {
console.log('i : '+ i)
}
console.log('final : ' + i)
}
test()
/* 印出
i : 0
i : 1
i : 2
i : 3
i : 4
i : 5
i : 6
i : 7
i : 8
i : 9
final : 10
*/
```
可以理解,因為都在同一個 function,所以 console.log('final : ' + i) 可以取得 i 的值
那如果改成 let,那就會是
```javascript=
function test()
for( let i = 0; i < 10; i++) {
console.log('i : '+ i)
}
console.log('final : ' + i)
}
test()
/* 印出
i : 0
i : 1
i : 2
i : 3
i : 4
i : 5
i : 6
i : 7
i : 8
i : 9
final : i is not defineed
*/
```
而在 eslint 套件也會強迫我們將 for 迴圈的條件宣告改為 let
所以我們可以得到下列結論 :
- var 是屬於 `function scope`
- let、const `屬於 block scope`