# 首部曲- 那些你一直搞不懂的地方(變數, 賦值, 作用域) ###### 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`