可迭代与类数组
前言
我们在学js时,有两个概念可能会弄混,可迭代对象和类数组对象。说某个对象是可迭代的,比如数组let arr = [1,2,3]
或者let map = new Map([[1, 2], [3, 4]])
,它们可以使用for of
进行遍历。然后又说某个对象是类数组,我们可以将它转换为数组,因为它有数值索引和length
属性。来看一个小例子:
let o1 = {
0: 'aa',
1: 'bb',
length: 2
}
let arr1 = Array.from(o1)
console.log(arr1); // [ 'aa', 'bb' ]
for (const item of arr1) {
console.log(item);
}
// aa
// bb
let map = new Map([[1, 2], [3, 4]])
for (const item of map) {
console.log(item);
}
// [ 1, 2 ]
// [ 3, 4 ]
如上面的例子,有一个类数组对象o1
,使用Array.from()
显示的将其转换为数组,然后这个对象就可以使用for of
进行遍历了,其实上面这个小例子就涉及到了这两个概念:
- 类数组对象(array-like object):有数值索引属性和
length
属性的对象,称为类数组对象,比如例子中的o1
- 可迭代对象(iterable object):可以使用
for of
进行遍历的对象,就称为可迭代的,比如例子中的arr1
(转换后的数组)和map
。
那么为什么有的对象可以使用for of
遍历(也即是可迭代对象),而有的对象却不可以,这是为什么呢?
可迭代对象
实际上那些可以直接使用for of遍历的对象,是系统内置实现了Symbol.iterator
迭代器方法。我们来看下面这个例子。
一个小例子
如下面的例子所示,有一个普通对象range
,显然是不可迭代的(因为它就是一个普通对象,啥也没干),我们使用for of
检验一下,果然也报错了,TypeError: range is not iterable
。那么如果我们想要让它变为可迭代的,该怎么办呢?比如,我们想实现这样的效果:使range
对象可以使用用for of
进行遍历,依次从1
输出到5
。
let range = {
from: 1,
to: 5
}
for (const iterator of range) {
console.log(iterator);
}
// TypeError: range is not iterable
我们来改写一下,既然想让这个对象变为可迭代的,那么就必须实现Symbol.iterator
迭代方法。
let range = {
from: 1,
to: 5
}
/*
1. 迭代器实际就是一个对象,对象上有 next() 方法
2. 迭代器每次调用一个next()方法后,都会返回一个标识对象
{
value: '', // 本次迭代获得的值
done: true|false // 标识是否还有下个值
}
3. for of 会获取到这个迭代对象,每遍历一次,调用一次迭代器的next方法,输出返回值中的value
*/
range[Symbol.iterator] = function () {
// 返回一个迭代器对象
return {
current: this.from,
last: this.to,
next() {
// 结束条件
if (this.current > this.last) {
return { done: true }
} else {
return {
value: this.current++, // current++ 第一次调用的时候,取值还是 1,
done: false
}
}
}
}
}
for (const item of range) {
console.log(item);
}
// 1 2 3 4 5
我们通过实现range[Symbol.iterator]
方法,让range
对象也变成了可迭代对象,是不是很简单。我们只需要注意下几点
range
本身是没有next()
方法的,是通过调用range[Symbol.iterator]()
方法,返回了一个对象,即所谓的迭代器对象,通过迭代器对象上的next()
方法获取到的值。next()
方法返回的值也是一个对象,对象的格式遵循下面的规范js{ value: '', // 本次迭代的值 done: true|false // 标识是否迭代完成 }
for of
的工作原理,实际上就是调用了对象的[Symbol.iterator]()
方法,生成了一个迭代器对象,然后每次循环就调用一次next()
方法,然后获取到返回值上的value
,当标识符done === true
时,就结束循环。
显示调用迭代器
还用上面的例子举例,其实我们可以显示的调用range
的迭代器方法,生成一个迭代器对象,然后模拟出和for of
一样的效果,比如:
// 获取迭代器对象
let iterator = range[Symbol.iterator]()
while (true) {
let result = iterator.next()
// 出口
if (result.done) {
break
}
console.log(result.value);
}
// 1 2 3 4 5
很少需要我们这样做,但是比 for..of
给了我们更多的控制权。例如,我们可以拆分迭代过程:迭代一部分,然后停止,做一些其他处理,然后再恢复迭代。
类数组对象
通过上面的学习,我们已经认清了可迭代对象的本质,实际上就是对象按照规范实现了Symbol[iterator]
方法。那什么是类数组对象呢?我们工作中总能碰到这样的场景,某个对象它是可迭代的,某个对象它是类数组,某个对象即可迭代又是个类数组。可迭代我们已经知道了,类数组的本质其实也很简单,就是那些具有数值索引和length
属性的对象。
一个小例子
如下面的例子,arrayLike
对象即是一个类数组对象,它具备了类数组对象的特征,有数值索引、有length
。但是类数组对象没有数组上的方法,比如push
、pop
等等,如果想用,就得使用call
或者apply
等改变一下this
指向,借用一下Array
原型上的方法。
let arrayLike = { // 有索引和 length 属性 => 类数组对象
0: "Hello",
1: "World",
sayHi() {
console.log('hhh');
},
length: 2
};
[].push.call(arrayLike, 'nn');
console.log(arrayLike);
/*
{
'0': 'Hello',
'1': 'World',
'2': 'nn',
sayHi: [Function: sayHi],
length: 3
}
*/
let str = [].join.call(arrayLike, '-')
console.log(str); // Hello-World-nn
我们发现,
arrayLike
对象是个类数组对象,但是它是不可迭代的,因为我们没有实现它的Symbol[iterator]
迭代器方法。而在可迭代对象中举的例子,range
对象,它是可迭代的,但它不是一个类数组,因为它没有类数组对象的特征(没有数值索引和length
)。不过在我们实际工作中,有很多对象它即是类数组,又是可迭代的。比如字符串(for..of
对它们有效,并且有数值索引和length
属性)。
Array.from
Array.from
我们常用来将一个类数组对象转换为真正的数组。其实它还可以将可迭代对象也转换为数组。
类数组转数组
先看一下类数组转数组的例子:
let arrayLike = {
0: 'hello',
1: 'world',
length: 2
}
console.log(Array.from(arrayLike)); // [ 'hello', 'world' ]
实际就是按照数值索引的位置,将对应的值塞到一个新数组中,数组的长度取决于length
的长度,长度不够就截断,长度超出就在对应位置补undefined
。
可迭代转数组
再看一个可迭代转数组的例子,还拿我们写的range
举例子。
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: true }
} else {
return {
value: this.current++, // current++ 第一次调用的时候,取值还是 1,
done: false
}
}
}
}
}
// 将可迭代对象,转换为了数组
console.log(Array.from(range)); // [ 1, 2, 3, 4, 5 ]
实际就是获取到迭代器的next().value
的值,依次塞到新数组中去,然后返回这个新数组。
总结
可迭代对象(iterable object):可以使用
for of
遍历的对象就是可迭代对象。可迭代的本质是该对象实现了
Symbol.iterator
迭代器方法。迭代器返回的是一个对象,即是所谓的迭代器对象。迭代器对象有
next()
方法,next()
方法的返回值也是一个对象,遵循这样的规范{value: 'xxx', done: true|false}
。for of
实际就是调用了一下[Symbol.iterator]()
方法,获取了迭代器对象,然后每次遍历时调用一次迭代器对象的next()
方法,获取到value
的值,当标识符done === true
时,表示遍历完成,结束for of
。类数组对象(array-like object):具有数值索引和
length
属性的对象。类数组对象没有数组上的
push、pop
等方法,要想使用的话有两个办法:- 转换为数组,使用
Array.from(arrayLike)
- 使用
call、apply、bind
改变this
的指向,借用Array
原型上的方法,比如[].push.call(arrayLike, 'xxx')
- 转换为数组,使用
Array.from
可以将可迭代对象或者类数组对象转换为一个真正的数组。- 转换类数组对象时,就是把索引值获取到,依次按照索引值塞进一个新数组中(长度由
length
属性确定,塞入的位置由索引值确定,长度不够就截断,长度超出就在对应位置塞undefined
)。 - 转换可迭代对象时,就是获取到每次迭代器对象
next().value
的值,依次塞到一个新数组中。
- 转换类数组对象时,就是把索引值获取到,依次按照索引值塞进一个新数组中(长度由