# ES6 ## ES module 以前javascript是沒有模塊體系的,後來js社區提出了三種模塊化方案,主要是CommonJS,AMD,CMD三大規範。比如nodejs的模塊系統require、webpack等都是以CommonJS規範來實現的;requirejs是基於AMD規範來實現的,seajs是基於CMD規範實現的。 但這些都不是最好的方式,於是es6提出了官方的es-module模塊化方案,成為瀏覽器和服務器通用的模塊解決方案。 es-module主要由兩個命令構成:export和import,即導出和導入。 使用es-module時,可以說一個js文件就是一個模塊,文件內的變數、函式、類需要export,然後才能在另一個模塊中使用import來引用。 ### 普通導入導出 ```javascript // test1.js // 导出 - export一个count变量 export const count = 100; // test2.js // 导入 - import text1.js 的count import { count } from './test1'; ``` ### 默認引入導出 ```javascript // test1.js // 默认导出一个count变量 const count = 100; export default count; // 使用默认导出时,不能使用变量声明表达式 // 导入时是默认的变量名`default`,你可以定义一个新的名字,或使用`default as`重命名 // test2.js import count from './test1'; 或 import anyname from './test1'; // anyname可以是任意变量名 // 等同于 import { default as count } from './test1'; 或 import { default as anyname } from './test1'; ``` >[color=#61dafb]使用默認導出,在其導入時其變數名可自定義 ### 多種引入導出 ```javascript // test1.js export const Component = {}; export default = {}; // test2.js import React,{ Component } from './test1'; // React是默认导出的{},默认导出的变量是可以自定义的 ``` ### 使用 * as 首先,在validate.js定義兩個驗證函式 ```javascript // validate.js // 验证手机号 export function checkphone(phone) { ... return phone; } // 验证email export function checkemail(email) { ... return email; } // test.js import { checkphone,checkemail } from './validate'; console.log(checkphone); console.log(checkemail); // 使用* as整體介紹 // test.js import * as validates from './validate'; console.log(validates.checkphone); console.log(validates.checkemail); ``` :::warning `* as`實際上是將導入模塊的所有值存放在一個 object 中,然後可以通過這個 object 去訪問它們 ::: ### 引入時重命名、引入普通樣式 ```javascript // test1.js export let count = 100; // test2.js 导入时将count重命名为num import { count as num } from './test1' // 引入普通樣式 import './style.css'; import styles from './style.css'; // css-module ``` ### 注意事項 1. 普通導出(export)時,可以導出物件,聲明表達式,但不能導出已聲明的變數,或者無變數聲明的表達式 >無變數聲明,某些匿名函式function(){...},class {...}等等 :::danger 總之:export 命令規定的是對外的接口,必須與模塊內部的變數建立一一對應關係。因為使用 import 命令的時候,用戶需要知道所要加載的變數名或函式名 ::: ```javascript const count = 100; export count; // 错误❌ export 100; // 错误❌ export function(){}; // 错误❌ export class {}; // 错误❌ export const count = 100; // 正确✔️ export function check(){}; // 正确✔️ export class Component {}; // 正确✔️ export { count }; // 正确✔️ ``` 2. 默認導出(export default)時,不能導出聲明表達式,class除外 這是因為 export default 命令其實只是輸出一個叫做 default 的變數,所以它後面不能跟變數聲明語句。 ```javascript export default const count = 100; // 错误❌ const count = 100; export default count; // 正确✔️ export default { count }; // 正确✔️ export default [ count ] // 正确✔️ export default function check(){}; // 正确✔️ export default class Component {}; // 正确✔️ ``` 3. import導入的變數是唯讀的 ```javascript import { a } from './xxx.js' a = {}; // 错误 Syntax Error : 'a' is read-only; a.foo = 'hello'; // 合法操作 ``` ## 解構賦值 ES6 允許按照一定模式,從陣列和物件中提取值,對變數進行賦值,這被稱為解構賦值(Destructuring)。 ### 物件解構賦值 物件屬性的結構是key:value,解構賦值時,變數名需要和屬性名 key 一致,否則會是 undefined。 ```javascript let user = { name: "ben", age: 20, gender:"男" }; // 解构赋值 const { name, age } = user; // 变量名对应的是对象属性的key console.log(name); // ben console.log(age); // 20 // 传统写法 let user = { name: "ben", age: 20, gender:"男" }; const name = user.name; const age = user.age; ``` #### 解構時重命名 物件屬性是 key:value 結構,因此在解構時,解構出來的變數名默認是該鍵名key,但我們可以重新命名它。 ```javascript let user = { name: "ben", age: 20, gender:"男" }; // 解构赋值 const { name: username , age: userage } = user; console.log(name); // 报错 console.log(age); // 报错 console.log(username); // ben 解构时已将变量名改为username console.log(userage); // 20 解构时已将变量名改为userage ``` #### 解構時設置預設值 ```javascript // 没有name的user let user = { age: 20, gender:"男" }; // 正常解构赋值 const { name } = user; console.log(name); // undefined // 设置默认值 const { name="ben" } = user; console.log(name); // ben 值不存在时使用默认值 ``` ### 複製的解構賦值 #### 基本使用 陣列的解構賦值和物件不同,陣列解構賦值對應的是鍵(key),而陣列對應的是索引(Index) ```javascript let [first, ,last] = [1, 2, 3]; console.log(first); // 1 console.log(last); // 3 ``` #### 設置預設值 ```javascript let [ first, , ,last=10 ] = [1, 2, 3]; console.log(first); // 1 console.log(last); // 10 // 该陣列第四位不存在,因此使用默认值10 ``` #### 設置多個變數 當要設置多個變數時,可使用變數解構賦值來簡化程式碼 ```javascript // 传统写法 let name = "ben"; let age = 23; let gender = "男"; // 解构赋值写法 let [ name, age, gender ] = [ "ben", 23, "男" ]; ``` ### 字串的解構賦值 ```javascript const [a, b, c, d, e] = 'hello'; console.log(a); // h console.log(b); // e console.log(e); // o ``` ### 函式的解構賦值 #### 參數是陣列 ```javascript // 原來 function add(args){ return args[0] + args[1]; } // 解構賦值 function add([x, y]){ return x + y; } add([1, 2]); // 3 ``` #### 參數是物件 ```javascript // 原來 function add(args){ return args.x + args.y; } // 解構賦值 function add({ x, y }){ return x + y; } add({ x:1, y:2 }); // 3 ``` #### 使用默認值 ```javascript // 解構賦值 function add({ x=0, y=1 }){ return x + y; } add({ x:1, y:2 }); // 3 add({ y:2 }); // 2 add(); // 1 ``` ## 物件擴展 ### 物件屬性簡寫 #### 基本使用 ```javascript // 解構賦值 let name = "ben"; let age = 23; let gender = "男"; // 赋值给user变量,使用简写的形式 const user = { name, age, gender }; // 实际上等同于: const user = { name: name, age: age, gender: gender } ``` #### 擴展運算子 ```javascript // 賦值給user變數,使用簡寫的形式 const base = { name:"ben", age:23 }; // 使用`...` const user = { ...base, gender:"男" } console.log(user); // { name:"ben", age:23, gender:"男" } ``` ### Object.keys() 物件屬性是 key:value 結構,而Object.keys()方法主要用於把物件屬性的鍵 key 轉成一個陣列。 ```javascript const user = { name:"ben", age:23, gender:"男" }; const userkey = Object.keys(user); console.log(userkey); // [ "name", "age", "gender" ] ``` ### Object.values() 和 Object.keys() 不同,Object.values 方法主要用於把物件屬性的值 value 轉成一個陣列。 > Object.values 是 ES7 中新增的方法 ```javascript const user = { name:"ben", age:23, gender:"男" }; const uservalue = Object.keys(user); console.log(uservalue); // [ "ben", 23, "男" ] ``` ### Object.assign() 這是一個物件合併的方法。從右往左合併,當遇到相同屬性時會直接覆蓋。 :::danger 這是淺層複製 ::: ```javascript const base = { name:"ben", age:23 }; // 基本信息 const expand = { age: 20, gender:"男", birthday:"1月1日" }; // 額外信息 // 物件合併 const user = object.assign(base,expand) console.log(user); // { name:"ben", age: 20, gender:"男", birthday:"1月1日" } ``` ### Object.entries() 這是一個物件屬性迭代的方法,最終產生一個二維陣列。二維陣列中的每個屬性對應物件屬性的 key 和 value。 ```javascript const user = { name:"ben", age:23}; // 物件屬性迭代 console.log(Object.entries(user)); // [ ["name","ben"],["age",23] ] ``` ### Object.is Object.is() 用於判斷兩個值是否相等,與運算符 === 等效。 ```javascript Object.is('foo', 'foo') // true Object.is({}, {}) // false ``` 如果要判斷兩個物件是否相等,建議使用 [lodash](https://lodash.com/) 的 isEqual 方法 ```javascript import _ from 'lodash'; let object = { 'a': 1 }; let other = { 'a': 1 }; _.isEqual(object, other); // => true object === other; // => false ``` ## 擴展 (展開) 拓展運算符...是在物件或陣列中展開屬性的一種語法。遵循從右往左,相同則覆蓋的原則。 ### 物件中使用 ```javascript const base = { name:"ben", age:23 }; // 通過拓展運算符'...'將base屬性在user中展開,從而合併物件。 const user = { age:20, gender: "男", ...base }; console.log(user); // { age:23, gender: "男", name:"ben"}; ``` ### React 中使用 ```javascript class UserComponent extends Component { render(){ const user = { name:"ben", age:23, gender: "男" }; return <div> <Header /> <UserPage {...user} /> {/* 等同於 */} <UserPage name="ben" age="23" gender="男" /> <Footer /> </div> } } ``` ### 陣列中使用 ```javascript const base = [ 1, 2, 3 ]; const arr = [ 0, ...base ]; // 數組合併,和concat有相同的功效 console.log(arr); // [ 0, 1, 2, 3 ] ``` ### new Set 和 ... 實現陣列去重複 ES6 提供了新的數據結構 Set。它類似於陣列,但是成員的值都是唯一的,沒有重複的值。 ```javascript const arr = [ 1,1,2,2,3,4,5,5,5 ]; const noRepeatArr = [ ...new Set(arr) ]; console.log(noRepeatArr); // [1,2,3,4,5] ``` ### 字串中使用 ```javascript const str = "hello"; const arr = [ 0, ...str ]; console.log(arr); // [ 0, "h","e","l","l","o" ] ``` ## includes includes 是 ES7 中新增的一個方法,用於在陣列或字串中判斷是否包含某個元素,如果包含就返回 true,否則就返回 false。 ### 陣列中使用 ```javascript const arr = [ 1, 2, 3 ]; console.log(arr.includes(2)); // true console.log(arr.includes(9)); // false // 傳統寫法 console.log(arr.indexOf(2)!==-1); // true console.log(arr.indexOf(9)!==-1); // false ``` ### 字串中使用 ```javascript const str = "Hello world!"; console.log(str.includes("e")); // true console.log(str.includes("z")); // false // 傳統寫法 console.log(str.indexOf("e")!==-1); // true console.log(str.indexOf("z")!==-1); // false ``` ## class 類別 ES6 中的 class 是 ES5 原型寫法的一個語法糖,擁有很好的開發體驗 ### 基本使用 ```javascript // 定義一個空的類 class Point { } // 如果class內沒有定義constructor,則默認class的constructor為空函式。 class Point { constructor() {} } ``` 生成實例,傳遞參數 ```javascript // es6寫法 class Point { // 構造函式 constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } } // 訪問 const a = new Point(1, 2); // 傳統es5寫法 function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; }; // 訪問 var p = new Point(1, 2); ``` ### 定義 class 類別屬性 #### 在 constructor 內定義 但這樣有點麻煩,因為都必須寫在 constructor 內。 ```javascript // es6寫法 class Point { constructor() { this.state={ name: null } this.getname = this.getname.bind(this); } getname(){ // do } } ``` #### 使用 class properties proposal 定義類屬性 [class properties proposal](https://github.com/tc39/proposal-class-fields) 支持省略 constructor,在類內直接定義類別的屬性,大大簡化了類別的操作。 >class properties proposal 是 ES7 新提案, 需要[@babel/plugin-proposal-class-properties插件](https://babeljs.io/docs/en/babel-plugin-proposal-class-properties)的支持。 ```javascript class Point { // 直接定義類屬性 state = { name: null } // 不再需要在constructor內綁定 getname(){ // do } } ``` #### class properties proposal 在 React 中的應用 ```javascript class App extends Component { // 直接定義類屬性state state = { name: "ben", age: 23, gender: "男" } render(){ const { name, age, gender } = this.state; return <div> <p> 當前登錄用戶: { name },{ age }歲,{ gender } </p> </div> } } ``` ### 使用 static 定義靜態方法和靜態屬性 使用static定義類的靜態方法,不需要實例化就能訪問。 ```javascript class Point { // static定義靜態方法 static getname(name){ return name; } // static定義靜態屬性 static gender = "man"; checkphone(){ // do something } } // 訪問 const P = Point.getname("ben"); // ben const gender = Point.gender; // => man ``` ### 在 class 類別內定義函式 ```javascript class Point { getname(){ return 1; } // 或使用箭頭函式 getname = () =>{ return 1; } } ``` ### class 類別的繼承 #### 基本使用 class 的繼承需要使用 extends 關鍵字 ,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。 ```javascript class A {} class B extends A { constructor() { super(); // do } } ``` #### super() :::warning 子類要繼承父類,還需要在子類的構造函式內調用 super 方法,否則會報錯。 super實際上是調用父類的構造函式,但它的this指向的是子類。 ::: :::danger super 只能用在子類的構造函式中,並且需要執行 super() 之後才能訪問 this ::: ```javascript class A {} class B extends A { constructor() { console.log(this); // 報錯 super(); // 繼承父類A的屬性和方法 console.log(this); // 正確 } } ``` #### 使用 class properties proposal 直接繼承 使用 class properties proposal 的好處是可以省略constructor的繁瑣綁定,可以直接定義且自動繼承。 ```javascript class A { state={ age:23 } } class B extends A { state = { userage: this.state.age // age已被繼承,因此可直接訪問。this指向的是B。 } } // 訪問 const P = new B(); console.log(P.state.userage); // 23 ``` ### Decorator 類別修飾器 修飾器是 ES7 新增的一個特性,主要用於修改 class 類別的行為。[proposal-decorators](https://github.com/tc39/proposal-decorators) 使用情況特殊,但以不建議使用 詳細教學參考 2019 react 入门到高级课程 - ES6 - 022 堂 :::warning 修飾器只能用於類別和類別的方法,不能用於函式。 ::: 老師建議: :::warning 隨著 react hook 的推出,開發者將更偏向於函式組件,而類組件將會被更少地使用,而修飾器主要應用於類,因此往後修飾器的使用也會減少。 因此不建議大量地使用修飾器,或者可以**不使用它**。 ::: ## Promise Promise 是異步編程的一種解決方案。 Promise是一個原生構造函式,通過 new 可生成 promise 實例。它接受一個函式作為參數,並且這個函式有 resolve 和 reject 兩個參數。 Promise 有三種狀態,分別是: - pending: 等待,未完成 - resolved: 成功 - rejected: 失敗 :::warning 詳細教學參考 2019 react 入门到高级课程 - ES6 - 023 堂 教得很細,可複習 ::: ### then then()是 promise 的一個實例方法,它接受兩個回調函式作為參數,第一個回調是當 pending=>resolved 時調用,表示成功, 第二個回調是當 pending=>rejected 時調用,表示失敗。 :::warning then 方法執行完會返回一個新的 promise 實例,因此它還可以接下一個 then,下一個 then...,可採用鏈式寫法。 ::: ```javascript const promise = new Promise((resolve, reject)=>{ // ...做一些事情 if (true){ resolve(1); } else { reject(2); } }) // 第一個then,處理完後返回一個新的promise實例 .then( // resolve回調函式 value=>{ console.log(value); // 1 return 11; }, // reject回調函式 error=>{ console.log(error); // 2 return getJSON(post.commentURL); // 返回一個promise實例給下一個then } ) // 第二個then,處理上一個then返回的結果 .then( // resolve回調函式 value=>{ console.log(value); // 11 }, // 上一個then如果返回promise實例,並且是reject狀態,則執行這個回調 error=>{ console.log(error); // 22 } ) // 第三個then ... ... 以此類推 ``` :::warning 總之,error 回調總是 rejected 的時候才會被調用,如果上一個 then 沒有返回 promise 實例,或者返回的 promise 實例沒有 rejected,則不會調用。 ::: ### catch ```javascript getJSON('/posts.json') .then(posts=>{ // ... }) .then(value=>{ // ... }) .catch((error)=>{ // 異常捕獲 // 處理 getJSON 和 前一個回調函式運行時發生的錯誤 console.log('發生錯誤!', error); }); ``` ### finally ```javascript fetch('/posts.json') // 注意: 請求本地json文件時,這裡的路徑是相對於index.html的 .then(value=>{ // ... }) .then(value=>{ // ... }) .catch(error=>{ // 異常捕獲 // 處理 getJSON 和 前一個回調函式運行時發生的錯誤 console.log('發生錯誤!', error); }) .finally(()=>{ // 最後執行 // 不管成功失敗都會執行 }) ``` ### 範例:使用 promise 封裝 ajax 原生 ajax 並不支持 promise,但我們可以用 promise 封裝它。 ```javascript const getJSON =( url )=>{ return new Promise(function(resolve, reject) { const XHR = new XMLHttpRequest(); XHR.open('GET', url, true); XHR.send(); XHR.onreadystatechange =()=>{ if (XHR.readyState == 4) { if (XHR.status == 200) { try { const response = JSON.parse(XHR.responseText); resolve(response); } catch (e) { reject(e); } } else { reject(new Error(XHR.statusText)); } } } })} getJSON('./post.json') .then(data=>{ // 拿到數據後,做一些處理 if(data.code===200){ console.log(data); // alert("請求成功"); }else{ alert(`請求失敗:${data.msg}`); } }) .catch(error=>{ // 異常捕獲 // 處理 getJSON 和 前一個回調函式運行時發生的錯誤 console.log('發生錯誤!', error); }) .finally(()=>{ console.log('請求處理結束'); // 最後執行 // 不管成功失敗都會執行 }) ``` ### fetch 使用 fetch 請求本地 json 文件,fetch 請求默認會返回一個promise。 ```javascript fetch('./post.json') // 注意: 請求本地json文件時,這裡的路徑是相對於index.html的 .then(response=>{ // 解析json數據 return response.json(); }) .then(data=>{ // 拿到數據後,做一些處理 if(data.code===200){ console.log(data); // alert("請求成功"); }else{ alert(`請求失敗:${data.msg}`); } }) .catch(error=>{ // 異常捕獲 // 處理 getJSON 和 前一個回調函式運行時發生的錯誤 console.log('發生錯誤!', error); }) .finally(()=>{ console.log('請求處理結束'); // 最後執行 // 不管成功失敗都會執行 }) ``` ### Promise.all([p1, p2, p3...]) 方法 Promise.all 方法用於將多個 Promise 實例,包裝成一個全新的 Promise 實例,也就是包裝實例。而傳入的 Promise 實例可以稱之為內部實例。 :::info 只有內部實例全部處於 fulfilled 狀態時,這個包裝實例才會處於 fulfilled 狀態。並且內部實例的返回值會組成一個陣列返回給包裝實例。 ::: :::info 當內部實例中有一個被 rejected 時,包裝實例的狀態也會變成 rejected,並且這第一個被 rejected 的內部實例的返回值會傳遞給包裝實例 ::: ```javascript // 生成一個Promise物件的數組 const promises = [2, 3, 5, 7, 11, 13].map(function (id) { return getJSON('/post/' + id + ".json"); }); Promise.all(promises).then(function (posts) { // 當所有內部實例fulfilled時執行 }).catch(function(reason){ // 異常捕獲 }); ``` :::warning 注意:如果內部實例自定義了 catch,則該內部實例拋出錯誤時不會再走包裝實例的 catch,而是走自定義的 catch。 ::: ## Generator 和 promise 一樣,generator 也是一種非同步處理方案。 但 generator 做得更徹底,它能實現以同步的方式來處理非同步,過去處理非同步可能需要十幾行程式碼,而它只需要一行程式碼。 詳細教學參考 2019 react 入门到高级课程 - ES6 - 024 堂 ## async、await ES2017(es8) 標準引入了 async 函式,使得非同步操作變得更加方便。 async / await 對應的是 generator 的function* / yield,它是 generator 的語法糖,但比 generator 更友好,async 內置執行器,靈活簡單。 ### 基本定義 async 函式的定義很簡單,只需要在函式聲明前加 async 關鍵詞即可。async 函式內可使用 await 關鍵詞來處理非同步。 ```javascript async function fn() { const result = await promise ; // await後面通常需要接promise,比如fetch、axios請求等 } // 調用 fn(); ``` >當函式執行的時候,一旦遇到 await 就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。 > ### async 函式多種寫法 ```javascript // 1,函式聲明 async function fn(){ // Todo } // 2,函式表達式 const foo = async function (){ // Todo }; // 3,物件的方法 let obj = { name:"ben", age: 23, async fn() { // Todo } }; obj.fn(); // 4,Class類的方法 class Post { async getPost(id) { const post = await fetch("/api/post",{ postId: id }).then(res=>res.json()); // ... } } const Posts = new Post(); Posts.getPost('id'); // 5,箭頭函式 const fn = async () => { // ... }; ``` ### 基本用法 ```javascript async function getPost() { try { const post = await fetch('./post.json').then(res=>res.json()); // 或 const postres = await fetch('./post.json') const post = await postres.json(); console.log(post); // 其他工作 ... } catch(err){ // 捕獲異常 console.log(err); } } // 調用 getPost(); ``` ### 返回 Promise async 函式返回一個 Promise 物件。 >async 函式內部 return 語句返回的值,會成為 then 方法回調函式的參數。 如果 await 後面的異步操作出錯,那麼 async 函式返回的 Promise 物件就和被reject。 ```javascript async function getPost() { try { return await fetch('./post.json'); // 返回await結果 // 其他工作 } catch(err){ // 異常捕獲 console.log(err); } } // 調用 getPost() .then(res=>{ return res.json(); }) .then(data=>{ console.log(data); }) .catch(err=>{ console.log(err); }) ``` ### 匿名執行 async ```javascript (async () => { const response = await fetch('./post.json').then(res=>res.json()); console.log(response); })(); ```