前端進階-ES6內建功能

moduzhang發表於2018-11-27

Symbol

Symbol 簡介

Symbol 是 JS 原始資料型別列表中(numbersstringsbooleansnullundefined)的最新補充。Symbol 是一個唯一識別符號,常用於唯一標識物件中的屬性。

在這裡插入圖片描述

碗這個物件中有幾個水果屬性(水果也是物件),當有兩個相同的水果在碗裡時,會出現混亂,我們不知道什麼時候拿哪一個,這就是問題。我們需要一種方式來唯一的標識這這些香蕉。

Symbol 識別符號

Symbol 是一種獨特的且不可變的資料型別,經常用來標識物件屬性

const sym1 = Symbol('apple');
console.log(sym1); // Symbol(apple)

它將建立唯一的識別符號,並將其儲存在 sym1 中。描述 “apple” 只是用來描述識別符號的一種方式,但是不能用來訪問識別符號本身

const sym2 = Symbol('banana');
const sym3 = Symbol('banana');
console.log(sym2 === sym3); // false

描述只是用來描述符號,它並不是識別符號本身的一部分。無論描述是什麼,每次都建立新的識別符號

示例說明,下面是代表上圖中的 bowl(碗)的程式碼

const bowl = {
  'apple': { color: 'red', weight: 136.078 },
  'banana': { color: 'yellow', weight: 183.15 },
  'orange': { color: 'orange', weight: 170.097 }
};

碗中包含水果,它們是 bowl 的屬性物件。但是,當我們新增第二個香蕉時,遇到了問題。

const bowl = {
  'apple': { color: 'red', weight: 136.078 },
  'banana': { color: 'yellow', weight: 183.151 },
  'orange': { color: 'orange', weight: 170.097 },
  'banana': { color: 'yellow', weight: 176.845 }
};
console.log(bowl);
// Object {apple: Object, banana: Object, orange: Object}

新新增的香蕉將上一個香蕉覆蓋了。為了解決該問題,我們可以使用識別符號。

const bowl = {
  [Symbol('apple')]: { color: 'red', weight: 136.078 },
  [Symbol('banana')]: { color: 'yellow', weight: 183.15 },
  [Symbol('orange')]: { color: 'orange', weight: 170.097 },
  [Symbol('banana')]: { color: 'yellow', weight: 176.845 }
};
console.log(bowl);
// Object {Symbol(apple): Object, Symbol(banana): Object, Symbol(orange): Object, Symbol(banana): Object}

通過更改 bowl 的屬性並使用識別符號,每個屬性都是唯一的識別符號,第一個香蕉不會被第二個香蕉覆蓋。

迭代器協議和可迭代協議

ES6 中的兩個新協議:

  • 可迭代協議
  • 迭代器協議

這兩個協議不是內建的,但是它們可以幫助你理解 ES6 中的新迭代概念,就像給你展示識別符號的使用案例一樣。

可迭代協議

可迭代協議用來定義和自定義物件的迭代行為。也就是說在 ES6 中,你可以靈活地指定迴圈訪問物件中的值的方式。對於某些物件,它們已經內建了這一行為。例如,字串陣列就是內建可迭代型別的例子。

const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const digit of digits) {
  console.log(digit);
}

任何可迭代的物件都可以使用新的 for...of 迴圈。

原理

為了使物件可迭代,它必須實現可迭代介面。介面其實就是為了讓物件可迭代,它必須包含預設的迭代器方法。該方法將定義物件如何被迭代。

迭代器協議

迭代器協議用來定義物件生成一系列值的標準方式。實際上就是現在有了定義物件如何迭代的流程。通過執行 .next() 方法來完成這一流程

原理

當物件執行 .next() 方法時,就變成了迭代器.next() 方法是無引數函式,返回具有兩個屬性的物件

  • value:表示物件內值序列的下個值的資料
  • done:表示迭代器是否已迴圈訪問完值序列的布林值。如果 donetrue,則迭代器已到達值序列的末尾處;如果 donefalse,則迭代器能夠生成值序列中的另一個值。
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const arrayIterator = digits[Symbol.iterator]();

