重學ES6基礎語法(六)

acoderbeauty發表於2019-11-02

本系列部落格為ES6基礎語法的使用及總結,如有錯誤,歡迎指正。 重學ES6基礎語法(六)主要包括 GeneratorSet/MapProxy等。

非同步程式設計

Javascript語言的執行環境是"單執行緒"(single thread)。

所謂"單執行緒",就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。

Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和非同步(Asynchronous)。

"同步模式"就是上一段的模式,後一個任務等待前一個任務結束,然後再執行,程式的執行順序與任務的排列順序是一致的、同步的;
"非同步模式"則完全不同,每一個任務有一個或多個回撥函式(callback),前一個任務結束後,不是執行後一個任務,而是執行回撥函式,後一個任務則是不等前一個任務結束就執行,所以程式的執行順序與任務的排列順序是不一致的、非同步的。

"非同步模式"非常重要。在瀏覽器端,耗時很長的操作都應該非同步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在伺服器端,"非同步模式"甚至是唯一的模式,因為執行環境是單執行緒的,如果允許同步執行所有http請求,伺服器效能會急劇下降,很快就會失去響應。

非同步程式設計的方法

通過回撥函式

優點是簡單,容易理解和部署;缺點是不利於程式碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程混亂,而且每個任務只能指定一個回撥函式。

通過事件監聽

任務的執行不取決於程式碼的順序,而取決於某個事件是否發生。

通過事件監聽,可以繫結多個事件,每個事件可以指定多個回撥函式,而且可以"去耦合"(Decoupling),有利於實現模組化。缺點是整個程式都要變成事件驅動型,執行流程會變得很不清晰。

採用釋出/訂閱模式

我們假定,存在一個"訊號中心",某個任務執行完成,就向訊號中心"釋出"(publish)一個訊號,其他任務可以向訊號中心"訂閱"(subscribe)這個訊號,從而知道什麼時候自己可以開始執行。這就叫做"釋出/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。

這種方法的性質與"事件監聽"類似,但是明顯優於後者。因為我們可以通過檢視"訊息中心",瞭解存在多少訊號、每個訊號有多少訂閱者,從而監控程式的執行。

Promise

不多贅述,這裡有說

Generator

Generator 函式是 ES6 提供的一種非同步程式設計解決方案

顧名思義,它是一個生成器,它也是一個狀態機,內部擁有值及相關的狀態。生成器返回一個迭代器Iterator物件,我們可以通過這個迭代器,手動地遍歷相關的值、狀態,保證正確的執行順序。

所謂生成器,其實就是一個物件,它每次能生成一系列值中的一個。要建立生成器,可以讓函式通過 yield 操作符返回某個特殊的值。對於使用 yield 操作符返回值的函式,呼叫它時就會建立並返回一個新的 Generator 例項。然後,在這個例項上呼叫 next()方法就能取得生成器的第一個值。此時,執行的是原來的函式,但執行流到 yield 語句就會停止,只返回特定的值。從這個角度看,yield 與 return 很相似。如果再次呼叫 next()方法,原來函式中位於 yield 語句後的程式碼會繼續執行,直到再次遇見 yield 語句時停止執行。

1.用法

Generator 函式不同於普通函式,是可以暫停執行的,所以函式名之前要加星號,以示區別。

整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用 yield 關鍵字註明。

  • yield關鍵字可以讓 Generator內部的邏輯能夠切割成多個部分。
function* listNum() {
    let i = 1;
    yield i;
    i++;
    yield i;
    i++;
    yield i;
}
const num = listNum();
console.log(listNum());
複製程式碼

重學ES6基礎語法(六)

  • 通過呼叫迭代器物件的next方法執行一個部分程式碼,執行哪個部分就會返回哪個部分定義的狀態
const num = listNum();
console.log(num.next());
console.log(num.next());
console.log(num.next());
複製程式碼

重學ES6基礎語法(六)

如上程式碼,定義了一個listNum的生成器函式,呼叫之後返回了一個迭代器物件(即num)

呼叫next方法後,函式內執行第一條yield語句,輸出當前的狀態done(迭代器是否遍歷完成)以及相應值(一般為yield關鍵字後面的運算結果)

每呼叫一次next,則執行一次yield語句,並在該處暫停。

2.Generator 函式遍歷陣列

2.1 可以發現,如果不呼叫next方法,那麼函式中封裝的程式碼不會立即被執行(下例中的console.log(arr);沒有執行)

