第七章 Set集合與Map集合
Set集合是一種無重復元素的列表,開發(fā)者們一般不會逐一讀取數(shù)組中的元素,也不大可能逐一訪問Set集合中的每個元素,通常的做法是檢測給定的值在某個集合中是否存在。
Map集合內(nèi)含多組鍵值對,集合中每個元素分別存放著可訪問的鍵名和他對應的值,Map集合經(jīng)常被用于緩存頻繁取用的數(shù)據(jù)。
1. ES5中使用對象模擬Set、Map集合的問題
對象的鍵值會被自動轉(zhuǎn)化為字符串
var map = Object.create(null);
map[5] = "foo";
console.log(map["5"]); // foo
var key1 = {};
var key2 = {};
map[key1] = "foo"; // key被轉(zhuǎn)換為 [object Object]
console.log(map[key2]); // foo
2. ES6中的Set集合
在Set集合中不會對所存值進行強制的類型轉(zhuǎn)換,引擎內(nèi)部使用Object.is()方法檢測兩個值是否一致。
Set具有的基本方法: add
, has
, delete
, clear
size
屬性可以獲取集合中目前的元素數(shù)量。
let set = new Set();
/************ add 添加元素 ****************/
set.add(5);
set.add("5");
console.log(set.size); // 2
set.add("5"); // 重復 - 本次調(diào)用直接被忽略
console.log(set.size); // 2
let key1 = {};
let key2 = {};
set.add(key1);
set.add(key2);
console.log(set.size); // 4
/************ has 檢測存在 ****************/
console.log(set.has(key1)); // true
console.log(set.has(5)); // true
console.log(set.has(6)); // false
/************ delete 移除元素 ****************/
set.delete(5);
console.log(set.has(5)); // false
console.log(set.size); // 3
/************ clear 清除元素 ****************/
set.clear();
console.log(set.size); // 0
2.1 Set轉(zhuǎn)數(shù)組
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
let array = [...set];
console.log(array); // [1,2,3,4,5]
/*****************************************/
function eliminateDuplicates(item){
return [...new Set(item)];
}
let numbers = [1,2,3,4,5,5,5,5];
let noDuplicates = eliminateDuplicates(numbers);
console.log(noDuplicates); // [1, 2, 3, 4, 5]
2.2 forEach方法
Set的forEach方法的回調(diào)函數(shù)接收一下三個參數(shù):
- Set集合中下一次索引的位置
- 與第一個參數(shù)一樣的值
- 被遍歷的Set集合本身
我們可以看到第一二個參數(shù)的值是一模一樣的,這是因為為了和數(shù)組的forEach方法保持一致,避免分歧過大。
let set = new Set([1, 2]);
set.forEach(function(value, index, ownerSet){
console.log(index + " " + value);
console.log(index === value);
console.log(set === ownerSet);
});
// 輸出
1 1
true
true
2 2
true
true
forEach方法的第二個參數(shù)也和數(shù)組一樣,如果需要在回調(diào)函數(shù)中使用this,則可以將它作為第二個參數(shù)傳入forEach()函數(shù)。當然,也可以使用箭頭函數(shù)。
let set = new Set([1, 2]);
let processor = {
output(value) {
console.log(value);
},
process(dataSet) {
dataSet.forEach(function (value) {
this.output(value);
}, this);
},
processArrow(dataSet) {
dataSet.forEach(value => this.output(value));
}
}
processor.process(set); // 1 2
processor.processArrow(set); // 1 2
請記住,盡管Set更適合用來跟蹤多個值,而且又可以通過forEach()方法操作集合中的每一個元素,但是你不能像訪問數(shù)組元素那樣直接通過索引來訪問集合中的元素,如有需要,請先轉(zhuǎn)換為數(shù)組。
2.3 Weak Set 集合
將對象存儲在Set的實例與存儲在變量中一樣,只要Set實例中的引用存在,垃圾回收機制就不能釋放該對象的內(nèi)存空間,于是之前提到的Set類型可以被看做是一個強引用的Set集合。
let set = new Set();
let key = {};
set.add(key);
console.log(set.size); // 1
// 移除原始引用
key = null;
console.log(set.size); // 1
// 重新取回原始引用
key = [...set][0];
大部分情況下這種代碼運行良好,但是有時候你希望某個對象的其他所有引用都不再存在時,讓Set集合中的這些引用隨之消失。例如,在頁面中通過JS記錄了一些DOM,但是這些DOM可能被另一端腳本移除,而你又不希望自己的代碼保留這些DOM元素的最后一個引用(這個情景被稱為內(nèi)存泄漏)。
為了解決這個問題,ES6引入了Weak Set集合。Weak Set集合只存儲對象的弱引用,并且不可以存儲原始值;集合中的弱引用如果是對象唯一的引用,則會被回收并釋放相應內(nèi)存。
Weak Set只有add
、has
、delete
三個方法。
let set = new WeakSet(),
key = {};
set.add(key);
console.log(set.has(key)); // true
set.delete(key);
console.log(set.has(key)); // false
// 以下皆報錯
// TypeError: Invalid value used in weak set
set.add(5);
set.add("5");
set.add(true);
兩類Set的主要區(qū)別
最大區(qū)別是Weak Set保存的是對象的弱引用,可惜我們沒有辦法用代碼來驗證,例如下面的代碼
let weakSet = new WeakSet(),
set = new Set(),
key = {};
set.add(key);
weakSet.add(key);
console.log(set.size); // 1
console.log(weakSet.size); // undefined
key = null;
console.log(set.size); // 1
console.log(weakSet.size); // undefined
這是因為WeakSet沒有size屬性。所以說,我們可以看到WeakSet和Set的差別還有下面這幾點:
- WeakSet中,add、has、delete三個方法傳入非對象參數(shù)都會報錯
- WeakSet不可迭代,不能被用于for-of
- WeakSet不暴露任何迭代器(例如keys、values方法),所以無法通過程序本身來檢測其中的內(nèi)容
- 不支持forEach方法
- 不支持size屬性
Weak Set集合的功能看似受限,其實這是為了讓它能夠正確地處理內(nèi)存中的數(shù)據(jù)。總之,如果你只需要跟蹤對象引用,你更應該使用Weak Set集合而不是Set集合。
Set類型可以用來處理列表中的值,但是不適用于處理鍵值對這樣的信息結(jié)構(gòu)。ES6中添加了Map集合來解決類似的問題。
ES6中的Map集合
ES6中的Map類型是一種存儲著許多鍵值對的有序列表,其中的鍵名和對應的值支持所有的數(shù)據(jù)類型。鍵名的等價性判斷是通過調(diào)用Object.is()
方法實現(xiàn)的。
Map集合支持的方法
- set
- get
- has(key)
- delete(key)
- clear()
let map = new Map();
map.set("name", "NowhereToRun");
map.set("age", "24");
console.log(map.size); // 2
console.log(map.has("name")); // true
console.log(map.get("name")); // NowhereToRun
map.delete("name");
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.size); // 1
map.clear();
console.log(map.size); // 0
let key1 = {};
let key2 = {};
map.set(key1, 1);
map.set(key2, 2);
console.log(map.size); // 2
console.log(map.get(key1)); // 1
Map集合初始化方式
由于Map集合可以接受任意數(shù)據(jù)類型的鍵名,為了確保它們在被存儲到Map集合中之前不會被強制轉(zhuǎn)換為其他數(shù)據(jù)類型,因而只能將他們放在數(shù)組中,因為這是唯一一種可以準確地呈現(xiàn)鍵名類型的方式。
let map = new Map([["name", "NowhereToRun"], ["age", "24"]]);
console.log(map.size); // 2
console.log(map.has("name")); // true
console.log(map.get("name")); // NowhereToRun
console.log(map.has("age")); // true
console.log(map.get("age")); // 24
Map集合的 forEach() 方法
let map = new Map([["name", "NowhereToRun"], ["age", "24"]]);
map.forEach(function (value, key, ownerMap) {
console.log(key + ' ' + value);
console.log(ownerMap === map);
})
// name NowhereToRun
// true
// age 24
// true
遍歷過程中,會按照鍵值對插入Map集合的順序將相應信息傳入forEach()方法的毀掉函數(shù),而在數(shù)組中,會按照數(shù)值型索引值的順序依次傳入回調(diào)函數(shù)。
Weak Map集合
鍵名必須是對象,否則會報錯;
集合中保存的是對象的弱引用,如果在弱引用之外不存在其他的強引用,會被垃圾回收;但是只有集合的鍵名遵從這個規(guī)則,鍵名對應的鍵值如果是一個對象,則保存的是對象的強引用,不會觸發(fā)垃圾回收
支持的方法:
- set
- get
- has
- delete
和Weak Set一致,不支持size屬性。從而無法驗證集合是否為空,同樣,(在鍵名對象銷毀后)由于沒有鍵對應的引用,因而無法通過get()方法獲取到相應的值。(就是無腦相信引擎已經(jīng)給他干掉了)
Weak Map的用處
- 一般用來存dom
let map = new WeakMap();
document.querySelector(".element");
map.set(element, "original");
let value = map.get(element);
console.log(value); // original
// 移除element元素
element.parentNode.removeChild(element);
element = null;
// 此時Weak Map集合為空
- 存私有對象數(shù)據(jù)
ES5中實現(xiàn)
var Person = (function () {
var privateData = {};
var privateId = 0;
function Person(name) {
Object.defineProperties(this, "_id", { value: privateId++ });
privateData[this._id] = {
name: name
};
}
Person.prototype.getName = function(){
return privateData[this._id].name;
};
return Person;
}())
這種方法最大的問題是,如果不主動管理,由于無法獲知對象實例何時被銷毀,因此privateData中的數(shù)據(jù)就永遠不會消失。而使用WeakMap可以解決這個問題
var Person = (function () {
var privateData = new WeakMap();
function Person(name) {
privateData.set(this, {name: name});
}
Person.prototype.getName = function(){
return privateData.get(this).name;
};
return Person;
}())
只要對象實例銷毀,相關(guān)信息也被銷毀,從而保證了信息的私有性。
什么時候使用WeakMap
只用對象作為集合的鍵名?WeakMap : Map
需要forEach()||size||clear() ? Map : WeakMap
第十章 改進的數(shù)組功能
Array.of()
出這個Api的目的是規(guī)范化使用構(gòu)造函數(shù)創(chuàng)建數(shù)組時的怪異行為。
let items = new Array(2);
console.log(items.length); // 2
console.log(items[0]); // undefined
console.log(items[1]); // undefined
items = new Array('2');
console.log(items.length); // 1
console.log(items[0]); // '2'
console.log(items[1]); // undefined
items = new Array(1, 2);
console.log(items.length); // 2
console.log(items[0]); // 1
console.log(items[1]); // 2
items = new Array(3, '2');
console.log(items.length); // 2
console.log(items[0]); // 3
console.log(items[1]); // '2'
如果給Array構(gòu)造函數(shù)傳入一個數(shù)值型的數(shù),那么數(shù)組的length屬性會被設為該值;如果傳入多個值,無論是否是數(shù)值型,都會變成數(shù)組的元素。
Array.of
方法,無論參數(shù)是什么類型,無論參數(shù)有多少個,Array.of()總會創(chuàng)建一個包含所有參數(shù)的數(shù)組。
let items = Array.of(1, 2);
console.log(items.length); // 2
console.log(items[0]); // 1
console.log(items[1]); // 2
items = Array.of(2);
console.log(items.length); // 1
console.log(items[0]); // 2
items = Array.of('2');
console.log(items.length); // 1
console.log(items[0]); // '2'
Array.from
處理類數(shù)組轉(zhuǎn)換。例如一個處理數(shù)組類型轉(zhuǎn)換的函數(shù):
// 舊方法:
Array.prototype.slice.call(arrayLike);
// 新方法:
Array.from(arrayLike);
映射轉(zhuǎn)換
如果想進一步轉(zhuǎn)換,可以提供給一個函數(shù)作為Array.from的參數(shù)。同時,第三個函數(shù)也可指定,用以確定this指向。
function translate(){
return Array.from(arguments, (value) => value + 1);
}
translate(1,2,3); // [2, 3, 4]
用Array.from轉(zhuǎn)換可迭代對象
Array.from除了可以處理類數(shù)組對象,還可以處理可迭代對象。
為所有數(shù)組添加的新方法
find
和findIndex
這兩函數(shù)都可接收兩參數(shù),第一個為回調(diào)函數(shù);為一個為可選參數(shù),確定回調(diào)函數(shù)中this的值。
let numbers = [25, 30, 35, 40, 999];
console.log(numbers.find(n => n > 33)); // 35
console.log(numbers.findIndex(n => n > 33)); // 2
如果在數(shù)組中根據(jù)某個條件查找匹配元素,那么find和findIndex是最好的選擇;如果只想查找與某個值匹配的元素,indexOf和lastIndexOf即可。
fill
使用指定的值填充一至多個數(shù)組元素。第一個參數(shù)為數(shù)值,第二個為起始位置,第三個不傳默認為終點,傳則為終止點(不包含這個位置)。二三個參數(shù)都可接收負數(shù)。
let numbers = [25, 30, 35, 40, 999];
numbers.fill(1, 2); // numbers: [25, 30, 1, 1, 1];
numbers.fill(5); // numbers: [5, 5, 5, 5, 5]
numbers.fill(6, 1, 3);// numbers: [5, 6, 6, 5, 5]
copyWithin
由定型數(shù)組引出的方法。
定型數(shù)組
為了方便高性能算術(shù)運算使用。所謂定型數(shù)組,就是將任何數(shù)字轉(zhuǎn)換為一個包含數(shù)字比特的數(shù)組,隨后就可以通過我們熟悉的JavaScript數(shù)組方法來進一步處理。
第十一章 Promise與異步編程
背景不多說,一搜一大堆,記一下平時容易疏忽的點。
眾所周知的Promise的三個狀態(tài)“pending”、“fulfilled”、“rejected”。由內(nèi)部屬性[[PromiseState]]來標記。這個屬性不暴露在Promise對象上,所以不能以編程的方式檢測Promise的狀態(tài),只有當Promise的狀態(tài)改變時,通過then方法來采取特定的行動。
Promise的對象都有then方法,接收兩個參數(shù),不再贅述。如果一個對象實現(xiàn)了上述的then方法,那這么對象我們稱之為thanable對象。所有的Promise都是thenable對象,但并非所有thenable對象都是Promise。
Promise的catch方法,相當于只給其傳入拒絕處理程序的then方法。
每次調(diào)用then方法或catch方法都會創(chuàng)建一個新任務,當Promise被解決(resolved)時執(zhí)行。這些任務最終會被加入到一個為Promise量身定制的獨立隊列中,這個任務隊列的具體細節(jié)對于理解如何使用Promise而言不重要,通常你只要理解任務隊列是如何運作的就可以了。
Promise執(zhí)行順序。
let promise = new Promise(resolve => {
console.log('in promise');
resolve(1);
})
console.log('Hi!');
promise.then(e => console.log(e));
// in promise
// Hi!
// 1
- 創(chuàng)建已處理的Promise
let promise = Promise.resolve('test');
promise.then((msg) => {
console.log(msg);
});
// test
let promise = Promise.reject('test');
promise.then(msg => {
console.log(msg)
}).catch(msg => {
console.log('catch', msg);
});
// catch test
如果向Promise.resolve()和Promise.reject()方法傳入一個Promise,那么這個Promise會被直接返回。
let promise2 = new Promise((resolve, reject) => {
setTimeout(reject(123), 30000);
})
let promise = Promise.resolve(promise2);
promise.then(msg => {
console.log(msg)
}).catch(msg => {
console.log('catch', msg);
});
// 立刻輸出 catch 123
- 非Promise的Thenable對象
Promise.resolve()和Promise.reject()都可以接收非Promise的Thenable對象作為參數(shù)。如果傳入一個非Promise的Thenable對象,則這些方法會創(chuàng)建一個新的Promise,并在then()函數(shù)中被調(diào)用。
擁有then方法,并接resolve和reject這兩個參數(shù)的普通對象就是非Promise的Thenable對象。
let thenable = {
then: (resolve, reject) => {
resolve(42);
}
}
let p1 = Promise.resolve(thenable);
p1.then(value => {
console.log(value); // 42
})
也可以reject
let thenable = {
then: (resolve, reject) => {
reject(42);
}
}
let p1 = Promise.resolve(thenable);
p1.then(value => {
console.log(value);
}).catch(value => {
console.log('error', value); // error, 42
})
如果不確定某個對象是不是Promise對象,那么可以根據(jù)預期的結(jié)果將其傳入Promise.resolve()方法中或Promise.reject()方法中,如果它是Promise對象,則不會有任何變化
- 執(zhí)行器錯誤
let promise = new Promise((resolve, reject) => {
throw new Error('explosion!');
})
promise.catch((err) => {
console.log(err.message); // "explosion"
})
// 等價于
let promise = new Promise((resolve, reject) => {
try {
throw new Error('explosion!');
} catch(e){
reject(e);
}
})
promise.catch((err) => {
console.log(err.message); // "explosion"
})
即執(zhí)行器會捕獲所有拋出的錯誤,但只有當拒絕處理程序存在時才會記錄執(zhí)行器中拋出的錯誤,否則錯誤會被忽略掉。
在早期的時候,開發(fā)人員使用Promise會遇到這種問題,后來,JavaScript環(huán)境提供了一些捕獲已拒絕Promise的鉤子函數(shù)來解決這個問題。
- 全局的Promise拒絕處理
瀏覽器通過觸發(fā)兩個事件來識別未處理的拒絕。
- unhandledrejection 在一個時間循環(huán)中,當Promise被拒絕,并且沒有提供拒絕處理程序時被調(diào)用。
- rejectionhandled 在一個時間循環(huán)后,當Promise被拒絕,拒絕處理程序執(zhí)行時觸發(fā) 。
事件接受一個有以下屬性的事件對象作為參數(shù):
- type 事件名稱(“unhandledrejection”或“rejectionhandled”)
- promise 被拒絕的Promise對象
- reason 來自Promise的拒絕值
所有參數(shù)可參考下圖
let rejected;
window.addEventListener("unhandledrejection", event => {
console.log(event, -new Date());
console.log(event.type); // unhandledrejection
console.log(event.reason.message); // Explosion!
console.log(rejected === event.promise); // true
})
window.addEventListener("rejectionhandled", event => {
console.log(event, -new Date());
console.log(event.type);
console.log(event.reason.message);
console.log(rejected === event.promise);
})
rejected = Promise.reject(new Error("Explosion!"));
setTimeout(() => {
rejected.catch(e => {
console.log('處理錯誤', e);
})
}, 5000)
函數(shù)輸出如下圖:
1是Promise.reject后沒有添加處理程序,
unhandledrejection
內(nèi)監(jiān)聽到,2是命令行輸出的報錯信息(一開始是紅色,在catch添加后變成黑色)
3是rejected.catch中輸出的log
4是Promise.reject被catch后
rejectionhandled
輸出的信息
注意,這里的setTimeout很重要,因為rejectionhandled 是監(jiān)聽的在一個時間循環(huán)后的之前未處理之后已處理的錯誤。如果直接catch錯誤,兩個事件都不會觸發(fā)。(這也符合常理,因為錯誤被我們立刻捕獲到了)
- 跟蹤未處理拒絕
對于錯誤日志,錯誤監(jiān)控,我們需要監(jiān)聽這兩個事件,并做一些什么處理。這里的handleRejection只是個例子,隨便打了console,實際應用中按需修改。
let handleRejection = (a, b) => {
console.log(a, b);
}
let possiblyUnhandedRejections = new Map();
// 如果一個拒絕沒有被處理,則將它添加到Map集合中
window.addEventListener("unhandledrejection", event => {
console.log(event);
possiblyUnhandedRejections.set(event.promise, event.reason);
})
window.addEventListener("rejectionhandled", event => {
console.log(event);
possiblyUnhandedRejections.delete(event.promise);
})
setInterval(() => {
possiblyUnhandedRejections.forEach((reason, promise) => {
console.log(reason.message ? reason.message : reason);
// 做一下什么來處理這些拒絕
handleRejection(promise, reason);
});
possiblyUnhandedRejections.clear();
}, 6000)
rejected = Promise.reject(new Error("Explosion!"));
Promise.all
傳入值是一個可迭代對象,全部成功才返回,返回值是一個數(shù)組,按順序返回。一個失敗則直接返回,返回的為失敗的信息,不再是數(shù)組。Promise.race
一個被完成即返回。
let p1 = new Promise(resolve => setTimeout(() => {
resolve(1)
}, 100));
let p2 = new Promise(resolve => resolve(2));
let p3 = Promise.resolve(3);
let p4 = Promise.race([p1, p2, p3]);
p4.then(value => {
console.log(value); // 2
})
第一個被拒絕也直接返回拒絕(下面這段代碼執(zhí)行結(jié)果和書上說明的不一致,Promise.reject或Promise.resolve并沒有比new Promise后直接返回執(zhí)行的快,書上說的是后者有一個編排過程,但似乎沒有發(fā)現(xiàn))
let p1 = new Promise(resolve => setTimeout(() => {
resolve(1)
}, 100));
let p2 = new Promise((resolve, reject) => {
resolve(2)
});
let p3 = Promise.reject(3);
let p4 = Promise.race([p1, p2, p3]);
p4.then(value => {
console.log('then', value); // then 2
}).catch(value => {
console.log('catch', value);
})