笔记参考javascript.info中文站
- Iterable object(可迭代对象)
- 1. Symbol.iterator
- 2. 字符串的迭代
- 3. 可迭代(iterable)和类数组(array-like)
- 4. Array.from
- Map and Set(映射和集合)
- 1. Map
- 2. Map 和对象
- 3. Set
- WeakMap and WeakSet(弱映射和弱集合)
- 1. WeakMap
- 2. WeakSet
有些对象并不是数组,也不是字符串,因此不能直接进行遍历,但是我们可以用 for...of 方法对那种看上去很适合进行迭代的对象进行迭代
1. Symbol.iterator首先通过 Symbol.iterator 理解一下迭代
假设我们有一个对象 range:
// 它看起来非常适合1 2 3 4 5的迭代
let range = {
from: 1,
to: 5
};
想要对其使用 for...of 方法,首先需要给对象创建一个 Symbol.iterator 方法,也就是迭代器
当 for...of 开始运行,首先会调用迭代器,没有就会报错,随后会依次返回对象的值,并在返回后期待返回下一个值,也就是 next() 函数,该函数返回的是一个对象 {done: Boolean, value: any} ,当 done 是 true 时迭代结束,否则返回 value,也就是下一个迭代值
也就是说,我们在迭代过程中创建了一个临时对象用于表示迭代的目标,即所谓的“迭代器”对象,因此迭代器对象和它所代表的对象其实不是一个对象,是分割开的
实现:
let range = {
from: 1,
to: 5
};
// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function() {
// ……它返回迭代器对象(iterator object):
// 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值
return {
current: this.from,
last: this.to,
// 3. next() 在 for..of 的每一轮循环迭代中被调用
next() {
// 4. 它将会返回 {done:.., value :...} 格式的对象
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// 现在它可以运行了!
for (let num of range) {
alert(num); // 1, 然后是 2, 3, 4, 5
}
如果我们将 range.to 设置成 Infinite ,那么我们就得到了一个无穷迭代器
数组和字符串是使用最广泛的内建可迭代对象,其中字符串的迭代是把字符串以字符为单位遍历
for (let char of "test") {
// 触发 4 次,每个字符一次
alert( char ); // t, e, s, t
}
字符串有内建的迭代器,因此我们可以显式的调用 Symbol.iterator,就和 for...of 一样
let str = "Hello";
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // 一个接一个地输出字符
}
这样做自由度更高,但书写难度大,而且可读性相对一般
可迭代指的是:实现了 Symbol.iterator 方法的对象
类数组指的是:和数组一样拥有索引功能和 length 属性的对象
二者看起来相似但实则不一样,因此他们也并不矛盾,如字符串既是可迭代对象也是类数组,准确的说,二者并无相互关系
Array.from(obj, mapFn, thisArg) 方法可以让我们将对象、字符串之类的可迭代变量转化成数组
其中 mapFn 是允许我们自定义的迭代规则,即“映射(mapping)”函数,thisArg 则是可选参数,自定义的 this。
举个例子:
// 假设 range 来自上文的例子中 let arr = Array.from(range); alert(arr); // 1,2,3,4,5 (数组的 toString 转化方法生效)
再举个使用了自定义映射关系的例子:
// 假设 range 来自上文例子中 // 求每个数的平方 let arr = Array.from(range, num => num * num); alert(arr); // 1,4,9,16,25
事实上这个方法做的事就是一个 for 循环,只是简练很多,可读性和维护性强很多,也更加美观
Map and Set(映射和集合)
尽管数组和对象已经能解决我们绝大多数问题,但仍有一部分特殊问题并不适用,因此我们需要 Map 和 Set
1. MapMap 是一种特殊的键值对组成的变量,类似于对象,但它特殊点在于他的键可以是任何类型,不限制于字符串
他有这些方法:
- new Map() —— 创建 map。
- map.set(key, value) —— 根据键存储值。
- map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。
- map.has(key) —— 如果 key 存在则返回 true,否则返回 false。
- map.delete(key) —— 删除指定键的值。
- map.clear() —— 清空 map。
- map.size —— 返回当前元素个数。
我们知道,map 本质也是一种由键值对组成的变量,但像对象那样用 map[key] = x 的方法创建新键值对是合法但不合适的,我们应该这样使用 map:
let map = new Map();
map.set('1', 'str1'); // 字符串键
map.set(1, 'num1'); // 数字键
map.set(true, 'bool1'); // 布尔值键
// 普通 Object 会将键转化为字符串
// Map 则会保留键的类型,所以下面这两个结果不同:
alert( map.get(1) ); // 'num1'
alert( map.get('1') ); // 'str1'
alert( map.size ); // 3
我们甚至可以让对象做键,这是对象类型无法做到的
可以看到对象的键值对很特殊,因此内部对于键的比较就需要一套特殊的方法: SameValueZero 算法,它和 === 差不多,但 NaN 和 NaN 相等因此可以被用作键。
Map 类型也可以进行迭代,可选的方法有:
- map.keys() —— 遍历并返回一个包含所有键的可迭代对象,
- map.values() —— 遍历并返回一个包含所有值的可迭代对象,
- map.entries() —— 遍历并返回一个包含所有实体 [key, value] 的可迭代对象,for..of
在默认情况下使用的就是这个。
举个例子:
const recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50],
]);
console.log(recipeMap.keys()); // { 'cucumber', 'tomatoes', 'onion', 1 }
console.log(recipeMap.values()); // { 500, 350, 50, 2 }
console.log(recipeMap.entries());
另外,foreach() 函数在 map 中也适用
将普通对象转换成 Map,我们可以使用 Object.entries(obj) 内建方法:
let obj = {
name: "John",
age: 30
};
let map = new Map(Object.entries(obj));
alert( map.get('name') ); // John
Object.entries(obj) 方法返回了一个键值对组成的数组,也就是 map 需要的格式。
也可以将 map 转换成普通对象,需要使用 Object.fromEntries 方法:
let prices = Object.fromEntries([
['banana', 1],
['orange', 2],
['meat', 4]
]);
// 现在 prices = { banana: 1, orange: 2, meat: 4 }
alert(prices.orange); // 2
也可以让 Object.fromEntries 的参数是 map.entries(),因为由键值对组成的数组这样的形式正是 Object.fromEntries 所需要的格式
3. Set
Set 是集合的意思,在 Javascript 中指的是 “值的集合” 没有键,也不能有重复的元素。
主要方法有:
- new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。
- set.add(value) —— 添加一个值,返回 set 本身
- set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false。
- set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false。
- set.clear() —— 清空 set。
- set.size —— 返回元素个数。
如果我们需要记录一些不能有重复的元素的集合,那么 Set f非常合适,举个例子:
let set = new Set();
// 这些人可能会访问我们
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
// visits,一些访客来访好几次
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);
// set 只保留不重复的值
alert( set.size ); // 3
for (let user of set) {
alert(user.name); // John(然后 Pete 和 Mary)
}
这就相当于创建一个数组,每次添加元素前先用 arr.find 查找一下有没有相同的元素,但这样明显更浪费性能
当然,我们也可以用 for..of 或 forEach 来遍历 Set:
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) alert(value);
// 与 forEach 相同:
set.forEach((value, valueAgain, set) => {
alert(value);
});
for..of 函数很简单,但 forEach 有一点需要注意: forEach 有三个参数,前两个都是 value ,一样,这是为了和 map 类型适配,因为 map 有键值两种数据;第三个参数是 set,用来表示遍历的数据类型
map 中出现的迭代方法也适用于 set:
- set.keys() —— 遍历并返回一个包含所有值的可迭代对象,
- set.values() —— 与 set.keys() 作用相同,这是为了兼容 Map,
- set.entries() —— 遍历并返回一个包含所有的实体 [value, value] 的可迭代对象,它的存在也是为了兼容 Map。
WeakMap and WeakSet(弱映射和弱集合)
Javascript 有自己的垃圾处理机制,也就是 “可达性原则” ,根据这个规则,当一个变量有任何其他变量对它有所引用时,这个变量都不会被当作垃圾
因此,当一个对象被存进一个数组,只要数组存在,数组中的一切元素都不会被当作垃圾,尽管某个对象的引用被全部删掉
同理,当 Map 中的对象作为键存在时,只要 Map 本身存在,作为键的对象尽管引用全无也不会被当作垃圾
而弱映射和弱集合做出了一些改变
首先注意,WeakMap 的键必须是对象,不能是原始值,否则会报错
其次,如果作为键的对象除了 WeakMap 以外没有其他引用,那么这个对象就会被系统删除,这个键值对也会被 map 删除
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // 覆盖引用
// john 被从内存中删除了!
此外, WeakMap 不能进行迭代,没有 keys(), value(), entires() 这些方法
这是因为 Javascript 的引擎不确定何时会进行垃圾清理,如果某哦谢键值对的键所对应的对象没有其他引用,被当作了垃圾即将被处理,我们依然有可能在迭代中看到了这个键值对,这会带来很大的误会。
让我们举些例子
例1. 额外数据的存储
有些时候我们需要让作为键的对象和它映射的值共存亡,否则会导致严重的内存问题
比如我们需要记录每个活跃用户的访问次数,并将 “用户 - 访问次数” 这样的键值对记录进一个 Map,如果一个用户注销了,那么我们也就没必要再记录这个用户的访问次数
这时候 WeakMap 就非常合适
let visitsCountMap = new Map(); // map: user => visits count
// 递增用户来访次数
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
let john = { name: "John" };
countUser(john); // count his visits
// 不久之后,john 离开了
john = null;
此时我们就应该删除 John 对应的访问次数,否则用户越来越多,我们的 visitsCountMap 就会无限大,而传统的 Map 不会删除,这时我们就需要 WeakMap
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
例2. 缓存
我们可以记录函数运行的结果,以后再调用用一个对象的函数时,可以直接在缓存 cache 中获取,无需函数计算。
// cache.js
let cache = new Map();
// 计算并记住结果
function process(obj) {
if (!cache.has(obj)) {
let result = obj;
cache.set(obj, result);
}
return cache.get(obj);
}
如果我们按照上面这么写,使用 map 创建 cache,那么会导致下面的情况:
let obj = {};
let result1 = process(obj); // 计算完成
// ……稍后,来自代码的另外一个地方……
let result2 = process(obj); // 取自缓存的被记忆的结果
// ……稍后,我们不再需要这个对象时:
obj = null;
alert(cache.size); // 1(该对象依然在 cache 中,并占据着内存!)
即使对象已经不存在了,缓存中依然有旧数据占空间
如果我们使用 WeakMap 就不会有这个问题。
WeakSet 和 Set 类似,不能有重复元素,只有值,特殊点在于:元素只能是对象,同时,只有当对象本身能在其他地方被访问时,才能留在 WeakSet 中,因此它不可迭代,也没有 size 方法
因此,利用这种特性我们可以关注于兑现个别女神是否还有用,而无需关注更是否有更大的容器包含对象
例如记录访客名单,如果访客对象本身已经引用全无,那么访客就被注销,不会再存在于集合中
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
visitedSet.add(john); // John 访问了我们
visitedSet.add(pete); // 然后是 Pete
visitedSet.add(john); // John 再次访问
// visitedSet 现在有两个用户了
// 检查 John 是否来访过?
alert(visitedSet.has(john)); // true
// 检查 Mary 是否来访过?
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet 将被自动清理(即自动清除其中已失效的值 john)