let arr = [
  {name: 'zs',age: 38,gender: 'male'},
  {name: 'yw',age: 48,gender: 'male'},
  {name: 'lc',age: 28,gender: 'male'},
];
function* loop(arr) {
  console.log(arr);
  for(let item of arr){
    yield item;
  }
}
let repoGen = loop(arr);
console.log(repoGen);
複製程式碼

重學ES6基礎語法(六)

2.2 只有開始呼叫next方法,生成器函式裡面的程式碼才開始執行

let repoGen = loop(arr);
console.log(repoGen.next());
console.log(repoGen.next());
console.log(repoGen.next());
複製程式碼

重學ES6基礎語法(六)

2.3 當遍歷完之後,done標誌變為true時,再列印生成器函式,可以發現它的狀態已經變為了closed

let repoGen = loop(arr);
console.log(repoGen);
console.log(repoGen.next());
console.log(repoGen.next());
console.log(repoGen.next());
console.log(repoGen.next());
console.log(repoGen);
複製程式碼

重學ES6基礎語法(六)

3.Generator函式和普通函式區別

3.1 呼叫Generator函式後,無論函式有沒有返回值,都會返回一個迭代器物件
3.2 呼叫Generator函式後,函式中封裝的程式碼不會立即被執行

4.next()呼叫中的傳參

在呼叫next方法的時候可以傳遞一個引數, 這個引數會傳遞給上一個yield
注意:第一次呼叫next()時是不能傳參的,只能從第二次開始

4.1 第一次呼叫next之後返回值one為1,但在第二次呼叫next的時候one其實是undefined的,因為generator不會自動儲存相應變數值,我們需要手動的指定,這時two值為NaN,在第三次呼叫next的時候執行到yield 3 * two,通過傳參將上次yield返回值two設為2,得到結果

function* showNumbers() {
   var one = yield 1;
   var two = yield 2 * one;
   yield 3 * two;
 }

 var show = showNumbers();

 console.log(show.next().value);// 1
 console.log(show.next().value);// NaN
 console.log(show.next(2).value); // 6
複製程式碼

4.2 解析我寫不出來(只能意會不能言傳...)

function* gen() {
  console.log("123");
  let res = yield "aaa";

  console.log(res);
  console.log("567");
  yield 1 + 1;

  console.log("789");
  yield true;
}
let it = gen();
console.log(it.next()); //先輸出123,再輸出{value: "aaa", done: false}
console.log(it.next("666")); //傳遞引數給res,輸出666;再輸出567;再輸出{value: 2, done: false}
console.log(it.next()); //{value: true, done: false}
console.log(it.next()); //{value: undefined, done: true}
複製程式碼

重學ES6基礎語法(六)

5.Generator函式應用場景

5.1 讓函式返回多個值

 function* calculate(a, b) {
     yield a + b;
     yield a - b;
 }
 let it = calculate(10, 5);
 console.log(it.next().value);
 console.log(it.next().value);
複製程式碼

5.2 用同步的流程來表示非同步的操作(用來處理ajax請求的工作流)
5.3 由於Generator函式就是遍歷器生成函式,因此可以把Generator賦值給物件的Symbol.iterator屬性,從而使得該物件具有Iterator介面。

6.for...of

for...of迴圈可以自動遍歷Generator函式時生成的Iterator物件,且此時不再需要呼叫next方法。

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5
複製程式碼

上面程式碼使用for...of迴圈,依次顯示5個yield語句的值。這裡需要注意,一旦next方法的返回物件的done屬性為truefor...of迴圈就會中止,且不包含該返回物件,所以上面程式碼的return語句返回的6,不包括在for...of迴圈之中。

Set資料結構

ES6提供了新的資料結構Set。它類似於陣列,但是成員的值都是唯一的,沒有重複的值。

與陣列的不同之處:Set資料結構不能通過索引來獲取元素。

1.用法

Set本身是一個建構函式,用來生成Set資料結構。

格式:new Set([iterable]);

let list = new Set();
console.log(list);

let color = new Set(['red', 'yellow', 'green']);
console.log(color);
複製程式碼

重學ES6基礎語法(六)

2.Set資料結構常用方法

2.1 add(value)

在Set物件尾部新增一個元素。返回該Set物件。

注意:新增相同的成員會被忽略

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list);
複製程式碼

2.2 .size

返回Set例項的成員總數。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list.size); //5
複製程式碼

2.3 delete(value)