console.log(arrayIterator.next());
console.log(arrayIterator.next());
console.log(arrayIterator.next());

/**
Object {value: 0, done: false}
Object {value: 1, done: false}
Object {value: 2, done: false}
*/

Set

數學意義上的集合(Set)

Set 就是唯一項的集合。例如,{2, 4, 5, 6} 是 Set,因為每個數字都是唯一的,只出現一次。但是,{1, 1, 2, 4} 不是 Set,因為它包含重複的專案(1 出現了兩次!)。

Set(集合)

在 ES6 中,有一個新的內建物件的行為和數學意義上的集合相同,使用起來類似於陣列。這個新物件就叫做“Set”。

Set 與陣列之間的最大區別是:

  • Set 不基於索引,不能根據集合中的條目在集合中的位置引用這些條目
  • Set 中的條目不能單獨被訪問

基本上,Set 是讓你可以儲存唯一條目的物件。你可以向 Set 中新增條目刪除條目,並迴圈訪問 Set。這些條目可以是原始值物件

如何建立 Set

const games1 = new Set();
console.log(games1); // Set {},其中沒有條目
// 根據值列表建立 Set,則使用陣列
const games2 = new Set(['Super Mario Bros.', 'Banjo-Kazooie', 'Mario Kart', 'Super Mario Bros.']);
console.log(games2);
// Set {'Super Mario Bros.', 'Banjo-Kazooie', 'Mario Kart'}
// 會自動移除重複的條目 "Super Mario Bros.",很整潔!

修改 Set

const games = new Set(['Super Mario Bros.', 'Banjo-Kazooie', 'Mario Kart', 'Super Mario Bros.']);

games.add('Banjo-Tooie');
games.add('Age of Empires');
games.delete('Super Mario Bros.');

console.log(games);
// Set {'Banjo-Kazooie', 'Mario Kart', 'Banjo-Tooie', 'Age of Empires'}
games.clear()
console.log(games); // Set {}

.add() 新增不管成功與否,都會返回該 Set 物件。另一方面,.delete() 則會返回一個布林值,該值取決於是否成功刪除(即如果該元素存在,返回 true,否則返回 false)。

使用 Set

.size 屬性可以返回 Set 中的條目數

