# 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 ]
```
### 小結
箭頭函式就是函式運算式(匿名函式)的懶人寫法。

## 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
```