刪除某個值,返回一個布林值,表示刪除是否成功。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list.delete(5)); //true
複製程式碼

2.4 .clear

移除Set物件內的所有元素,沒有返回值。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
list.clear();
console.log(list); //Set(0) {}
複製程式碼

2.5 has(value)

返回一個布林值,表示該值在Set中存在與否。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list.has(1)); //true
複製程式碼

3.Set是可以遍歷的

3.1 .values() 返回一個新的迭代器物件,該物件包含Set物件中的按插入順序排列的所有元素的值。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list.values());
複製程式碼

重學ES6基礎語法(六)

3.2 呼叫遍歷器的next()方法實現遍歷

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
let it = list.values();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
複製程式碼

重學ES6基礎語法(六)

3.3 Set方法部署了Iterator介面,所以也可以使用for of來遍歷

for(let key of list){
  console.log(key);
}
複製程式碼

利用forEach遍歷

list.forEach((item,key,ownSet) => {
  console.log(item, key, ownSet);
})
複製程式碼

4.利用Set進行陣列去重

面試的時候經常有問:用ES5方法和ES6分別實現陣列去重

先把陣列轉為Set結構實現去重,因為Set是可以遍歷的,所以再使用擴充套件運算子將Set轉為陣列

let arr = [1,2,4,6,4,3,6,8];
let numberSet = new Set(arr); //陣列轉Set
console.log(numberSet);
let uniqueArr = [...numberSet]; //Set轉陣列
console.log(uniqueArr);
複製程式碼

重學ES6基礎語法(六)
優雅寫法:

Array.prototype.unique = function () {
    return [...new Set(this)];
};
複製程式碼

WeakSet

WeakSet結構與Set類似,也是不重複的值的集合。

WeakSet結構有以下三個方法。

.add(value):向WeakSet例項新增一個新成員。
.delete(value):清除WeakSet例項的指定成員。
.has(value):返回一個布林值,表示某個值是否在WeakSet例項之中。

WeakSet與Set的區別

  • WeakSet的成員只能是物件,而不能是其他型別的值。
let lucy = {region:'America',age: 18};
let lily = {region:'Canada',age: 20};

let person = new WeakSet([lucy,lily]);
person.add('lucas');
console.log(person); //Invalid value used in weak set
複製程式碼
  • WeakSet是不可遍歷的。
for(let key of person){
  console.log(key); //person is not iterable
}
複製程式碼
  • WeakSet沒有size屬性,沒有辦法遍歷它的成員。
  • WeakSet沒有clear()方法,但是具有自己清除的作用,避免記憶體洩漏。
let lucy = {region:'America',age: 18};
let lily = {region:'Canada',age: 20};

let person = new WeakSet([lucy,lily]);
console.log(person);
複製程式碼

重學ES6基礎語法(六)
將lily物件置為null之後,前後兩次列印的都是隻有lucy物件

let lucy = {region:'America',age: 18};
let lily = {region:'Canada',age: 20};

let person = new WeakSet([lucy,lily]);
console.log(person);
lily = null;
console.log(person);
複製程式碼

重學ES6基礎語法(六)

Map資料結構

Map 物件儲存鍵值對。任何值(物件或者原始值) 都可以作為一個鍵或一個值。

Map 型別,也稱為簡單對映,只有一個目的:儲存一組鍵值對兒。開發人員通常都使用普通物件來儲存鍵值對兒,但問題是那樣做會導致鍵容易與原生屬性混淆。簡單對映能做到鍵和值與物件屬性分離,從而保證物件屬性的安全儲存。

ES6提供的Map資料結構。它類似於物件,也是鍵值對的集合,但是“鍵”的範圍不限於字串,各種型別的值(包括物件)都可以當作鍵。也就是說,Object結構提供了“字串—值”的對應,Map結構提供了“值—值”的對應,是一種更完善的Hash結構實現。如果你需要“鍵值對”的資料結構,Map比Object更合適。

1.用法

  • 語法:new Map([iterable])
  • 引數:Iterable 可以是一個陣列或者其他iterable物件,其元素為鍵值對(兩個元素的陣列,例如: [[ 1, 'one' ],[ 2, 'two' ]])。 每個鍵值對都會新增到新的 Map。null 會被當做 undefined。
const people = new Map();
people.set('lucy',18);
people.set('lily',20);
console.log(people);
複製程式碼

重學ES6基礎語法(六)

2.Map資料結構常用方法