const months = new Set(['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);
console.log(months.size); // 12

.has() 方法可以檢查 Set 中是否存在某個條目。如果 Set 中有該條目,則 .has() 將返回 true。如果 Set 中不存在該條目,則 .has() 將返回 false

console.log(months.has('September')); // true

.values() 方法可以返回 Set 中的值.values() 方法的返回值是 SetIterator 物件。

console.log(months.values());
/**
SetIterator {'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'}
*/

Set 與迭代器

ES6 中的新可迭代協議和迭代器協議中,Set 是內建可迭代型別。這意味著迴圈時的兩件事:

  • 可以使用 Set 的預設迭代器迴圈訪問 Set 中的每一項。
  • 可以使用新的 for...of 迴圈來迴圈訪問 Set 中的每一項。

使用 SetIterator

因為 .values() 方法返回新的迭代器物件(稱為 SetIterator),你可以將該迭代器物件儲存在變數中,並使用 .next() 訪問 Set 中的每一項。

const iterator = months.values();
iterator.next();
// Object {value: 'January', done: false}
iterator.next();
// Object {value: 'February', done: false}
// ...,一直執行到 done 等於 true 時,標誌著 Set 的結束。

使用for...of迴圈

const colors = new Set(['red', 'orange', 'yellow', 'green', 'blue', 'violet', 'brown', 'black']);
for (const color of colors) {
  console.log(color);
}
/**
red 
orange 
yellow 
green 
blue 
violet 
brown 
black
*/

WeakSet(弱集合)

WeakSet 和普通 Set 很像,但是具有以下關鍵區別:

  • WeakSet 只能包含物件
  • WeakSet 無法迭代,意味著不能迴圈訪問其中的物件
  • WeakSet 沒有 .clear() 方法
const student1 = { name: 'James', age: 26, gender: 'male' };
const student2 = { name: 'Julia', age: 27, gender: 'female' };
const student3 = { name: 'Richard', age: 31, gender: 'male' };

const roster = new WeakSet([student1, student2, student3]);
console.log(roster);
/**
WeakSet {Object {name: 'Julia', age: 27, gender: 'female'}, Object {name: 'Richard', age: 31, gender: 'male'}, Object {name: 'James', age: 26, gender: 'male'}}
*/
// 新增物件以外的內容,系統將報錯!
roster.add('Amanda');
// Uncaught TypeError: Invalid value used in weak set(…)

垃圾回收

在 JavaScript 中,建立新的值時會分配記憶體,並且當這些值不再需要時,將自動釋放記憶體。這種記憶體不再需要後釋放記憶體的過程稱為垃圾回收

WeakSet 通過專門使用物件作為鍵值來利用這一點。如果將物件設為 null,則本質上是刪除該物件。當 JavaScript 的垃圾回收器執行時,該物件之前佔用的記憶體將被釋放,以便稍後在程式中使用。

student3 = null;
console.log(roster);
/**
WeakSet {Object {name: 'Julia', age: 27, gender: 'female'}, Object {name: 'James', age: 26, gender: 'male'}}
*/

這種機制的好處在於你不用去擔心要刪掉對 WeakSet 中已刪除物件的引用,JavaScript 會幫你刪除!如果物件被刪除,當垃圾回收器執行時,該物件也會從 WeakSet 中刪除。這樣的話,如果你想要一種高效、輕便的解決方法去建立一組物件,就可以使用 WeakSet。垃圾回收的發生時間點取決於很多因素

Map

Map 和 WeakMap 在很多方面與 Set 和 WeakSet 相同,它們都有類似的屬性和方法。Map 和 Set 都是可迭代的,這意味著我們可以迴圈遍歷它們。而WeakMap 和 WeakSet 不會阻止物件被當作垃圾回收。但是 Map 是唯一的,因為它們是鍵值對的集合,而 Set 是唯一值的集合。可以說 Set 類似於陣列,而Map 類似於物件

建立和修改 Map

本質上,Map 是一個可以儲存鍵值對的物件,鍵和值都可以是物件、原始值或二者的結合。

建立 Map

const employees = new Map();
console.log(employees); // Map {}

修改 Map

.set() 方法新增鍵值

const employees = new Map();

employees.set('james.parkes@udacity.com', { 
    firstName: 'James',
    lastName: 'Parkes',
    role: 'Content Developer' 
});
employees.set('julia@udacity.com', {
    firstName: 'Julia',
    lastName: 'Van Cleve',
    role: 'Content Developer'
});
employees.set('richard@udacity.com', {
    firstName: 'Richard',
    lastName: 'Kalehoff',
    role: 'Content Developer'
});

console.log(employees);
/**
Map {'james.parkes@udacity.com' => Object {...}, 'julia@udacity.com' => Object {...}, 'richard@udacity.com' => Object {...}}
*/

.delete() 方法移除鍵值對

employees.delete('julia@udacity.com');
employees.delete('richard@udacity.com');
console.log(employees);
/**
Map {'james.parkes@udacity.com' => Object {firstName: 'James', lastName: 'Parkes', role: 'Course Developer'}}
*/

.clear() 方法從 Map 中刪除所有鍵值對

employees.clear()
console.log(employees); // Map {}

如果成功地刪除了鍵值對,.delete() 方法會返回 true,失敗則返回 false.set() 如果成功執行,則返回 Map 物件本身。如果你使用 .set() 向 Map 中新增鍵已存在的鍵值對,不會收到錯誤,但是該鍵值對將覆蓋 Map 中的現有鍵值對。

處理 Map

.has() 方法並向其傳入一個鍵來檢查 Map 中是否存在該鍵值對

const members = new Map();

members.set('Evelyn', 75.68);
members.set('Liam', 20.16);
members.set('Sophia', 0);
members.set('Marcus', 10.25);

console.log(members.has('Xavier')); // false
console.log(members.has('Marcus')); // true

.get() 方法傳入一個鍵,檢索 Map 中的值。

console.log(members.get('Evelyn')); // 75.68

迴圈訪問 Map

三種方式迴圈訪問:

  • 使用 Map 的預設迭代器迴圈訪問每個鍵或值
  • 使用新的 for...of 迴圈來迴圈訪問每個鍵值對
  • 使用 Map 的 .forEach() 方法迴圈訪問每個鍵值對

使用 MapIterator
在 Map 上使用 .keys().values() 方法將返回新的迭代器物件,叫做 MapIterator。你可以將該迭代器物件儲存在新的變數中,並使用 .next() 迴圈訪問每個鍵或值。

// 訪問 Map 的鍵
let iteratorObjForKeys = members.keys();
iteratorObjForKeys.next();// Object {value: 'Evelyn', done: false}
iteratorObjForKeys.next(); // Object {value: 'Liam', done: false}
// 等等
// 訪問 Map 的值
let iteratorObjForValues = members.values();
iteratorObjForValues.next(); // Object {value: 75.68, done: false}
// 等等

使用 for…of 迴圈

for (const member of members) {
  console.log(member);
}
// 鍵值對會拆分為一個陣列,第一個元素是鍵,第二個元素是值。
/**
 ['Evelyn', 75.68]
 ['Liam', 20.16]
 ['Sophia', 0]
 ['Marcus', 10.25]
*/

使用 forEach 迴圈

members.forEach((value, key) => console.log(value, key));
/**
'Evelyn' 75.68
 'Liam' 20.16
 'Sophia' 0
 'Marcus' 10.25
*/

WeakMap

WeakMap 和普通 Map 很像,但是具有以下關鍵區別:

  • WeakMap 只能包含物件作為鍵
  • WeakMap 無法迭代,意味著無法迴圈訪問
  • WeakMap 沒有 .clear() 方法
const book1 = { title: 'Pride and Prejudice', author: 'Jane Austen' };
const book2 = { title: 'The Catcher in the Rye', author: 'J.D. Salinger' };
const book3 = { title: 'Gulliver's Travels', author: 'Jonathan Swift' };

const library = new WeakMap();
library.set(book1, true);
library.set(book2, false);
library.set(book3, true);

console.log(library);
/**
WeakMap {Object {title: 'Pride and Prejudice', author: 'Jane Austen'} => true, Object {title: 'The Catcher in the Rye', author: 'J.D. Salinger'} => false, Object {title: 'Gulliver's Travels', author: 'Jonathan Swift'} => true}
*/
// 新增物件以外的內容作為鍵,系統將報錯!
library.set('The Grapes of Wrath', false);
// Uncaught TypeError: Invalid value used as weak map key(…)

垃圾回收

book1 = null;
console.log(library);
/**
WeakMap {Object {title: 'The Catcher in the Rye', author: 'J.D. Salinger'} => false, Object {title: 'Gulliver’s Travels', author: 'Jonathan Swift'} => true}
*/

Promise-非同步請求

簡介

使用 JavaScript Promise 是處理非同步請求的一種新方法,是對我們過去程式碼構建方式的一種改良。處理一個請求需要來回等待,造成大量的停機時間,能夠在停機時間同時進行其它工作,並且通知我們請求處理完成,就是 Promise 在 JavaScript 中的作用。

使用 Promise

JavaScript Promise 是用新的 Promise 建構函式 - new Promise() 建立而成的。promise 使你能夠展開一些可以非同步完成的工作,並回到常規工作。建立 promise 時,必須向其提供非同步執行的程式碼。將該程式碼當做引數提供給建構函式:

new Promise(function () {
    window.setTimeout(function createSundae(flavor = 'chocolate') {
        const sundae = {};
        // 請求冰淇淋
        // 得到錐形蛋筒
        // 加熱冰淇淋
        // 舀一大勺到蛋筒裡!
    }, Math.random() * 2000);
});

JavaScript 如何通知我們它已經完成操作,準備好讓我們恢復工作?

它通過向初始函式中傳入兩個函式來實現這一點,通常我們將這兩個函式稱為 resolvereject

resolve 函式-請求完成

new Promise(function (resolve, reject) {
    window.setTimeout(function createSundae(flavor = 'chocolate') {
        const sundae = {};
        // 請求冰淇淋
        // 得到錐形蛋筒
        // 加熱冰淇淋
        // 舀一大勺到蛋筒裡!
        resolve(sundae);
    }, Math.random() * 2000);
});

當 sundae 被成功建立後,它會呼叫 resolve 方法並向其傳遞我們要返回的資料,在本例中,返回的資料是完成的 sundae。因此 resolve 方法用來表示請求已完成,並且成功完成了請求。

reject 函式-請求失敗

如果請求存在問題,無法完成請求,那麼我們可以使用傳遞給該函式的第二個函式。通常,該函式儲存在一個叫做"reject"的識別符號中,表示如果請求因為某種原因失敗了,應該使用該函式

new Promise(function (resolve, reject) {
    window.setTimeout(function createSundae(flavor = 'chocolate') {
        const sundae = {};
        // 請求冰淇淋
        // 得到錐形蛋筒
        // 加熱冰淇淋
        // 舀一大勺到蛋筒裡!
        if ( /* iceCreamConeIsEmpty(flavor) */ ) {
            reject(`Sorry, we're out of that flavor :-(`);
        }
        resolve(sundae);
    }, Math.random() * 2000);
});

如果請求無法完成,則使用 reject 方法。注意,即使請求失敗了,我們依然可以返回資料。

Promise建構函式需要一個可以執行的函式,執行一段時間後,將成功完成(使用 resolve 方法)或失敗(使用 reject 方法)。當結果最終確定時(請求成功完成或失敗),現在 promise 已經實現了,並且將通知我們,這樣我們便能決定將如何對結果做處理。

Promise 立即返回物件

首先要注意的是,Promise 將立即返回一個物件。

const myPromiseObj = new Promise(function (resolve, reject) {
    // 聖代建立程式碼
});

該物件上具有一個 .then() 方法,我們可以讓該方法通知我們 promise 中的請求成功與否。.then() 方法會接收兩個函式:

  • 請求成功完成時要執行的函式
  • 請求失敗時要執行的函式
mySundae.then(function(sundae) {
    console.log(`Time to eat my delicious ${sundae}`);
}, function(msg) {
    console.log(msg);
    self.goCry(); // 不是一個真正的方法
});

傳遞給 .then() 的第一個函式將被呼叫,並傳入 Promise 的 resolve 函式需要使用的資料。這裡,該函式將接收 sundae 物件。第二個函式傳入的資料會在 Promise 的 reject 函式被呼叫時使用。

更多 Promise

Proxy-代理

JavaScript 代理會讓一個物件代表另一個物件,來處理另一個物件的所有互動。代理可以直接處理請求,接收或傳送目標物件資料,以及處理一大堆其他的事情。

建立 Proxy

使用 Proxy 建構函式 new Proxy();。Proxy 建構函式接收兩個專案:

  • 它將要代理的物件
  • 包含將為被代理物件處理的方法列表的物件

第二個物件叫做處理器

建立 Proxy 的最簡單方式是提供物件和空的 handler(處理器)物件

var richard = {status: 'looking for work'};
var agent = new Proxy(richard, {});

agent.status; // 返回 'looking for work'

上述程式碼並沒有對 Proxy 執行任何特殊操作,只是將請求直接傳遞給源物件!如果我們希望 Proxy 物件截獲請求,這就是 handler 物件的作用了!

讓 Proxy 變得有用的關鍵是當做第二個物件傳遞給 Proxy 建構函式的 handler 物件。handler 物件由將用於訪問屬性的方法構成。

Get Trap(捕獲器)

get 用來截獲對屬性的呼叫:

const richard = {status: 'looking for work'};
const handler = {
    get(target, propName) {
        console.log(target); // `richard` 物件,不是 `handler` 也不是 `agent`
        console.log(propName); // 代理(本例中為`agent`)正在檢查的屬性名稱
    }
};
const agent = new Proxy(richard, handler);
agent.status; // 登出 richard 物件(不是代理物件!)和正在訪問的屬性的名稱(`status`)

在上述程式碼中,handler 物件具有一個 get 方法(因為被用在 Proxy 中,所以將"function"(方法)稱之為"trap"(捕獲器))。當程式碼 agent.status; 在最後一行執行時,因為存在 get 捕獲器,它將截獲該呼叫以獲得 status(狀態)屬性並執行 get 捕獲器方法。這樣將會輸出 Proxy 的目標物件(richard 物件),然後輸出被請求的屬性(status 屬性)的名稱。它的作用就是這些!它不會實際地輸出屬性!這很重要 —— 如果使用了捕獲器,你需要確保為該捕獲器提供所有的功能

從 Proxy 內部訪問目標物件

如果我們想真正地提供真實的結果,我們需要返回目標物件的屬性:

const richard = {status: 'looking for work'};
const handler = {
    get(target, propName) {
        console.log(target);
        console.log(propName);
        return target[propName];
    }
};
const agent = new Proxy(richard, handler);
agent.status; //  (1)列印 richard 物件,(2)列印被訪問的​​屬性,(3)返回 richard.status 中的文字

get trap 中新增了最後一行 return target[propName];,這樣將會訪問目標物件的屬性並返回它。

直接獲取 Proxy 的返回資訊

使用 Proxy 提供直接的反饋:

const richard = {status: 'looking for work'};
const handler = {
    get(target, propName) {
        return `He's following many leads, so you should offer a contract as soon as possible!`;
    }
};
const agent = new Proxy(richard, handler);
agent.status; // 返回文字 `He's following many leads, so you should offer a contract as soon as possible!`

對於上述程式碼,Proxy 甚至不會檢查目標物件,直接對呼叫程式碼做出響應

因此每當 Proxy 上的屬性被訪問,get trap 將接管任務。如果我們想截獲呼叫以更改屬性,則需要使用 set trap!

set trap 用來截獲將更改屬性的程式碼。set trap 將接收: 它代理的物件 被設定的屬性 Proxy 的新值

const richard = {status: 'looking for work'};
const handler = {
    set(target, propName, value) {
        if (propName === 'payRate') { // 如果工資正在確定,則需要15%作為佣金。
            value = value * 0.85;
        }
        target[propName] = value;
    }
};
const agent = new Proxy(richard, handler);
agent.payRate = 1000; // 將演員的工資設定為 1,000美元
agent.payRate; // 850美元是演員的實際工資

在上述程式碼中,注意 set trap 會檢查是否設定了 payRate 屬性。如果設定了,Proxy 就從中拿走 15% 的費用作為自己的佣金!當演員的薪酬是一千美元時,因為 payRate 屬性已設定,程式碼從中扣除 15% 的費用,並將實際 payRate 屬性設為 850;

其他 Trap

實際上總共有 13 種不同的 Trap,它們都可以用在處理程式中!

  1. get trap - 使 proxy 能處理對屬性訪問權的呼叫
  2. set trap - 使 proxy 能將屬性設為新值
  3. apply trap - 使 proxy 能被呼叫(被代理的物件是函式)
  4. has trap - 使 proxy 能使用 in 運算子
  5. deleteProperty trap - 使 proxy 能確定屬性是否被刪除
  6. ownKeys trap - 使 proxy 能處理當所有鍵被請求時的情況
  7. construct trap - 使 proxy 能處理 proxy 與 new 關鍵字一起使用當做建構函式的情形
  8. defineProperty trap - 使 proxy 能處理當 defineProperty 被用於建立新的物件屬性的情形
  9. getOwnPropertyDescriptor trap - 使 proxy 能獲得屬性的描述符
  10. preventExtenions trap - 使 proxy 能對 proxy 物件呼叫 Object.preventExtensions()
  11. isExtensible trap - 使 proxy 能對 proxy 物件呼叫 Object.isExtensible
  12. getPrototypeOf trap - 使 proxy 能對 proxy 物件呼叫 Object.getPrototypeOf
  13. setPrototypeOf trap - 使 proxy 能對 proxy 物件呼叫 Object.setPrototypeOf

Proxy 與 ES5 Getter/Setter

對於 ES5 的 getter 和 setter 方法,你需要提前知道要獲取/設定的屬性

var obj = {
    _age: 5,
    _height: 4,
    get age() {
        console.log(`getting the "age" property`);
        console.log(this._age);
    },
    get height() {
        console.log(`getting the "height" property`);
        console.log(this._height);
    }
};

對於上述程式碼,注意在初始化物件時,我們需要設定 get age()get height()。因此,當我們呼叫下面的程式碼時,將獲得以下結果:

obj.age; // 列印 'getting the "age" property' 和 5
obj.height; // 列印 'getting the "height" property' 和 4

但是當我們向該物件新增新的屬性時,並沒有顯示 ageheight 屬性那樣生成的 getting the "weight" property 訊息。

obj.weight = 120; // 在物件上設定一個新的屬性
obj.weight; // 只列印120

對於 ES6 中的 Proxy,我們不需要提前知道這些屬性:

const proxyObj = new Proxy({age: 5, height: 4}, {
    get(targetObj, property) {
        console.log(`getting the ${property} property`);
        console.log(targetObj[property]);
    }
});

proxyObj.age; // 列印 'getting the age property' 和 5
proxyObj.height; // 列印 'getting the height property' 和 4

當我們新增新的屬性時

proxyObj.weight = 120; // 在物件上設定一個新的屬性
proxyObj.weight; // 列印 'getting the weight property' 和 120

因此 proxy 物件的某些功能可能看起來類似於現有的 ES5 getter/setter 方法,但是對於 proxy,在初始化物件時,不需要針對每個屬性使用 getter/setter 初始化物件

Proxy 是一種強大的建立和管理物件之間的互動的新方式。

生成器

每當函式被呼叫時,JavaScript 引擎就會在函式頂部啟動,並執行每行程式碼,直到到達底部。無法中途停止執行程式碼,並稍後重新開始。一直都是這種“執行到結束”的工作方式:

function getEmployee() {
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        console.log(name);
    }

    console.log('the function has ended');
}

getEmployee();

執行上述程式碼將在控制檯中輸出以下內容:

the function has started
Amanda
Diego
Farrin
James
Kagure
Kavita
Orit
Richard
the function has ended

如果你想先輸出前三名員工的姓名,然後停止一段時間,稍後再從停下的地方繼續輸出更多員工的姓名呢?普通函式無法這麼做,因為無法中途“暫停”執行函式。

可暫停的函式

如果我們希望能夠中途暫停執行函式,則需要使用 ES6 中新提供的一種函式,叫做 generator(生成器)函式!

function* getEmployee() {
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        console.log( name );
    }

    console.log('the function has ended');
}

星號表示該函式實際上是生成器!嘗試執行該函式

getEmployee();

// 這是我在 Chrome 中獲得的迴應:
getEmployee {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}

注意,生成器的星號實際上可以放在 function 關鍵字和函式名稱之間的任何位置

生成器和迭代器

生成器被呼叫時,它不會執行函式中的任何程式碼,而是建立和返回迭代器。該迭代器可以用來執行實際生成器的內部程式碼。

const generatorIterator = getEmployee();
generatorIterator.next();

關鍵字 yield

關鍵字 yield 是 ES6 中新出現的關鍵字。只能用在生成器函式中。yield 會導致生成器暫停下來。我們向我們的生成器中新增 yield

function* getEmployee() {
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        console.log(name);
        yield;
    }

    console.log('the function has ended');
}

