ES6學習筆記三 (Set、Map、改進的數(shù)組功能、Promise與異步編程)

第七章 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只有addhasdelete三個方法。

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的用處
  1. 一般用來存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集合為空
(但是此時用Chrome輸出map會輸出這些的信息,像是沒回收)
  1. 存私有對象數(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ù)組添加的新方法

findfindIndex

這兩函數(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與異步編程

背景不多說,一搜一大堆,記一下平時容易疏忽的點。

  1. 眾所周知的Promise的三個狀態(tài)“pending”、“fulfilled”、“rejected”。由內(nèi)部屬性[[PromiseState]]來標記。這個屬性不暴露在Promise對象上,所以不能以編程的方式檢測Promise的狀態(tài),只有當Promise的狀態(tài)改變時,通過then方法來采取特定的行動。

  2. Promise的對象都有then方法,接收兩個參數(shù),不再贅述。如果一個對象實現(xiàn)了上述的then方法,那這么對象我們稱之為thanable對象。所有的Promise都是thenable對象,但并非所有thenable對象都是Promise。

  3. Promise的catch方法,相當于只給其傳入拒絕處理程序的then方法。

  4. 每次調(diào)用then方法或catch方法都會創(chuàng)建一個新任務,當Promise被解決(resolved)時執(zhí)行。這些任務最終會被加入到一個為Promise量身定制的獨立隊列中,這個任務隊列的具體細節(jié)對于理解如何使用Promise而言不重要,通常你只要理解任務隊列是如何運作的就可以了。

  5. 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
  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
  1. 非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對象,則不會有任何變化

  1. 執(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ù)來解決這個問題。

  1. 全局的Promise拒絕處理
    瀏覽器通過觸發(fā)兩個事件來識別未處理的拒絕。
  • unhandledrejection 在一個時間循環(huán),當Promise被拒絕,并且沒有提供拒絕處理程序時被調(diào)用。
  • rejectionhandled 在一個時間循環(huán),當Promise被拒絕,拒絕處理程序執(zhí)行時觸發(fā) 。

事件接受一個有以下屬性的事件對象作為參數(shù):

  • type 事件名稱(“unhandledrejection”或“rejectionhandled”)
  • promise 被拒絕的Promise對象
  • reason 來自Promise的拒絕值

所有參數(shù)可參考下圖


unhandledrejection
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ā)。(這也符合常理,因為錯誤被我們立刻捕獲到了)

  1. 跟蹤未處理拒絕
    對于錯誤日志,錯誤監(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!"));
  1. Promise.all
    傳入值是一個可迭代對象,全部成功才返回,返回值是一個數(shù)組,按順序返回。一個失敗則直接返回,返回的為失敗的信息,不再是數(shù)組。

  2. 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);
})
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容