---
# System prepended metadata

title: '首部曲- 那些你一直搞不懂的地方(變數, 賦值, 作用域)'
tags: [JavaScript]

---

# 首部曲- 那些你一直搞不懂的地方(變數, 賦值, 作用域)

###### 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)了

![](https://github.com/ClayGao/My-study/raw/master/Lidemy/week17/img/2.jpg)

由此可見 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`
