# ES6 ## ES6 宣告變數方式 const 與 let ### let 用來宣告一個變數像 `var` 一樣,不過 `let` 是塊級作用域, `var` 是函數作用域。 ### const `const` 用來宣告一個常數且一定要賦值,其值不能再藉由指派運算子(例如等號運算子)進行變動,否則報錯。常數名稱可以用大寫表示以利區分: ``` js //宣告常數一定要賦值 const A //Uncaught SyntaxError: Missing initializer in const declaration const PI = 3.14 PI = 20 console.log(PI) // output to be => TypeError: Assignment to constant variable. ``` 注意!當你用 `const` 宣告物件時有例外情況: ``` js const obj = { b: 10 } obj.b = 20 console.log(obj.b) // output to be 20 const arr = [1, 2, 3] arr.push(4, 5) console.log(arr) // output to be [1, 2, 3, 4, 5] ``` 因為變數存取物件(包含陣列)的方式是存取記憶體位置,原則上此 obj 物件的記憶體位置(指引)沒有被更動,被更動的是指引指向的值。 > 回顧一下: [從博物館寄物櫃理解變數儲存模型](https://medium.com/@hulitw/variable-and-frontdesk-a53a0440af3c)。 不過若你用 `const` 宣告了一個物件,當你想要修改這個物件的內容時(再將其他值指派給這個變數),JavaScript 引擎會認為你要創建新的物件,所以就會有新的記憶體位址,這樣常數就會被改變,所以會報錯: ```js const obj = {a: 1} obj = {a: 2} //Uncaught TypeError: Assignment to constant variable. //將其他值再指派給 obj 會有一個新的記憶體位址, ``` ### let 與 const 的共同特性 #### 作用域 scope `let` 與 `const` 是區塊作用域 (block scope) `{}` 包起來的區域,`var` 則是函式作用域 (function scope)。 使用 `var` 宣告變數 a 時,可用範圍在 test function 內,即便 `console.log(a)` 在 `if` block 之外仍能讀取到 a 的值: ``` js function test() { if (true) { var a = 10 } console.log(a); } test() // output to be 10 ``` 使用 `let` 或 `const` 宣告變數 a 時,可用範圍在 `if` block (大括號) 內,所以 `console.log(a)` 在離開 `if` block 的區域便無法讀取到 a 的值: ``` js function test() { if (true) { let a = 10 } console.log(a) } test() // ReferenceError: a is not defined ``` #### 仍然會提升 (Hoisting) 使用 `let` 與 `const` 宣告變數,在創造階段變數一樣會被放入記憶體中,但沒有初始化為 undefined。到了執行階段,賦值之前嘗試取用的話會發生錯誤,這一段不能取用變數的期間稱為暫時性死死區 (Temporal Dead Zone)。 #### 不會出現在全域物件 window 裡面 ```js var a = 1 const b = 1 let c = 1 console.log(window.a) //1 console.log(window.b) //undefined console.log(window.c) //undefined ``` #### 不能重複宣告 ```js let a = 1 let a = 3 //Uncaught SyntaxError: Identifier 'a' has already been declared const b = 2 const b = 3 //Uncaught SyntaxError: Identifier 'b' has already been declared ``` 因應 ES6 的出現,使用上建議不要再用 `var` 來宣告變數,優先使用 `const`,若須重新賦值再使用 `let` ,藉由限縮變數的活動範圍來減少發生錯誤的可能。 ## 作用域 scope (變數的生存範圍) + ### let 與 const 是區塊作用域 (block scope) `{} 包起來的區域` + ### var 是函式作用域 (function scope) ``` js function test() { if (true) { var a = 10 } console.log(a); } test() // output to be 10 ``` 使用 `var` 宣告變數 a 時,可用範圍在 test function 內,即便 `console.log(a)` 在 `if` block 之外仍能讀取到 a 的值。 ``` js function test() { if (true) { let a = 10 } console.log(a) } test() // ReferenceError: a is not defined ``` 使用 `let` 宣告變數 a 時,可用範圍在 `if` block (大括號) 內,所以 `console.log(a)` 在離開 `if` block 的區域便無法讀取到 a 的值。 ``` js function test() { if (true) { const a = 10 } console.log(a) } test() // ReferenceError: a is not defined ``` `const` 與 `let` 同理,都是區塊作用域 (block scope)。 ### 因應 ES6 的出現,使用上建議不要再用 `var` 來宣告變數,改用 `let` 與 `const`,限縮變數的活動範圍來減少發生錯誤的可能。 ## 模板字串 Template Literals + Template Literals 是增強版的字串表示法,Template Literals 讓你可以寫多行字串 (multi-line strings),也可以在字串中插入變數或 JavaScript 表達式。 + 用法:使用兩個反引號 (back-tick) ` `` ` 標示,而在字串中可以使用 `${ }` 語法來嵌入變數或 JavaScript 表達式。 ### 多行字串 Multi-line Strings ``` js //傳統寫法 let str = 'this is line one\n' + 'this is line two' console.log(str); /* output to be => this is line one this is line two */ // 反斜線n \n 為換行 //Template Literals 寫法 let newStr = ` this is line one this is line two` console.log(newStr) /* output to be => this is line one this is line two */ ``` ### 嵌入變數或任何表達式 ``` js //傳統寫法 let name1 = 'Jack' let age1 = 25 console.log('His name is ' + name1 + 'and he is ' + age1 + ' years old.') // output to be => His name is Jack and he is 25 years old. //Template Literals 寫法 let name2 = 'Ryan' let age2 = 23 console.log(`His name is ${name2} and he is ${age2} years old.`) // output to be => His name is Ryan and he is 23 years old. //加入表達式 //${} 中可以是任何 JavaScript expression function sayHello(name) { return `Hello ${name.toUpperCase()}!` } console.log(sayHello('Maggie')) //output to be => Hello MAGGIE! ``` ## 解構賦值 Destructuring 解構賦值 ( Destructuring Assignment)是一個在 ES6 的新特性,目的用於提取陣列或物件中的資料**變成獨立變數**。 ### 陣列解構賦值 + 傳統寫法 ``` js let arr = [1, 2, 3, 4] let first = arr[0] let second = arr[1] let third = arr[2] let fourth = arr[3] console.log(first, second, third, fourth) // 1 2 3 4 ``` + ES6 解構寫法 ``` js let arr = [1, 2, 3, 4] let [first, second, third, fourth] = arr console.log(first, second, third, fourth) // 1 2 3 4 ``` ### 其他案例 *註:陣列解構賦值會將右方的資料與左邊對應,一個位置對應一個值。* + 情況一:當變數多於所給的值 ``` js let [a , b, c, d] = [1, 2, 3] console.log(a, b, c, d) // 1 2 3 undefined ``` + 情況二:當變數少於所給的值 ``` js let [a , b, c] = [1, 2, 3, 4] console.log(a, b, c) // 1 2 3 ``` + 情況三:若遇到空的變數,這些值將被跳過 ``` js let [a, , b, c] = [1, 2, 3, 4] console.log(a, b, c) // 1 3 4 let [x, , ,y, z] = [5, 6, 7, 8] console.log(x, y, z) // 5 8 undefined ``` + 情況四:字串拆解 ``` js let str = 'Ryan' let [a, b, c, d] = str console.log(a, c) // R a ``` + 情況五:交換變數 ``` js let apple = 'red' let lemon = 'green' ;[apple, lemon] = [lemon, apple] console.log(apple, lemon) ``` > ### 防雷須知 *此內容引用自 [@PJCHENder](https://pjchender.blogspot.com/2017/01/es6-array-destructuring.html)* > 如果你使用的是 [standard JS](https://standardjs.com/readme-zhtw.html) 當作你的 code style,那麼你應該很習慣在結尾不加分號,但是在使用陣列的結構賦值時,這麼做**有可能**會發生錯誤。例如: ``` js let apple = 'red' let lemon = 'green' [apple, lemon] = [lemon, apple] console.log(apple, lemon) ``` > 這邊會得到 `ReferenceError: lemon is not defined` 的錯誤。首先簡單說明一下之所以可以不用在結尾加分號是因為在**多數情況**下,在語句或一段代碼敘述後,加了 Enter 鍵(\n)後,JS剖析器會在執行期間自動幫你插入分號。上面提到是多數情況,但是在**某些情況下** JS 引擎是不會幫你加上分號的,其中像是這裡的開頭以 `[` 開頭的語句。因此在這裡,請記得在 [ 的前面加上分號,寫起來會像是這樣 `;[apple, lemon] = [lemon, apple]`,才能避免錯誤產生。 ### 物件解構賦值 物件的解構賦值強調的是`屬性名稱`,屬性名稱必須與變數名稱相互對應才能取到值,反之則會無法取值。 + 傳統寫法 ``` js let person = { gender: 'male', firstName: 'Leonardo', lastName: 'Dicaprio' } let gender = person.gender let firstName = person.firstName let lastName = person.lastName console.log(gender) // male console.log(firstName) // Leonardo console.log(lastName) // Dicaprio ``` + ES6 解構寫法 ``` js let person = { gender: 'male', firstName: 'Leonardo', lastName: 'Dicaprio' } //一次宣告三個變數 let {gender, firstName, lastName} = person console.log(gender) // male console.log(firstName) // Leonardo console.log(lastName) // Dicaprio ``` ### 其他案例 + 情況一:變數名稱對應不到物件中的屬性名稱時,則會出現 undefined ``` js let person = { gender: 'male', firstName: 'Leonardo', lastName: 'Dicaprio' } let {gender, first, last} = person console.log(gender) //male console.log(first) //undefined console.log(last) //undefined ``` + 情況二:將變數名稱重新命名 (不想以屬性名稱當作變數的時候) ``` js let person = { gender: 'male', firstName: 'Leonardo', lastName: 'Dicaprio' } let {gender, firstName: first, lastName: last} = person console.log(gender) //male console.log(first) //Leonardo console.log(last) //Dicaprio ``` 使用冒號 `:` 後面接新的變數名稱 + 情況三:解構再解構 ``` js let person = { gender: 'male', name: { firstName: 'Leonardo', lastName: 'Dicaprio' } } let {name} = person console.log(name) //{ firstName: 'Leonardo', lastName: 'Dicaprio' } let {name: {firstName, lastName}} = person console.log(firstName) //Leonardo console.log(lastName) //Dicaprio ``` 冒號 `:` 後面再使用大括號即再解構一次 + 情況四:解構再解構再加上重新命名變數名稱 ``` js let person = { gender: 'male', name: { firstName: 'Leonardo', lastName: 'Dicaprio' } } let {name: {firstName: one, lastName: two}} = person console.log(one) //Leonardo console.log(two) //Dicaprio ``` + 解構也能使用在函式的參數定義中,使用方式如同將想要傳入的物件對應到函式參數上。這樣參數一樣能夠能夠自訂變數名稱、順序、預設值等 (預設值的用法後面會提到) ``` js function test({a, b}) { console.log(a, b) } test({ a: 1, b: 2 }) // output to be 1 2 /* 原本的寫法為: function test(obj) { console.log(obj.a, obj.b); } test({ a: 1, b: 2 }) */ ``` 更多解構使用在函式參數定義中的詳細說明,可參考此兩篇文章:[MDN 解構賦值](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) 和 [Day 08: ES6篇 - Destructuring Assignment(解構賦值)](https://ithelp.ithome.com.tw/articles/10185430?sc=pt)。 ## 加上預設值 Default Parameters ### 在解構賦值時使用預設值 將物件 obj 解構的情況: ``` js const obj = { a: 1, b: 2 } const {a, b} = obj console.log(a, b) // 1 2 ``` 若 obj 中只有 b 的時候: ``` js const obj = { b: 2 } const {a, b} = obj console.log(a, b) //undefined 2 ``` a 為 undefined,因為物件 obj 中沒有相對應的屬性名稱 a,這時候可以替 a 加上預設值: ``` js const obj = { b: 2 } const {a = 123, b} = obj console.log(a, b) //123 2 ``` 若已經有賦值,則以賦的值為優先,否則將輸出預設值 ``` js const obj = { a: 1 } const {a = 123, b = 456} = obj console.log(a, b) //1 456 ``` ### 在函數的參數定義中使用預設值 假設我們寫了一個非常簡易的 sayHi function,情況如下: ``` js function sayHi(name) { return `Hi, ${name}` } console.log(sayHi()) ////Hi, undefined ``` 替 name 加上預設值: ``` js function sayHi(name = 'Leo') { return `Hi, ${name}` } console.log(sayHi()) ////Hi, Leo console.log(sayHi('Maggie')) ////Hi, Maggie ``` ## 展開運算子 Spread Operator + 展開運算子用來展開一個陣列,轉化為多個逗點隔開的獨立參數 + 用法:使用三個點 `...` (three dots) 標示 ### 使用案例: + 用來串聯陣列 ``` js let arr1 = [1, 2, 3] let arr2 = [arr1, 4, 5, 6] console.log(arr2) //[ [ 1, 2, 3 ], 4, 5, 6 ] let arr3 = [...arr1, 4, 5, 6] console.log(arr3) //[ 1, 2, 3, 4, 5, 6 ] let arr4 = [4, 5, ...arr1, 6] console.log(arr4) //[ 4, 5, 1, 2, 3, 6 ] ``` + 可以用來複製陣列 ``` js let arr1 = [1, 2 ,3] let arr2 = [...arr1] console.log(arr2) //[1, 2, 3] console.log(arr1 === arr2) //false //arr2 是一個新陣列並不影響 arr1,但還有以下情形: let newArr = [4] let arrA = [...arr1, newArr] // console.log(arrA) //[ 1, 2, 3, [ 4 ] ] let arrB = [...arrA] console.log(arrB) //[ 1, 2, 3, [ 4 ] ] console.log(arrA === arrB) //false console.log(arrA[3] === arrB[3]) //true //arrA[3] 與 arrB[3] 還是指向同一個記憶體位置,因為 [4] 是一個陣列(物件) ``` + 可以將字串展開為各單一字串的一個陣列 ``` js let str = 'hello' let strArr = [...str] console.log(strArr) //[ 'h', 'e', 'l', 'l', 'o' ] ``` + 也可以用來把一個陣列展開,然後傳入函式作為參數值 ``` js function add(a, b, c) { return (a + b +c); } let arr = [1, 2, 3] console.log(add(...arr)) //6 ``` ## 其餘運算子 Rest Operator + 其餘運算子用來將不確定數量的參數集合起來並存成一個**陣列** + 用法:與展開運算子相同,都是使用三個點 `...` (three dots) 來標示 ### 使用案例: + 與解構搭配使用 ``` js let [first, ...rest] = [1, 2, 3, 4] console.log(first) // 1 console.log(rest) //[ 2, 3, 4 ] //可自定義使用其餘運算子的變數名稱 let [one, two, ...others] = [1, 2, 3, 4, 5, 6] console.log(others) // [ 3, 4, 5, 6 ] let [a, b, ...theRestOnes] = ['hi', 'hello', 'hey', 'hoo'] console.log(theRestOnes) //[ 'hey', 'hoo' ] //注意!其餘運算子只能放在最後一位,並且只能有一個其餘參數,否則會出現錯誤 let [five, six, ...middle, lastOne] = [5, 6, 7 ,8, 9] console.log(middle) //SyntaxError: Rest element must be last element //當右邊的值與左邊變數數量不相等時,用了其餘運算子的那個變數,就會變成一個空陣列 let [x, y, ...z] = [1] console.log(x) //1 console.log(y) //undefined console.log(z) // [] ``` + 使用在函式的參數定義中 (不確定的傳入參數值有幾個的時候),又稱「其餘參數 (Rest Parameters)」。 ``` js function sum(...others) { let total =0 for (let i=0; i<others.length; i++) { total += others[i] } return total } console.log(sum(1, 2, 3, 4, 5)) //15 //其餘參數的值在沒有傳入實際值的時候,會變為一個空陣列,而不是undefined function test(x, ...y) { console.log('x =', x, ', y =', y); } test(1, 2, 3) //x = 1 , y = [ 2, 3 ] test(1) //x = 1 , y = [] test() //x = undefined , y = [] ``` ## 小結 (懶人包) ### 展開運算子 Spread Operator: + 用來展開一個陣列,轉化為多個逗點隔開的獨立參數 ``` js let arrA = [1, 2, 3] let arrB = [...arrA, 4, 5] console.log(arrB) //[1, 2, 3, 4, 5] ``` + 傳入函式使用 ``` js function test(a, b, c) { console.log(a, b ,c) } let arr = [1, 2, 3] test(...arr) // 1 2 3 ``` ### 其餘運算子 Rest Operator: + 搭配解構賦值使用 ``` js let [a, b, ...c] = [1, 2, 3, 4, 5] ``` + 使用在函式的參數定義上 ``` js function f(...rest){} ``` ## 在 *物件* 上使用展開運算子與其餘運算子 > 此內容引用自 [[ES6-重點紀錄] 擴展運算子 Spread Operator](https://ithelp.ithome.com.tw/articles/10195477) 與 [Day 09: ES6篇: Spread Operator & Rest Operator(展開與其餘運算符)](https://ithelp.ithome.com.tw/articles/10185496) + 上面都只有談到與陣列搭配使用,但根據 [Object Rest/Spread Properties](https://github.com/tc39/proposal-object-rest-spread) 的內容,其實物件也能夠使用這個運算子 `...`,只是目前還未正式加入 ES6 的標準中。這些都是還在制定中的 ES7 之後的草案標準,稱為展開屬性 (Spread Properties) 與其餘屬性 (Rest Properties)。 + 使用方式基本上與陣列大同小異 ### 展開屬性 (Spread Properties) 使用案例: + 串聯物件 ``` js let obj1 = { a: 1, b: 2 } let obj2 = { ...obj1, c: 3 } console.log(obj2) //{ a: 1, b: 2, c: 3 } //遇到有相同屬性名的,合併後只會使用最後一個物件的內容值 let objOne = { x: 5, y: 6, z: 7 } let objTwo = { y: 10 } let newObj = { ...objOne, ...objTwo } console.log(newObj) //{ x: 5, y: 10, z: 7 } ``` + 複製物件 ``` js let obj1 = { a: 1, b: 2 } let obj2 = { ...obj1 } console.log(obj2) //{ a: 1, b: 2 } console.log(obj1 === obj2) //false ``` ### 其餘屬性 (Rest Properties) 使用案例: + 與解構搭配使用 ``` js let obj1 = { a: 1, b: 2, c: 3 } let { a, ...obj2 } = obj1 console.log(a) //1 console.log(obj2) //{ b: 2, c: 3 } 物件 //這邊一樣可以自定義使用其餘運算子的變數名 (這邊用 rest) let { ...rest } = obj1 console.log(rest) //{ a: 1, b: 2, c: 3 } console.log(rest === obj1) //false ``` ## 箭頭函式 > 此篇內容引用自 [MDN 箭頭函式](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Functions/Arrow_functions) 與 [重新認識 JavaScript: Day 10 函式 Functions 的基本概念](https://ithelp.ithome.com.tw/articles/10191549)。 箭頭函式運算式 (arrow function expression) 為**函式運算式**的簡短語法。它沒有自己的 `this`、arguments、super、new.target 等語法。本函式運算式適用於非方法的函式,但不能被用作建構式 (constructor)。 ### 基本語法: ``` (參數1, 參數2, …, 參數N) => { 陳述式 } (參數1, 參數2, …, 參數N) => { return 表示式 } //相當於 (參數1, 參數2, …, 參數N) => 表示式 // 只有一個參數時,括號才能不加: (單一參數) => { 陳述式 } 單一參數 => { 陳述式 } //若無參數,就一定要加括號: () => { statements } ``` ### 範例: ``` js const square = (n) => {return n * n} ``` 等同於函式運算式(匿名函式)的寫法: ``` js const square = function(n) {return n * n} ``` 若只有一個參數可省略括號: ``` js const square = n => {return n * n} ``` 若箭頭函式裡的 `{}` 只有一句 `return` 語句的時候,可以將 `return` 和 `{}`省略: ``` js const square = n => n * n ``` ### 其他範例: 這是一個函式運算式(匿名函式): ``` js let arr = [1, 2, 3, 4, 5] let newArr = arr.map(function (x) { return x * x }) console.log(newArr) //[ 1, 4, 9, 16, 25 ] ``` 可簡化成箭頭函式: ``` js let arr = [1, 2, 3, 4, 5] let newArr = arr.map(x => x * x) console.log(newArr) //[ 1, 4, 9, 16, 25 ] ``` ### 小結 箭頭函式就是函式運算式(匿名函式)的懶人寫法。 ![](https://i.imgur.com/JdpJOOD.png) ## import 與 export (ESM) #### 假設現在有兩個 JS 檔案在同一資料夾底下, `main.js` 與 `module.js`。 ``` js //in module.js function add(a, b){ return a + b } const pi = 3.14 ``` #### 我們想要將 `module.js` 的 `add function` 與 `pi` 輸出 (export) 出去,可以直接在前面加上 `export` 語句: ``` js //in module.js export function add(a, b){ return a + b } export const pi = 3.14 ``` #### 在 `main.js` 將 `module.js` 的 `add function` 與 `pi` 引入 (import): ``` js //in main.js import {add, pi} from './module' console.log(add(3, 5)) //8 console.log(pi) //3.14 ``` ### 其他使用方法 #### 也可以將想要輸出 (export) 的模組整合成一個區塊 `{}`: ``` js //in module.js function add(a, b){ return a + b } const pi = 3.14 export{ add, pi } ``` ``` js //in main.js import {add, pi} from './module' console.log(add(3, 5)) //8 console.log(pi) //3.14 ``` #### 更改輸出模組的名稱,使用 `as` 後面接新名稱: ``` js //in module.js function add(a, b){ return a + b } const pi = 3.14 export{ add as sum, pi } ``` ``` js //in main.js //記得引入這邊的名稱也要改成新的 import {sum, pi} from './module' console.log(sum(3, 5)) //8 console.log(pi) //3.14 ``` #### 或是直接在引入 (import) 時更改模組名稱: ``` js //in module.js function add(a, b){ return a + b } const pi = 3.14 export{ add, pi } ``` ``` js //in main.js import {add as sum, pi} from './module' console.log(sum(3, 5)) //8 console.log(pi) //3.14 ``` #### 一次引入所有模組 (當模組很多的時候),使用 `import * as [module name]`: ``` js //in module.js function add(a, b){ return a + b } const pi = 3.14 export{ add, pi } ``` ``` js //in main.js //這邊的 myModule 可自行定義任何名稱 import * as myModule from './module' console.log(myModule.add(3, 5)) //8 console.log(myModule.pi) //3.14 ``` #### export default 預設輸出的用法: > 註:一個 JS 檔案只能有一個 export default。 ``` js //in module.js export default function add(a, b){ return a + b } export const pi = 3.14 ``` ``` js //in main.js //注意這邊因為 export default 的關係,可以用任意名字 import 原先的 add function 並且不用加上大括號 //這邊沿用 add import add, {pi} from './module' console.log(add(3, 5)) //8 console.log(pi) //3.14 ------ //另一種 import 方法 import {default as add, pi} from './module' console.log(add(3, 5)) //8 console.log(pi) //3.14 ```