ECMAScript 6 學習筆記
參考學習資源:阮一峰 ECMAScript 6 (ES6) 標準入門教程、從 ES6 開始的 JavaScript 學習生活
本筆記僅記錄複習 ES6+ 時一些不太熟悉或是比較重要的內容,並非所有的新特性及內容。
Symbol
ES6 引入了一種新的原始型別
Symbol
,表示獨一無二的值。 它是 JavaScript 語言的原始型別之一,其他的分別是:undefined
、null
、布林值(Boolean
)、字串(String
)、數值(Number
)、物件(Object
)。
使用 Symbol 作為物件屬性名
由於每一個 Symbol 值都是不相等的,這意味著 Symbol 值可以作為標識符,用於物件的屬性名,就能保證不會出現同名的屬性。 這對於一個物件由多個模組構成的情況非常有用,能防止某一個鍵(key)被不小心改寫或覆蓋。
let obj = {
name: 'sheep',
getName() {
console.log(this.name);
},
};
let name = Symbol();
obj[name] = 'hello';
console.log(obj);
/*
{
name: 'sheep',
Symbol(): 'hello',
getName: ƒ
}
*/
console.log(obj[name]); // hello
console.log(obj.name); // sheep
作為常數
沒有 Symbol 時:
// before
const VIDEO = 1;
const AUDIO = 2;
const IMAGE = 3;
function play(type) {
switch (type) {
case VIDEO:
console.log('播放影片');
break;
case AUDIO:
console.log('播放音樂');
break;
case IMAGE:
console.log('顯示圖片');
break;
}
}
play(AUDIO); // prints: 播放音樂
play(2); // prints: 播放音樂
可以發現當傳入 2 時仍然能正常使用這個函式。
改成使用 Symbol:
const VIDEO = Symbol();
const AUDIO = Symbol();
const IMAGE = Symbol();
function play(type) {
switch (type) {
case VIDEO:
console.log('播放影片');
break;
case AUDIO:
console.log('播放音樂');
break;
case IMAGE:
console.log('顯示圖片');
break;
}
}
play(AUDIO); // prints: 播放音樂
只有約定好的常數才能正常使用這個函式。
常數使用 Symbol 值最大的好處,就是其他任何值都不可能有相同的值了,因此可以保證上面的 switch
語句會按設計的方式工作。
Iterator 迭代器
Iterator 的作用有三個:
一是為各種資料結構,提供一個統一的,簡便的存取介面;
二是使得資料結構的成員能夠按某種次序排列;
三是 ES6 創造了一種新的遍歷寫法 for...of 迴圈,Iterator 主要在提供 for...of 迴圈
let arr = ['aaa', 'bbb', 'ccc'];
for (let i of arr) {
console.log(i); // prints: aaa, bbb, ccc
}
Symbol.iterator
屬性
一個資料結構只要具有 Symbol.iterator 屬性,就可以認為是“可迭代的”(iterable)。 Symbol.iterator 屬性本身是一個函式,就是當前資料結構預設的迭代器生成函式。執行這個函式,就會回傳一個迭代器。
Iterator 的遍歷過程是這樣的:
- 建立一個指標(pointer),指向目前資料結構的起始位置。也就是說,Iterator 本質上就是一個 pointer。
- 第一次呼叫指標的
next
方法,可以將指標指向資料結構的第一個成員。 - 第二次呼叫指標的
next
方法,指標就指向資料結構的第二個成員。 - 不斷呼叫指標的
next
方法,直到它指向資料結構的結束位置。
let arr = ['aaa', 'bbb', 'ccc'];
let iter = arr[Symbol.iterator]();
// 回傳的 iter 就是 Iterator 物件
console.log(iter.next()); // prints: {value: 'aaa', done: false}
console.log(iter.next()); // prints: {value: 'bbb', done: false}
console.log(iter.next()); // prints: {value: 'ccc', done: false}
console.log(iter.next()); // prints: {value: undefined, done: true}
原生具備 Iterator 的資料結構如下:
- Array
- Set
- Map
- String
- arguments 物件
- NodeList 物件
如何對物件進行 for of 遍歷?
如果是一個線性的物件,可以透過原型將陣列身上的 iterator 賦值給我們手動埋的 Symbol.iterator
中:
let obj = {
0: 'sheep',
1: 'hitsuji',
2: '綿羊',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator],
};
for (let i of obj) {
console.log(i); // prints: sheep, hitsuji, 綿羊
}
進階實作:
let obj2 = {
code: 200,
name: 'obj2',
list: ['sheep', 'hitsuji', '綿羊'],
// 迭代器
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.list.length) {
return {
value: this.list[index++],
done: false,
};
}
return { value: undefined, done: true };
},
};
},
};
let iter = obj2[Symbol.iterator]();
console.log(iter.next()); // prints: {value: 'sheep', done: false}
console.log(iter.next()); // prints: {value: 'hitsuji', done: false}
console.log(iter.next()); // prints: {value: '綿羊', done: false}
console.log(iter.next()); // prints: {value: undefined, done: true}
// 透過 for...of 去遍歷
for (let i of obj2) {
console.log(i); // prints: sheep, hitsuji, 綿羊
}
Set
ES6 提供了新的資料結構 Set。 它類似於陣列,但是成員的值都是唯一的,沒有重複的值。
基本用法
let s1 = new Set([1, 2, 3, 2, 3]);
console.log(s1); // prints: Set(3) {1, 2, 3}
// 將 Set 轉回普通陣列
console.log([...s1]); // prints: [1, 2, 3]
console.log(Array.from(s1)); // [prints: 1, 2, 3]
通過 add()
方法可以向 Set 添加新元素,且不會添加重複的值:
let s2 = new Set();
s2.add(1);
s2.add(2);
s2.add(2);
s2.add(3);
console.log(s2); // Set(3) {1, 2, 3}
Set 實體物件的屬性和方法
Set.prototype.size
:回傳 Set 實體的成員總數。
let s1 = new Set([1, 2, 3, 2, 3]);
console.log(s1.size); // prints: 3
Set.prototype.add(value)
:添加某個值,回傳 Set 結構本身。
// 可以鏈式呼叫
s1.add(4).add(5);
console.log(s1); // prints: Set(5) {1, 2, 3, 4, 5}
Set.prototype.has(value)
:回傳一個 boolean
,表示該值是否為 Set 的成員。
console.log(s1.has(8)); // false
console.log(s1.has(5)); // true
Set.prototype.delete(value)
:刪除某個值,回傳一個 boolean
,表示刪除是否成功。
s1.delete(5);
console.log(s1.has(5)); // false
Set.prototype.clear()
:清除所有成員,沒有回傳值。
s1.clear();
console.log(s1); // prints: Set(0) {size: 0}
迭代(遍歷) Sets
Set.prototype.keys()
:回傳鍵名的迭代器
Set.prototype.values()
:回傳鍵值的迭代器
Set.prototype.entries()
:回傳鍵值對的迭代器
Set.prototype.forEach()
:迭代每個成員
let s2 = new Set([11, 22, 33]);
for (let i of s2) {
console.log(i); // prints: 11, 22, 33
}
for (let i of s2.keys()) {
console.log(i); // prints: 11, 22, 33
}
for (let i of s2.values()) {
console.log(i); // prints: 11, 22, 33
}
for (let i of s2.entries()) {
console.log(i); // prints: [11, 11], [22, 22], [33, 33]
}
// entries 在陣列上的應用
let arr = ['aa', 'bb', 'cc'];
for (let [index, item] of arr.entries()) {
console.log(index, item); // prints: 0 'aa', 1 'bb', 2 'cc'
}
s2.forEach((item, index) => {
console.log(item, index); // prints: 11 11, 22 22, 33 33
});
刪除重複項的應用
有一個複雜的陣列結構如下,如何做到去除重複的成員?
let list = [
1,
2,
2,
'sheep',
'sheep',
[1, 2],
[3, 4],
[1, 2],
{ name: 'sheep' },
{ age: 25 },
{ name: 'sheep' },
undefined,
undefined,
NaN,
NaN,
];
利用 Set 實作:
function uni(arr) {
const res = new Set();
return arr.filter((item) => {
const id = JSON.stringify(item);
// 判斷 has return false
if (res.has(id)) return false;
// 沒有 return true
res.add(id);
return true;
});
}
Map
類似於物件,也是鍵值對(key-value pairs)的集合,但是 key 的範圍不限於字串,各種型別的值(包括物件)都可以當作 key。
基本用法
let m1 = new Map([
['name', 'sheep'],
['age', 25],
[{ a: 1 }, 'hello'],
]);
console.log(m1);
// prints: Map(3) {'name' => 'sheep', 'age' => 25, {…} => 'hello'}
let m2 = new Map();
m2.set('name', 'sheep');
m2.set('age', 100);
m2.set({ a: 1 }, 'hello');
console.log(m2);
// prints: Map(3) {'name' => 'sheep', 'age' => 25, {…} => 'hello'}
Map 同時也內建 iterator 在裡面,可以使用 spread 讓它轉成一個二維陣列。
例如:
[...m2]
會轉為[['name', 'sheep'], ['age', 100], [{ a: 1 }, 'hello']]
Map 實體物件的屬性和方法
Map 的屬性和方法與 Set 幾乎一樣。
- 屬性:
Map.prototype.size
:回傳 Map 結構的成員總數。
- 方法:
Map.prototype.set(key, value)
:設置鍵名 key 對應的鍵值為 value,然後回傳整個 Map 結構。 如果 key 已經有值,則鍵值會被更新,否則就新生成該鍵。
Map.prototype.get(key)
:讀取 key 對應的鍵值,如果找不到 key,回傳 undefined
。
Map.prototype.has(key)
:回傳一個布林值,表示某個鍵是否在當前 Map 物件之中。
Map.prototype.delete(key)
:刪除某個鍵,回傳 true
。 如果刪除失敗,回傳 false
。
Map.prototype.clear()
:清除所有成員,沒有回傳值。
迭代 Map
Map.prototype.keys()
:回傳鍵名的迭代器
Map.prototype.values()
:回傳鍵值的迭代器
Map.prototype.entries()
:回傳所有成員的迭代器
Map.prototype.forEach()
:迭代 Map 的每個成員
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同於使用 map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
Proxy
Proxy 如其名,它的作用是在物件和物件的屬性值之間設置一個代理,獲取該物件的值或者設置該物件的值,以及實例化等等多種操作,都會被攔截住,經過這一層我們可以統一處理,我們可以認為它就是"代理器"
如果我們想要在修改一個物件屬性時,能夠去攔截到這個操作時,在 ES6 之前我們會使用 Object.defineProperty()
來實作:
<body>
<div id="box"></div>
<script>
let obj = {};
Object.defineProperty(obj, 'data', {
get() {
console.log('get');
return box.innerHTML;
},
set(val) {
console.log('set', val);
// 設置 DOM
box.innerHTML = val;
},
});
</script>
</body>
利用
get
和set
去讓我們在修改 data 的同時也可以讓畫面響應式更新。
基本使用
到了 ES6 出現了一個更高級的 Proxy
,它可以替代 Object.defineProperty
,並且不會直接去改動到原始資料而是透過一層“代理”。
接著將剛剛的案例改成 Proxy
寫法:
<body>
<div id="box"></div>
<script>
let obj = {};
let proxy = new Proxy(obj, {
get(target, key) {
console.log('get', target[key]);
return target[key];
},
set(target, key, value) {
console.log('set', target, key, value);
if (key === 'data') {
box.innerHTML = value;
}
target[key] = value;
},
});
</script>
</body>
Proxy 的 this 指向問題
在對 Set
或是 Map
進行代理時,需要注意 this 指向:
let s = new Set();
let proxy = new Proxy(s, {
get(target, key) {
let value = target[key];
// 判斷如果是方法,修正 this 指向
if (value instanceof Function) {
return value.bind(target);
}
return value;
},
set() {
console.log('set');
},
});
Proxy 本質上屬於元程式設計(Metaprogramming)非破壞性資料攔截,在原物件的基礎上進行了功能的衍生而又不影響原物件,符合低耦合高內聚的設計理念。
Reflect
Reflect 可以用於獲取目標物件的行為,它與 Object 類似,但是更易讀,為操作物件提供了一種更優雅的方式。 它的方法與 Proxy 是對應的。
代替 Object 的某些方法
let obj = {};
Reflect.defineProperty(obj, 'name', {
value: 'sheep',
writable: false,
enumerable: false,
configurable: false,
});
修改某些 Object 的回傳結果
原本 Object 寫法在遇到無法定義屬性時會直接報錯:
let obj = {};
Object.defineProperty(obj, 'name', {
value: 'sheep',
writable: false,
enumerable: false,
});
Object.defineProperty(obj, 'name', {
value: 'hitsuji',
});
// Uncaught TypeError: Cannot redefine property: name at Function.defineProperty
Reflect 遇到錯誤時則只會回傳一個 false
:
Reflect.defineProperty(obj, 'name', {
value: 'sheep',
writable: false,
enumerable: false,
});
const res = Reflect.defineProperty(obj, 'name', {
value: 'hitsuji',
});
console.log(res); // prints: false
因此使用 Reflect 時就不用像過去一樣必須用 trycatch
去處理:
// 舊寫法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
指令式變為函式行為
某些 Object
操作是指令式,比如 name in obj
和 delete obj[name]
,而 Reflect.has(obj, name)
和 Reflect.deleteProperty(obj, name)
讓它們變成了函式行為。
let obj = {
name: 'sheep',
};
// 舊寫法
console.log('name' in obj); // true
// 新寫法
console.log(Reflect.has(obj, 'name')); // true
// 舊寫法
delete obj.name;
// 新寫法
Reflect.deleteProperty(obj, 'name');
配合 Proxy
將前面 Proxy 的案例結合 Reflect 進行改造:
let s = new Set();
let proxy = new Proxy(s, {
get(target, key) {
// 判斷如果是方法,修正 this 指向
let value = Reflect.get(target, key);
if (value instanceof Function) {
return value.bind(target);
}
return target[key];
},
set(target, key, value) {
Reflect.set(...arguments);
},
});
有了 Proxy
+ Reflect
就可以輕鬆地去攔截一個陣列,這是過去 Object.defineProperty
所無法做到的事情。(Vue3 將響應式資料改成 Proxy 後就不需要在對陣列方法進行一些改裝,直接用原生即可。)
let arr = [1, 2, 3];
let proxy = new Proxy(arr, {
get(target, key) {
console.log('get', key);
return Reflect.get(...arguments);
},
set(target, key, value) {
console.log('set', key, value);
return Reflect.set(...arguments);
},
});
// 直接使用原生方法,Reflect 會去攔截到
proxy.push(4);
console.log(proxy); // Proxy {0: 1, 1: 2, 2: 3, 3: 4}