注意,現在 for...of 迴圈中出現了 yield。如果我們呼叫該生成器(生成迭代器),然後呼叫 .next(),將獲得以下輸出:

const generatorIterator = getEmployee();
generatorIterator.next();
/**
the function has started
Amanda
*/
generatorIterator.next(); // Diego
generatorIterator.next(); // Farrin
// ...

它能完全記住上次停下的地方!它獲取到陣列中的下一項(Diego),記錄它,然後再次觸發了 yield,再次暫停。

向外面的世界生成資料

function* getEmployee() {
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        yield name;
    }

    console.log('the function has ended');
}

注意,現在從 console.log(name); 切換成了 yield name;。做出這一更改後,當生成器執行時,它會把姓名從函式裡返回出去,然後暫停執行程式碼。我們看看具體效果:

const generatorIterator = getEmployee();
let result = generatorIterator.next();
result.value // 是 "Amanda"

generatorIterator.next().value // 是 "Diego"
generatorIterator.next().value // 是 "Farrin"

示例

迭代器的 .next() 方法需要被呼叫多少次,才能完全完成/用盡下面的 udacity 生成器函式:

function* udacity() {
    yield 'Richard';
    yield 'James'
}

它被呼叫的次數將比生成器函式中的 yield 表示式的數量多一次

  1. .next() 的第一次呼叫將啟動該函式,並執行為第一個 yield
  2. .next() 的第二次呼叫將從暫停的地方繼續,並執行第二個 yield
  3. .next() 的第三次(即最後一次)呼叫將再次從暫停的地方繼續,並執行到函式結尾處。

