第七章 Set集合与Map集合
Set集合是一种无重复元素的列表,开发者们一般不会逐一读取数组中的元素,也不大可能逐一访问Set集合中的每个元素,通常的做法是检测给定的值在某个集合中是否存在。
Map集合内含多组键值对,集合中每个元素分别存放着可访问的键名和他对应的值,Map集合经常被用于缓存频繁取用的数据。
1. ES5中使用对象模拟Set、Map集合的问题
对象的键值会被自动转化为字符串
var map = Object.create(null);
map[5] = "foo";
console.log(map["5"]); // foo
var key1 = {};
var key2 = {};
map[key1] = "foo"; // key被转换为 [object Object]
console.log(map[key2]); // foo
2. ES6中的Set集合
在Set集合中不会对所存值进行强制的类型转换,引擎内部使用Object.is()方法检测两个值是否一致。
Set具有的基本方法: add
, has
, delete
, clear
size
属性可以获取集合中目前的元素数量。
let set = new Set();
/************ add 添加元素 ****************/
set.add(5);
set.add("5");
console.log(set.size); // 2
set.add("5"); // 重复 - 本次调用直接被忽略
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转数组
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方法的回调函数接收一下三个参数:
- Set集合中下一次索引的位置
- 与第一个参数一样的值
- 被遍历的Set集合本身
我们可以看到第一二个参数的值是一模一样的,这是因为为了和数组的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方法的第二个参数也和数组一样,如果需要在回调函数中使用this,则可以将它作为第二个参数传入forEach()函数。当然,也可以使用箭头函数。
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()方法操作集合中的每一个元素,但是你不能像访问数组元素那样直接通过索引来访问集合中的元素,如有需要,请先转换为数组。
2.3 Weak Set 集合
将对象存储在Set的实例与存储在变量中一样,只要Set实例中的引用存在,垃圾回收机制就不能释放该对象的内存空间,于是之前提到的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元素的最后一个引用(这个情景被称为内存泄漏)。
为了解决这个问题,ES6引入了Weak Set集合。Weak Set集合只存储对象的弱引用,并且不可以存储原始值;集合中的弱引用如果是对象唯一的引用,则会被回收并释放相应内存。
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的主要区别
最大区别是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三个方法传入非对象参数都会报错
- WeakSet不可迭代,不能被用于for-of
- WeakSet不暴露任何迭代器(例如keys、values方法),所以无法通过程序本身来检测其中的内容
- 不支持forEach方法
- 不支持size属性
Weak Set集合的功能看似受限,其实这是为了让它能够正确地处理内存中的数据。总之,如果你只需要跟踪对象引用,你更应该使用Weak Set集合而不是Set集合。
Set类型可以用来处理列表中的值,但是不适用于处理键值对这样的信息结构。ES6中添加了Map集合来解决类似的问题。
ES6中的Map集合
ES6中的Map类型是一种存储着许多键值对的有序列表,其中的键名和对应的值支持所有的数据类型。键名的等价性判断是通过调用Object.is()
方法实现的。
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集合可以接受任意数据类型的键名,为了确保它们在被存储到Map集合中之前不会被强制转换为其他数据类型,因而只能将他们放在数组中,因为这是唯一一种可以准确地呈现键名类型的方式。
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()方法的毁掉函数,而在数组中,会按照数值型索引值的顺序依次传入回调函数。
Weak Map集合
键名必须是对象,否则会报错;
集合中保存的是对象的弱引用,如果在弱引用之外不存在其他的强引用,会被垃圾回收;但是只有集合的键名遵从这个规则,键名对应的键值如果是一个对象,则保存的是对象的强引用,不会触发垃圾回收
支持的方法:
- set
- get
- has
- delete
和Weak Set一致,不支持size属性。从而无法验证集合是否为空,同样,(在键名对象销毁后)由于没有键对应的引用,因而无法通过get()方法获取到相应的值。(就是无脑相信引擎已经给他干掉了)
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集合为空
- 存私有对象数据
ES5中实现
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中的数据就永远不会消失。而使用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;
}())
只要对象实例销毁,相关信息也被销毁,从而保证了信息的私有性。
什么时候使用WeakMap
只用对象作为集合的键名?WeakMap : Map
需要forEach()||size||clear() ? Map : WeakMap
第十章 改进的数组功能
Array.of()
出这个Api的目的是规范化使用构造函数创建数组时的怪异行为。
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构造函数传入一个数值型的数,那么数组的length属性会被设为该值;如果传入多个值,无论是否是数值型,都会变成数组的元素。
Array.of
方法,无论参数是什么类型,无论参数有多少个,Array.of()总会创建一个包含所有参数的数组。
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
处理类数组转换。例如一个处理数组类型转换的函数:
// 旧方法:
Array.prototype.slice.call(arrayLike);
// 新方法:
Array.from(arrayLike);
映射转换
如果想进一步转换,可以提供给一个函数作为Array.from的参数。同时,第三个函数也可指定,用以确定this指向。
function translate(){
return Array.from(arguments, (value) => value + 1);
}
translate(1,2,3); // [2, 3, 4]
用Array.from转换可迭代对象
Array.from除了可以处理类数组对象,还可以处理可迭代对象。
为所有数组添加的新方法
find
和findIndex
这两函数都可接收两参数,第一个为回调函数;为一个为可选参数,确定回调函数中this的值。
let numbers = [25, 30, 35, 40, 999];
console.log(numbers.find(n => n > 33)); // 35
console.log(numbers.findIndex(n => n > 33)); // 2
如果在数组中根据某个条件查找匹配元素,那么find和findIndex是最好的选择;如果只想查找与某个值匹配的元素,indexOf和lastIndexOf即可。
fill
使用指定的值填充一至多个数组元素。第一个参数为数值,第二个为起始位置,第三个不传默认为终点,传则为终止点(不包含这个位置)。二三个参数都可接收负数。
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
由定型数组引出的方法。
定型数组
为了方便高性能算术运算使用。所谓定型数组,就是将任何数字转换为一个包含数字比特的数组,随后就可以通过我们熟悉的JavaScript数组方法来进一步处理。
第十一章 Promise与异步编程
背景不多说,一搜一大堆,记一下平时容易疏忽的点。
众所周知的Promise的三个状态“pending”、“fulfilled”、“rejected”。由内部属性[[PromiseState]]来标记。这个属性不暴露在Promise对象上,所以不能以编程的方式检测Promise的状态,只有当Promise的状态改变时,通过then方法来采取特定的行动。
Promise的对象都有then方法,接收两个参数,不再赘述。如果一个对象实现了上述的then方法,那这么对象我们称之为thanable对象。所有的Promise都是thenable对象,但并非所有thenable对象都是Promise。
Promise的catch方法,相当于只给其传入拒绝处理程序的then方法。
每次调用then方法或catch方法都会创建一个新任务,当Promise被解决(resolved)时执行。这些任务最终会被加入到一个为Promise量身定制的独立队列中,这个任务队列的具体细节对于理解如何使用Promise而言不重要,通常你只要理解任务队列是如何运作的就可以了。
Promise执行顺序。
let promise = new Promise(resolve => {
console.log('in promise');
resolve(1);
})
console.log('Hi!');
promise.then(e => console.log(e));
// in promise
// Hi!
// 1
- 创建已处理的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对象作为参数。如果传入一个非Promise的Thenable对象,则这些方法会创建一个新的Promise,并在then()函数中被调用。
拥有then方法,并接resolve和reject这两个参数的普通对象就是非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对象,那么可以根据预期的结果将其传入Promise.resolve()方法中或Promise.reject()方法中,如果它是Promise对象,则不会有任何变化
- 执行器错误
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"
})
即执行器会捕获所有抛出的错误,但只有当拒绝处理程序存在时才会记录执行器中抛出的错误,否则错误会被忽略掉。
在早期的时候,开发人员使用Promise会遇到这种问题,后来,JavaScript环境提供了一些捕获已拒绝Promise的钩子函数来解决这个问题。
- 全局的Promise拒绝处理
浏览器通过触发两个事件来识别未处理的拒绝。
- unhandledrejection 在一个时间循环中,当Promise被拒绝,并且没有提供拒绝处理程序时被调用。
- rejectionhandled 在一个时间循环后,当Promise被拒绝,拒绝处理程序执行时触发 。
事件接受一个有以下属性的事件对象作为参数:
- type 事件名称(“unhandledrejection”或“rejectionhandled”)
- promise 被拒绝的Promise对象
- reason 来自Promise的拒绝值
所有参数可参考下图
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)
函数输出如下图:
1是Promise.reject后没有添加处理程序,
unhandledrejection
内监听到,2是命令行输出的报错信息(一开始是红色,在catch添加后变成黑色)
3是rejected.catch中输出的log
4是Promise.reject被catch后
rejectionhandled
输出的信息
注意,这里的setTimeout很重要,因为rejectionhandled 是监听的在一个时间循环后的之前未处理之后已处理的错误。如果直接catch错误,两个事件都不会触发。(这也符合常理,因为错误被我们立刻捕获到了)
- 跟踪未处理拒绝
对于错误日志,错误监控,我们需要监听这两个事件,并做一些什么处理。这里的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
传入值是一个可迭代对象,全部成功才返回,返回值是一个数组,按顺序返回。一个失败则直接返回,返回的为失败的信息,不再是数组。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
})
第一个被拒绝也直接返回拒绝(下面这段代码执行结果和书上说明的不一致,Promise.reject或Promise.resolve并没有比new Promise后直接返回执行的快,书上说的是后者有一个编排过程,但似乎没有发现)
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);
})