2.1 .size
size屬性返回Map結構的成員總數。

2.2 set(key, value)
set方法設定key所對應的鍵值,然後返回整個Map結構。如果key已經有值,則鍵值會被更新,否則就新生成該鍵。

2.3 get(key)
get方法讀取key對應的鍵值,如果找不到key,返回undefined。

2.4 has(key)
has方法返回一個布林值,表示某個鍵是否在Map資料結構中。

2.5 delete(key)
delete方法刪除某個鍵,返回true。如果刪除失敗,返回false。

2.6 .clear()
clear方法清除所有成員,沒有返回值。

3.Map是可以遍歷的

const people = new Map();
people.set('lucy',18);
people.set('lily',20);
people.set({},3);
for(let key of people){
  console.log(key);
}
複製程式碼

重學ES6基礎語法(六)

people.forEach((value,key,map) => {
  console.log(value, key, map);
})
複製程式碼

重學ES6基礎語法(六)

4.Map和Object的區別

Map的鍵(即key)可以是任意型別值,可以是一個物件,可以是一個函式等

  • 一個Object的鍵只能是字串或者Symbols,但一個Map的鍵可以是任意值,包括函式、物件、基本型別。
  • Map 中的鍵值是有序的,而新增到物件中的鍵則不是。因此,當對它進行遍歷時,Map 物件是按插入的順序返回鍵值。
const people = new Map();
people.set('lucy',18);
people.set('lily',20);
people.set({},3);
console.log(people);
複製程式碼

重學ES6基礎語法(六)

5.與其他資料結構的互相轉換

5.1 Map轉為陣列
Map轉為陣列最方便的方法,就是使用擴充套件運算子(...)。

const people = new Map();
people.set('lucy',18);
people.set('lily',20);
people.set({},3);
let arr = [...people];
console.log(arr);
複製程式碼

重學ES6基礎語法(六)
5.2 陣列轉為Map
將陣列轉入Map建構函式,就可以轉為Map。

new Map([[true, 7], [{foo: 3}, ['abc']]])
複製程式碼

5.3 Map轉為物件
如果所有Map的鍵都是字串,它可以轉為物件。

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}
複製程式碼

5.4 物件轉為Map

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}
複製程式碼

WeakMap

WeakMap與Map的區別在於:

  • WeakMap它只接受物件作為鍵名(null除外),不接受其他型別的值作為鍵名
  • WeakMap不能遍歷
  • WeakMap的元素在其他地方沒有被引用時,垃圾回收機制會自動清理掉該元素

WeakMap與Map在API上的區別主要是兩個:

  • 沒有遍歷操作(即沒有key()values()entries()方法),也沒有size屬性;
  • 無法清空,即不支援clear方法。這與WeakMap的鍵不被計入引用、被垃圾回收機制忽略有關。因此,WeakMap只有四個方法可用:get()set()has()delete()

Proxy

Proxy 物件用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等)。 幫助我們重寫物件上的一些預設的方法,定義自己的業務邏輯。

在需要公開 API,而同時又要避免使用者直接操作底層資料的時候,可以使用代理。

1.用法

  • 語法:let p = new Proxy(target, handler);
  • 引數:
    target 用Proxy包裝的目標物件(可以是任何型別的物件,包括原生陣列,函式,甚至另一個代理)。
    handler 一個物件,其屬性是當執行一個操作時定義代理的行為的函式。

Proxy 物件的所有用法,都是上面這種形式,不同的只是handler引數的寫法。其中,new Proxy()表示生成一個Proxy例項,target參數列示所要攔截的目標物件,handler引數也是一個物件,用來定製攔截行為。

2.Proxy例項的方法

get()

用於攔截某個屬性的讀取操作

const person = {name: 'ghk',age: 18};
const personProxy = new Proxy(person,{
get(target, key){
  console.log(target, key); //{name: "zs", age: 18} "name"
  return target[key].toUpperCase();
},
});
personProxy.name = 'zs';
console.log(personProxy); //Proxy {name: "zs", age: 18}
console.log(personProxy.name); //ZS
複製程式碼

set()

set方法用來攔截某個屬性的賦值操作。

const person = {name: 'ghk',age: 18};
const personProxy = new Proxy(person,{
set(target, key, value){
  if(typeof value === 'string'){
    target[key] = value.trim();
  }
}
});
personProxy.string = '    this is a test   ';
console.log(personProxy.string); //THIS IS A TEST
複製程式碼