向生成器中傳送資料或從中向外傳送資料

我們可以使用關鍵字 yield 從生成器中獲取資料。我們還可以將資料傳送回生成器中。方式是使用 .next() 方法:

function* displayResponse() {
    const response = yield;
    console.log(`Your response is "${response}"!`);
}

const iterator = displayResponse();

iterator.next(); // 開始執行生成器函式
iterator.next('Hello Udacity Student'); // 將資料傳送到生成器中
// 上面的一行列印到控制檯:你的響應是 "Hello Udacity Student"!

使用資料呼叫 .next()(即 .next('Richard'))會將該資料傳送到生成器函式中上次離開的地方。它會將 yield 關鍵字替換為你提供的資料。

關鍵字 yield 用來暫停生成器並向生成器外傳送資料,然後 .next() 方法用來向生成器中傳入資料。下面是使用這兩種過程來一次次地迴圈訪問姓名列表的示例:

function* getEmployee() {
    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];
    const facts = [];
    for (const name of names) {
        // yield *出* 每個名稱並將返回的資料儲存到 facts 陣列中
        facts.push(yield name); 
    }
    return facts;
}

const generatorIterator = getEmployee();

// 從生成器中獲取第一個名稱
let name = generatorIterator.next().value;

// 將資料傳入 *並* 獲取下一個名稱
name = generatorIterator.next(`${name} is cool!`).value; 

