本系列部落格為ES6基礎語法的使用及總結,如有錯誤,歡迎指正。 重學ES6基礎語法(六)主要包括 Generator、Set/Map、Proxy等。
非同步程式設計
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());
複製程式碼
- 通過呼叫迭代器物件的
next
方法執行一個部分程式碼,執行哪個部分就會返回哪個部分定義的狀態
const num = listNum();
console.log(num.next());
console.log(num.next());
console.log(num.next());
複製程式碼
如上程式碼,定義了一個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);
複製程式碼
2.2 只有開始呼叫next
方法,生成器函式裡面的程式碼才開始執行
let repoGen = loop(arr);
console.log(repoGen.next());
console.log(repoGen.next());
console.log(repoGen.next());
複製程式碼
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);
複製程式碼
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}
複製程式碼
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
屬性為true
,for...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);
複製程式碼
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());
複製程式碼
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());
複製程式碼
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);
複製程式碼
優雅寫法:
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);
複製程式碼
將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);
複製程式碼
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);
複製程式碼
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);
}
複製程式碼
people.forEach((value,key,map) => {
console.log(value, key, map);
})
複製程式碼
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);
複製程式碼
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);
複製程式碼
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
複製程式碼