可迭代对象
for...of
可以用 for...of遍历数组:
let numbers = [1, 2, 3, 4, 5]
for (let num of numbers) {
console.log(num)
}
// 1
// 2
// 3
// 4
// 5
或者遍历对象属性:
let person = {
name: 'John',
age: 30
}
for (let key in person) {
console.log(key, person[key])
}
// name John
// age 30
还可以遍历字符串:
let message = 'Hello';
for (let char of message) {
console.log(char)
}
// H
// e
// l
// l
// o
迭代器
实际上我们可以定义对象的迭代器器,以及具体的迭代行为:
let range = {
from: 1,
to: 5
}
range[Symbol.iterator] = function () {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { done: false, value: this.current++ }
} else {
return { done: true }
}
}
}
}
for (let num of range) {
console.log(num)
}
// 1
// 2
// 3
// 4
// 5
就像示例中的那样,迭代器的 key 是符号类型Symbol.iterator,值是一个返回迭代器的对象,迭代器对象必须实现一个next方法,这个方法返回一个表示每次迭代结果的对象,改对象有两个属性(done和value),done 表示是否结束迭代,value 表示当前迭代获取到的值。
上面的实现是标准的迭代器设计模式,将迭代器与原始对象分离,当然也可以简单地使用原始对象作为迭代器:
let range2 = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ }
} else {
return { done: true }
}
}
}
for (let num of range2) {
console.log(num)
}
// 1
// 2
// 3
// 4
// 5
从细节上讲,两者是有区别的,第一种每次迭代开始时,获取到的是全新的迭代器对象,第二种则获取到的是同一个迭代器对象(原始对象),只不过重置了迭代标识(current)罢了。这个区别在单线程迭代的时候时没问题的,但如果多线程迭代,两个线程就会互相影响,导致bug。在 JS 中这个问题似乎几乎没有影响,因为 JS 是单线程的(大多数情况下)。
可迭代对象和类数组
可迭代对象和类数组虽然有点类似,但它们的定义不同:
-
可迭代对象:实现了 Symbol.iterator 方法的对象。
-
类数组:有数字索引和 length 属性的对象。
对于可迭代对象,可以使用for..of进行遍历,类数组则不行,除非它同时也是一个可迭代对象。比如字符串,即是可迭代对象,又是类数组。
Array.from
使用Array.from可以将一个可迭代对象或类数组转换为数组:
let array = Array.from(range2);
console.log(array);
// [ 1, 2, 3, 4, 5 ]
let arrayLike = {
'0': 'Hello',
'1': 'World',
'2': '!',
length: 3
}
let arr = Array.from(arrayLike);
console.log(arr);
// [ 'Hello', 'World', '!' ]
遍历属性
Object.keys
let salaries = {
"John": 100,
"Pete": 300,
"Mary": 250
};
console.log(Object.keys(salaries));
// [ 'John', 'Pete', 'Mary' ]
console.log(Object.values(salaries));
// [ 100, 300, 250 ]
console.log(Object.entries(salaries));
// [ [ 'John', 100 ], [ 'Pete', 300 ], [ 'Mary', 250 ] ]
就像示例中展示的,可以用这些方法遍历属性:
-
Object.keys,返回对象属性的 key 组成的数组 -
Object.values,返回对象属性的值组成的数组 -
Object.entries,返回对象属性的键值对组成的数组
可以利用这些方法获取对象的属性个数:
function count(obj) {
return Object.keys(obj).length;
}
console.log(count(salaries));
// 3
再看一个示例,遍历属性计算总和:
function sumSalaries(salaries) {
let sum = 0;
for (let salary of Object.values(salaries)) {
sum += salary;
}
return sum;
}
console.log(sumSalaries(salaries));
console.log(sumSalaries({}));
Object.fromEntries
使用Object.fromEntries可以将键值对数组“转换”为对象:
let person = Object.fromEntries([["name", "John"], ["age", 30]]);
console.log(person);
// { name: 'John', age: 30 }
比如利用这一点获取一个金额翻倍的新对象:
let salaries2 = {
"John": 100,
"Pete": 300,
"Mary": 250
};
function doubleSalaries(salaries) {
let entries = Object.entries(salaries).map(([name, salary]) => [name, salary * 2])
return Object.fromEntries(entries);
}
console.log(doubleSalaries(salaries2));
// { John: 200, Pete: 600, Mary: 500 }
Map
Map 与其他语言中的用法类似:
let map = new Map();
map.set('name', 'John');
map.set('age', 30);
console.log(map.get('name'));
console.log(map.get('age'));
// John
// 30
console.log(map.size);
// 2
区别是 Map 对于 key 的要求更为宽泛,比如可以使用对象作为 key,这在对象属性定义时是不行的:
let emails = new Map();
let john = { name: 'John' };
let jack = { name: 'Jack' };
emails.set(john, 'john@mail');
emails.set(jack, 'jack@mail');
console.log(emails.get(john));
console.log(emails.get(jack));
// john@mail
// jack@mail
与 Java 实现 HashMap 时使用对象的
hashCode()和equals()方法比对不同,javascript 使用===比较判断 Map 的 key 是否相同。
此外,Map 的 key key 如果是数字,它们不会被转换为字符串(像对象的 key 那样):
let map2 = new Map();
map2.set(1, 'one');
map2.set('1', 'two');
console.log(map2.size);
console.log(map2.get(1));
console.log(map2.get('1'));
// 2
// one
// two
基于上面的原因,不建议使用
map[...]的方式访问 Map 的属性,这样做会把 Map 对象作为一个普通对象进行处理,推荐使用map.get(...)获取 Map 的属性。
Map 的 Set 方法返回的是 Map 对象自己,因此可以链式调用:
let map3 = new Map();
map3.set(true, 'boolean')
.set(1, 'number');
console.log(map3.get(true));
console.log(map3.get(1));
// boolean
// number
遍历 Map
Map 是一个可迭代对象,迭代器返回的值是一个键值对:
let map4 = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three']
]);
for (let [key, value] of map4) {
console.log(key + ': ' + value);
}
// 1: one
// 2: two
// 3: three
与遍历对象属性类似,Map 也有类似的方法:
-
map.keys
-
map.values
-
map.entries
let map5 = new Map([
['1', 'one'],
['2', 'two'],
['3', 'three']
]);
for (let [key, value] of map5.entries()) {
console.log(key + ': ' + value);
}
// 1: one
// 2: two
// 3: three
效果与之前相同,只不过这里不是隐式使用迭代器进行遍历,而是显式通过entries方法获取到的键值对迭代器进行遍历。需要注意的是,Map 的这些方法返回的并不是数组,而是可迭代对象,虽然在使用forEach遍历时两者表现是相同的,但它们的数据类型并不相同。
将对象转换为 Map
Map 的构造器可以接收一个键值对数组用以初始化 Map:
let map6 = new Map([
["name", "jack"],
["age", 16]
]);
console.log(map6.get("name"));
可以利用这一点实现将普通对象转换为 Map 对象:
function getMapFromObj(obj){
if(!obj){
return null;
}
return new Map(Object.entries(obj));
}
let map7 = getMapFromObj({name: 'jack', age: 16});
console.log(map7.get('name'));
console.log(map7.get('age'));
// jack
// 16
将 Map 转换为对象
类似的,也可以利用这一点将 Map 转换为普通对象:
function getObjFromMap(map){
if(!map){
return null;
}
return Object.fromEntries(map.entries());
}
let map8 = new Map([
['name', 'jack'],
['age', 16]
]);
let obj = getObjFromMap(map8);
console.log(obj.name);
console.log(obj.age);
// jack
// 16
实际上这里可以简化一点,可以使用Object.fromEntries(map),因为fromEntries方法除了接收一个键值对数组外,还可以接收一个可迭代对象。
Set
Set 与 Map 类似,不过只存储不重复的值:
let set = new Set();
let person1 = { name: 'jack' };
let person2 = { name: 'tom' };
let person3 = { name: 'lucy' };
set.add(person1).add(person2).add(person3);
set.add(person1);
console.log(set.size);
// 3
可以使用 for...of遍历 Set:
for (let person of set) {
console.log(person.name);
}
// jack
// tom
// lucy
也可以使用forEach方法,不过有点奇怪:
set.forEach((person, personAgain, set) => {
console.log(person.name);
});
// jack
// tom
// lucy
方法中被遍历的元素在匿名函数列表中出现了两次(person 和 personAgain),这么做是为了和 Map 的 forEach 方法保持形式上的兼容。
The End.
本文的所有示例代码可以从获取。
参考资料

文章评论