// 將資料傳入 *並* 獲取下一個名稱
name = generatorIterator.next(`${name} is awesome!`).value; 

// 將資料傳入 *並* 獲取下一個名稱
name = generatorIterator.next(`${name} is stupendous!`).value; 

// 你懂的
name = generatorIterator.next(`${name} is rad!`).value; 
name = generatorIterator.next(`${name} is impressive!`).value;
name = generatorIterator.next(`${name} is stunning!`).value;
name = generatorIterator.next(`${name} is awe-inspiring!`).value;

// 傳遞最後一個資料,生成器結束並返回陣列
const positions = generatorIterator.next(`${name} is magnificent!`).value; 

// 在自己的行上顯示每個名稱及其描述
positions.join('\n'); 

示例

function* createSundae() {
    const toppings = [];

    toppings.push(yield);
    toppings.push(yield);
    toppings.push(yield);

    return toppings;
}

var it = createSundae();
it.next('hot fudge');
it.next('sprinkles');
it.next('whipped cream');
it.next();

注意,第一次呼叫 .next() 將初始化生成器,將在第一個 yield 位置暫停。第二次呼叫 .next() 將向該 yield 提供資料。

數數有多少個 yield,以及每次呼叫 .next() 時,資料是如何被傳入的。

toppings 陣列的最後一項將是 undefined。因為:

  1. 因為第一次呼叫 .next() 傳入了一些資料,但是該資料沒有儲存在任何位置。
  2. 最後一次呼叫 .next() 應該會獲得一些資料(空資料),因為生成到對 toppings.push() 的最後一次呼叫中。

生成器是強大的新型函式,能夠暫停執行程式碼,同時保持自己的狀態。生成器適用於一次一個地迴圈訪問列表項,以便單獨處理每項,然後再轉到下一項。還可以使用迭代器來處理巢狀回撥。例如,假設某個函式需要獲得所有倉庫的列表和被加星標的次數。在獲得每個倉庫的星標數量之前,需要獲得使用者的資訊。獲得使用者的個人資料後,程式碼可以利用該資訊查詢所有的倉庫。

相關文章