JavaScript 可迭代对象
ES6 新增了一种对象,可迭代对象 Iterable(它不是一种新的语法或新的内置对象),而是一种遵循迭代器协议 Iteration protocols 的对象。
Iterable 可迭代对象是数组的泛化,它们是通过「定制」的对象以适用于迭代循环的结构,如在 for-of 循环结构,在迭代时从对象中「自动」地一次一个的方式提取数据,是一个结构化的数据模型。内置的数据类型,如数组 Array
、集合 Set
就是可迭代对象。
迭代器协议
ES6 Iteration protocols 迭代器协议由两部分组成:
- 可迭代协议
iterable protocol
规定如何让对象变成可遍历对象,一般是在对象中添加一个名为Symbol.iterator
的方法(一个专门用于使对象可迭代的内置 symbol),该方法返回一个对象,且该对象满足迭代器协议 iterator protocol(因此返回的对象称为迭代器) - 迭代器协议
iterator protocol
定义了一个标准的方法用来产生一系列的值,如迭代器都含有next()
方法,该方法返回一个对象,对象中包含两个属性value
和done
,其中属性value
指向迭代序列中当前迭代使用的值,属性done
判断是否迭代完成。
Tip
协议就是一组特定的键值对的集合(以让对象具有特定的功能),一个对象包含了这些属性就实现了该协议。一个协议可以被多个对象实现,一个对象也可以实现多个协议。
让对象 range
可迭代,需要添加一个名为 Symbol.iterator
方法(有方法 next()
),在迭代结构,如 for-of 中,会调用对象中该方法,如果没有这种方法就会报错。
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
自身没有next()
方法。- 通过调用
range[Symbol.iterator]()
创建了另一个对象,即所谓的「迭代器」对象,使用迭代器的方法next()
会为迭代生成值。
Warning
从技术上说虽然可以将它们合并,并使用 range
自身作为迭代器来简化代码。但缺点是不能同时在对象上运行两个 for-of 循环了:它们将共享迭代状态,因为只有一个迭代器,即对象本身。但是两个并行的 for-of 是很罕见的,即使在异步情况下。
可迭代协议
可迭代协议 iterable protocol
允许 JavaScript 对象去定义或者自定义迭代行为,如什么样的对象元素可以在 for-of 循环中构建。要使对象 object
遵循可迭代协议,那么必须实现 @@iterator
方法,即对象(或对象原型链)必须有一个 Symbol.iterator
属性,其相应的值为函数,且该函数需要返回一个对象(满足迭代器协议的对象)。
属性(key) | 值(value) |
---|---|
[Symbol.iterator] | 一个包含0个参数的函数,该函数返回一个满足迭代器协议的对象 |
迭代器协议
迭代器协议 iterator protocol
定义了一个标准的方法用来产生一系列的值(无论是有限的还是无限的),即该协议规定迭代器应该是一个具有 next()
方法的对象,这个方法会返回对象其中包含两个属性 done
(布尔值)和 value
,其中属性 value
指向迭代序列中当前迭代使用的值,属性 done
判断是否迭代完成。
属性 | 值 |
---|---|
next | 一个具有0个参数的函数,返回一个包含以下两个属性的对象:done (boolean) 和 value |
- 当迭代器迭代完成,
done
则返回true
;如果迭代器还有下一个值,则返回false
value
是迭代器返回的值,当done
属性为true
时可以忽略(返回undefined
)
let obj = {
a: 1,
b: 2,
c: 3,
[Symbol.iterator]: function() {
// 该函数环境的 this 指代的是函数作为属性,其对应的对象 obj
// Object.keys(obj) 返回一个由一个给定对象自身可枚举属性的键组成的(字符串)数组
// 也可以使用 Object.values(obj) 返回属性值组成的数组
let keys = Object.keys(this); // 相当于 keys = ["a", "b", "c"]
let index = 0;
return {
next: () => { // 使用箭头函数,继承外部 this 即 obj
if(index < keys.length) {
// 以括号记法访问原对象 this 的属性值
return { value: this[keys[index++]], done: index === keys.length-1 ? true:false }
} else {
return { done: true }
}
}
}
}
}
let iterable = obj[Symbol.iterator]()
console.log(iterable.next().value); // 1
console.log(iterable.next().value); // 2
console.log(iterable.next().value); // 3
console.log(iterable.next().done); // true
Tip
注意 JavaScript 的作用域和闭包特性,推荐使用箭头函数避免函数作用域对 this 指代的影响
字符串是可迭代的,可以通过手动显式调用字符串内置的迭代器了解其原理
// 字符串就是一个内置的可迭代的对象实例
let someString = "hi";
//
// 访问属性 Symbol.iterator 并调用相应的函数,返回一个对象(迭代器)
let iterator = someString[Symbol.iterator]();
// 调用迭代器的方法 next() 获取当前的元素 value 和迭代状态 done
iterator.next(); // { value: "h", done: false }
iterator.next(); // { value: "i", done: false }
iterator.next(); // { value: undefined, done: true }
// 可以重定义迭代器,使其具有新行为
let someString = new String("hi");
// 重写字符串内置属性 Symbol.iterator 手动构建一个新的迭代器
someString[Symbol.iterator] = function() {
return { // 根据迭代器协议,迭代器具有方法 next
first: true,
next: function() {
if (this.first) {
this.first = false; // 第一次调用该方法后修改 first = false
return { value: "itbilu.com", done: false }; // 第一次迭代返回值为 itbilu.com
} else {
return { done: true }; // 这个迭代器只进行一次迭代就结束
}
}
};
};
for(let i of someString) {
console.log(i);
}; // itbilu.com
Tip
字符串迭代器能够识别代理对 surrogate pair,代理对也就是 UTF-16 扩展字符。
keys()/values()/entries()
可迭代对象是一种满足特殊数据结构的对象,它们支持方法 object.keys()
、object.values()
、object.entries()
以遍历对象中的数据(带有 enumerable
标志的非 Symbol 键、值、键值对),JavaScript 内置的可迭代对象有 Array
数组、Set
集合、Map
映射,普通对象也支持类似的方法,但是语法上有一些不同。
对于普通对象这些方法是可用的,但是请注意与可迭代对象的区别
Object.keys(obj)
返回一个包含该对象所有的键的数组。Object.values(obj)
返回一个包含该对象所有的值的数组。Object.entries(obj)
返回一个包含该对象所有[key, value]
键值对的数组。
与可迭代对象,如 map
的区别
Map | Object | |
---|---|---|
调用语法 | map.keys() | Object.keys(obj) ,而不是 obj.keys() |
返回值 | 可迭代项 | 真正的数组 |
let user = {
name: "John",
age: 30
};
Object.keys(user); // ["name", "age"]
Object.values(user); // ["John", 30]
Object.entries(user); // [ ["name","John"], ["age",30] ]
// 遍历所有的值
for (let value of Object.values(user)) {
alert(value); // John, 30
}
Warning
Object.keys/values/entries
方法,就像 for-in 循环结构一样,会忽略使用 Symbol(...)
作为键的属性。💡 如果想要迭代 Symbol 类型的键,可以单独的方法 Object.getOwnPropertySymbols(obj)
,它会返回一个只包含 Symbol 类型的键的数组(相应地 Object.getOwnPropertyNames(obj)
返回对象非 Symbol 键)。另外,还有一种方法 Reflect.ownKeys(obj)
,它会返回 所有 键。