ECMAScript 6 學習筆記
參考學習資源:阮一峰 ECMAScript 6 (ES6) 標準入門教程、從 ES6 開始的 JavaScript 學習生活
本筆記僅記錄複習 ES6+ 時一些不太熟悉或是比較重要的內容,並非所有的新特性及內容。
Promise
Promise 是非同步程式設計的一種解決方案,比傳統的解決方案 callback 更合理和更強大。ES6 將其寫進了語言標準,統一了用法,原生提供了
Promise
物件。
- 指定 callback 的方式更靈活易懂。
- 解決非同步 callback hell(回呼地獄) 的問題。
基本用法
Promise 建構函式接收一個函式(executor)作為參數,這個執行器函式又分別接收兩個函式作為參數 resolve
和 reject
。這兩個函式由 JavaScript 引擎提供,不需要自己去寫。
let pro = new Promise((resolve, reject) => {
// 執行器函式 (executor)
if (/* 非同步操作成功 */) {
resolve('success');
} else {
reject('fail');
}
});
// 可以用 then 方法分別指定 resolved 狀態和 rejected 狀態的 callback
pro.then(
// 成功 callback
() => {
console.log('實現');
},
// 失敗 callback
() => {
console.log('拒絕');
}
);
// 更常見的寫法是使用 .catch 去處理 rejected 狀態
pro
.then((res) => {
console.log('實現', res);
})
.catch((err) => {
console.log('拒絕', err);
});
Promise 物件的狀態
Promise 物件透過自身的狀態,來控制非同步操作。Promise 實體具有三種狀態:
- 非同步操作未完成(pending)
- 非同步操作成功(fulfilled)
- 非同步操作失敗(rejected)
這三種狀態的變化途徑只有兩種:
- 從"未完成"到"成功"
- 從"未完成"到"失敗"
一旦狀態發生變化,就凝固了,不會再有新的狀態變化。這也是 Promise 這個名字的由來,它的英文意思譯作"承諾",一旦承諾生效,就不得在改變了。Promise 實體的狀態變化只可能發生一次。
因此,Promise 的最終結果只有兩種:
非同步操作成功,Promise 實體拋出一個值(value),狀態變為 fulfilled。
非同步操作失敗,Promise 實體拋出一個錯誤(error),狀態變為 rejected。
案例:假設有個業務需將第一次請求的回傳值當作參數去發第二次請求
// 1.json
{
"data": 11111
}
// 2.json
{
"data": 22222
}
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(xhr.responseText);
}
}
};
});
}
ajax('1.json')
.then((res) => {
console.log(res);
return ajax('2.json', res);
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
then
如果回傳非 Promise 的值,則為 pending-fulfilled,如果回傳 Promise,則根據這個新的 Promise 的結果決定 pending-fulfilled 或 pending-rejected,透過鏈式呼叫 .then
可以解決掉過去 callback hell 的問題。
Promise.all()
Promise.all()
方法用於將多個 Promise 實體,包裝成一個新的 Promise 實體。
let pro1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1000);
}, 1000);
});
let pro2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2000);
}, 2000);
});
let pro3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3000);
}, 3000);
});
Promise.all([pro1, pro2, pro3])
.then((res) => {
console.log(res); // prints: [1000, 2000, 3000]
})
.catch((err) => console.log(err));
只有 3 個實體的狀態都變成
fulfilled
才會回傳一個由 pro1、pro2、pro3 的回傳值組成的陣列,只要其中一個被rejected
,Promise.all 的狀態就會變成rejected
,此時第一個被reject
的實體的回傳值,會傳遞給 catch 的 callback。
Promise.race()
Promise.race()
方法同樣是將多個 Promise 實體,包裝成一個新的 Promise 實體。
// 接續上方例子
Promise.race([pro1, pro2, pro3])
.then((res) => {
console.log(res); // prints: 1000
})
.catch((err) => console.log(err));
只要 pro1、pro2、pro3 之中有一個實體先改變狀態,Promise.race 的狀態就會跟著改變並且最先改變的 Promise 實體回傳值就會傳給 Promise.race 的 callback。
應用場景:請求超時處理
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1000);
}, 30000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(2000);
}, 2000);
});
Promise.race([p1, p2])
.then((res) => {
console.log(res);
})
.catch((err) => console.log(err, '超時了'));
// prints: 2000 '超時了'
Generator
Generator 函式是 ES6 提供的一種非同步程式設計的解決方案。
Generator 函式是一個狀態機,封裝了多個內部狀態。
執行 Generator 函式會回傳一個 iterator 物件,也就是說,Generator 函式除了狀態機,還是一個 iterator 物件生成的函式。回傳的 iterator 可以依次遍歷 Generator 函式內部的每一個狀態。
基本用法
function* gen() {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
}
let g = gen();
g.next();
g.next();
g.next();
/*
prints: 1 2 3
*/
yield 表達式是暫停執行的標記,而 next 方法可以恢復執行
非同步流程
用 Generator 去改寫 Promise 筆記中發送兩次請求案例:
- 手動驅動版本:
function* gen() {
let res = yield ajax('1.json');
console.log('第一次請求的結果', res);
let res2 = yield ajax('2.json', res);
console.log('第二次請求的結果', res2);
}
let g = gen();
g.next().value.then((data) => {
g.next(data).value.then((res) => {
g.next(res);
});
});
/*
第一次請求的結果 {data: 11111}
第二次請求的結果 {data: 22222}
*/
- 自動版本:
function* gen() {
let res = yield ajax('1.json');
console.log('第一次請求的結果', res);
let res2 = yield ajax('2.json', res);
console.log('第二次請求的結果', res2);
}
function AutoRun(gen) {
let g = gen();
function next(data) {
let res = g.next(data);
if (res.done) return;
res.value.then((data) => {
next(data);
});
}
next();
}
AutoRun(gen);
/*
第一次請求的結果 {data: 11111}
第二次請求的結果 {data: 22222}
*/
Class 語法
基本上,ES6 的
class
可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class
寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法而已。
類別的寫法
過去 ES5 的原型鏈寫法:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.name, this.age);
};
let obj = new Person('sheep', 25);
console.log(obj);
ES6 Class 寫法:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
say() {
console.log(this.name, this.age);
}
}
let obj = new Person('sheep', 25);
console.log(obj);
getter 與 setter
與 ES5 一樣,在“類別”的內部可以使用 get
和 set
關鍵字,對某個屬性設定存值函數和取值函數,攔截該屬性的存取行為。
<body>
<ul id="list"></ul>
<script>
class Person {
constructor(name, age, id) {
this.name = name;
this.age = age;
this.element = document.querySelector(`#${id}`);
}
get html() {
return this.element.innerHTML;
}
set html(data) {
this.element.innerHTML = data.map((item) => `<li>${item}</li>`).join('');
}
}
let obj = new Person('sheep', 25, 'list');
console.log(obj.html); // prints: ''
// 在設定 html 的值時,set 會攔截到並將 li 插入到 list 裡
obj.html = ['aaa', 'bbb', 'ccc'];
console.log(obj.html); // prints: '<li>aaa</li><li>bbb</li><li>ccc</li>'
</script>
</body>
靜態方法與靜態屬性
類別相當於實例的原型,所有在類別中定義的方法,都會被實體繼承。如果在一個方法前,加上 static
關鍵字,就表示該方法不會被實體繼承,而是直接透過類別來呼叫,這就稱為“靜態方法”。
靜態屬性指的是 Class 本身的屬性,即 Class.propName
,而不是定義在實體物件(this
)上的屬性。
class Person {
static myName = 'person類別的名字';
static myMethod = function () {
console.log('my method');
};
constructor(name, age) {
this.name = name;
this.age = age;
}
say() {
console.log(this.name, this.age);
}
}
// 舊寫法
// Person.myName = 'person類別的名字';
// Person.myMethod = function () {
// console.log('my method');
// };
let obj = new Person('sheep', 25);
console.log(Person.myName);
Person.myMethod();
Class 繼承
Class 可以透過 extends
關鍵字實現繼承,這比 ES5 的透過修改原型鏈實現繼承,要清晰和方便很多。
class Person {
static myName = 'person類別的名字';
static myMethod = function () {
console.log('my method');
};
constructor(name, age) {
this.name = name;
this.age = age;
}
say() {
console.log(this.name, this.age);
}
}
class Student extends Person {
constructor(name, age, score) {
super(name, age);
this.score = score;
}
say() {
super.say();
console.log(this.score);
}
getScore() {
console.log(this.score);
}
}
let obj = new Student('sheep', 25, 100);
TIP
父類別的靜態方法和屬性也會被子類別繼承
OOP render 案例
<body>
<div class="box1">
<h1></h1>
<ul></ul>
</div>
<div class="box2">
<h1></h1>
<img src="" alt="" style="width: 100px" />
<ul></ul>
</div>
<script>
var data1 = {
title: '體育',
list: ['體育-1', '體育-2', '體育-3'],
};
var data2 = {
title: '綜藝',
url: 'https://pic.maizuo.com/usr/movie/5011ee407fb407d47e333a3935ec33d1.jpg?x-oss-process=image/quality,Q_70',
list: ['綜藝-1', '綜藝-2', '綜藝-3'],
};
class CreateBox {
constructor(selector, data) {
this.element = document.querySelector(selector);
this.title = data.title;
this.list = data.list;
this.render();
}
render() {
let oh1 = this.element.querySelector('h1');
let oul = this.element.querySelector('ul');
oh1.innerHTML = this.title;
oul.innerHTML = this.list.map((item) => `<li>${item}</li>`).join('');
}
}
new CreateBox('.box1', data1);
class CreateImgBox extends CreateBox {
constructor(selector, data) {
super(selector, data);
this.imgUrl = data.url;
this.render();
}
render() {
super.render();
let oimg = this.element.querySelector('img');
oimg.src = this.imgUrl;
}
}
new CreateImgBox('.box2', data2);
</script>
</body>
執行上面的程式碼後,瀏覽器上會出現下圖的樣子:
模組化
JavaScript 現在有兩種模組。一種是 ES6 模組,簡稱 ESM;另一個是 CommonJS 模組,簡稱 CJS。
CommonJS 模組是 Node.js 專用的,與 ES6 模組不相容。語法上面,兩者最明顯的差異是,CommonJS 模組使用
require()
和module.exports
,ES6 模組使用import
和export
。
ES6 Module 不是物件,而是透過 export
指令顯性指定匯出的程式碼,再透過 import
指令匯入。
ES Module 基本寫法
- 預設匯出與匯入
export default A1;
import a1 from './1.js';
- 具名匯出與匯入
export { A1, A2 };
import { A1, A2 } from './1.js';
import { A1 as a1, A2 as a2 } from './1.js';
import * as obj from './1.js';
案例改寫
將剛剛的案例使用 ES6 Module 來改寫:
首先分別將 CreateBox
和 CreateImgBox
兩個 Class 拆成兩個 js 模組。
// CreateBox.js
export class CreateBox {
constructor(selector, data) {
this.element = document.querySelector(selector);
this.title = data.title;
this.list = data.list;
this.render();
}
render() {
let oh1 = this.element.querySelector('h1');
let oul = this.element.querySelector('ul');
oh1.innerHTML = this.title;
oul.innerHTML = this.list.map((item) => `<li>${item}</li>`).join('');
}
}
// CreateImgBox.js
import { CreateBox } from './CreateBox.js';
class CreateImgBox extends CreateBox {
constructor(selector, data) {
super(selector, data);
this.imgUrl = data.url;
this.render();
}
render() {
super.render();
let oimg = this.element.querySelector('img');
oimg.src = this.imgUrl;
}
}
export default CreateImgBox;
然後再匯入到需要使用的地方:
<!-- 必須要是 type module -->
<script type="module">
import { CreateBox } from './CreateBox.js';
import CreateImgBox from './CreateImgBox.js';
var data1 = {
title: '體育',
list: ['體育-1', '體育-2', '體育-3'],
};
var data2 = {
title: '綜藝',
url: '(略)',
list: ['綜藝-1', '綜藝-2', '綜藝-3'],
};
new CreateBox('.box1', data1);
new CreateImgBox('.box2', data2);
</script>
ES7 新特性
指數運算子 **
Math.pow(3, 2) === 3 ** 2; // 9
指數運算子是 right-associative:
a ** b ** c
相當於a ** (b ** c)
陣列的 includes 方法
[1, 2, NaN].includes(NaN) // true
[1, 2, NaN].indexOf(NaN) // -1
如果僅僅查詢資料是否在陣列中,建議使用
includes
,如果是查詢資料的索引值,建議使用indexOf
更好一點。