属性遍历与可迭代对象
1.可迭代对象与for-of循环
for-of循环用来遍历可迭代(iterable)对象。那什么是可迭代对象呢?
1.1 可迭代对象
如果学习过其他的面向对象语言,对于可迭代对象一定不陌生;关于迭代的设计,通常是实现一个Iterable接口,这个接口中有一个用来返回迭代器的方法。
在Javascript中,怎么实现Iterable接口来返回迭代器呢?
先引用一段MDN文档关于for-of循环的描述:
When a for...of loop iterates over an iterable, it first calls the iterable's [@@iterator]() method, which returns an iterator, and then repeatedly calls the resulting iterator's next() method to produce the sequence of values to be assigned to variable.
即,for-of的执行流程:
- 调用对象的
[@@iterator]()方法,这个方法返回一个迭代器 - 调用迭代器的
next()方法
也就是说,直接给某一个对象上添加一个key为[@@iterator]的方法,这个方法用来返回一个迭代器。
注意,这里的[@@iterator]不是一个字符串key(string-keyed),而是一个Symbol key,它已经被定义在了Symbol.iterator。
总结一下,在Javascript中,对象通过实现[@@iterator]方法(用来返回迭代器),就成为了一个可迭代对象。
obj[Symbol.iterator] = function() {
return iterator
}
最后,还要强调的一点就是,可以通过原型链,给某一个类型的所有实例实现[@@iterator]方法。
例如,我们想要自己封装一个可迭代的链表,可能会有下面这样的写法:
class LinkedList {
constructor() {
// ....
this[Symbol.iterator] = function() {
//...
}
}
}
对于所有的LinkedList实例来说,返回迭代器的方法应该是一致的,因此,我们可以直接放在原型上:
class LinkedList {
constructor() {
// ....
}
[Symbol.iterator]() {
// ...
}
}
1.2 迭代器
先来看一段简单的代码:
const person = {
name: 'Hello',
age: 20
}
for(const key of person) {
console.log(key)
}
很显然person是一个不可迭代的对象,然后浏览器也给出了提示:Uncaught TypeError: person is not iterable,person不可迭代。
有了之前的铺垫,我们很容易就可以想到将person声明为可迭代对象:
person[Symbol.iterator] = function() {
return iterator; // javascript中怎么声明迭代器?
}
现在新的问题来了,我们需要返回一个迭代器;但怎么声明迭代器呢?
它的接口可以定义为:
interface Iterator {
next(): IteratorResult
// 按照文档的描述还应该有return()方法和throw()方法
// 同时next()也可以接收参数,这部分内容和generator相关
// 限于篇幅以及使用频率,这里不再展开,感兴趣的读者可以自行查看文档
}
interface IteratorResult {
done: boolean;
value: unknow;
}
迭代器有一个next()方法,可以用来返回一个IteratorResult对象,这个对象有两个属性:
- done: 迭代是否完成
- value: 本次迭代的值
介绍完什么是迭代器后,我们继续回到上面的例子,通过for-of来遍历一个对象的属性:
const person = {
name: 'Hello',
age: 20,
[Symbol.iterator]() {
return {
items: Object.keys(this),
cursor: 0,
next() {
const current = this.cursor++;
return {
value: this.items[current],
done: current === this.items.length
}
}
}
}
}
for(const key of person) {
console.log(key)
// name
// age
}
const iter = person[Symbol.iterator]()
console.log(iter.next()) // {value: 'name', done: false}
console.log(iter.next()) // {value: 'age', done: false}
console.log(iter.next()) // {value: undefined, done: true}
需要注意:对于最后一个元素,上面的例子中就是{value: 'age', done: false},并不是{value: 'age', done: true}。对于{done: true}的迭代结果,表示迭代结束,并且value会被忽略。
我们这里手写了一个迭代器,但是用的更多的是使用generator,为了好理解,这里就不用generator了,文章最后也会给出使用generator的例子。
2.遍历对象属性
关于对象属性的遍历,这里涉及到了:如果对属性描述符不熟悉,可以看我的另一篇文章《Javascript对象属性》
- 属性的
key的类型:string或symbol - 属性是否可枚举
- 是否遍历继承来的属性(原型链中的属性)
我们构建一个例子用来覆盖这些情况:
function createObject(id, proto) {
const obj = Object.create(proto)
Object.defineProperty(obj, `${id}:string-enumerable`, {
enumerable: true,
value: `v:${id}:string-enumerable`
})
Object.defineProperty(obj, `${id}:string-unenumerable`, {
enumerable: false,
value: `v:${id}:string-unenumerable`
})
const symbolProEnumerable = Symbol(`${id}:symbol-enumerable`)
Object.defineProperty(obj, symbolProEnumerable, {
enumerable: true,
value: `v:${id}:symbol-enumerable`
})
const symbolPro = Symbol(`${id}:symbol-unenumerable`)
Object.defineProperty(obj, symbolPro, {
enumerable: false,
value: `v:${id}:symbol-unenumerable`
})
return obj
}
const proto = createObject('proto', null)
const obj = createObject('obj', proto)
创建两个对象proto和obj,将obj的原型对象设置为proto。同时,每个对象都有四个属性,覆盖了key的类型以及是否可枚举。
2.1 Object.keys()、Object.values()和Object.entries()
这三个方法我们放到一起说,它们都是用来获取满足以下条件的属性:
- 自有的(不包含原型链中的属性)
- 可枚举的
key为string类型的
这三个方法的不同之处在于,它们的获取到属性的不同部分。从名字也可以很清楚的看到:
Object.keys()用来获取属性的key;Object.values()用来获取属性的value;Object.entries()用来获取属性的key和value
// 1. Object.keys() 自有可枚举的string属性的key
console.log(Object.keys(obj)) // ['obj:string-enumerable']
// 2. Object.values() 自有可枚举的string属性的value
console.log(Object.values(obj)) // ['v:obj:string-enumerable']
// 3. Object.entries() 自有可枚举的string属性
console.log(Object.entries(obj)) // [['obj:string-enumerable', 'v:obj:string-enumerable']]
2.2 Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()
这两个方法都是用来用来获取自有属性,包括不可枚举的属性。它们之间的区别就是:
getOwnPropertyNames用来获取key是string类型的属性getOwnPropertySymbols用来获取key是Symbol类型的属性
// 4. Object.getOwnPropertyNames() 自有string属性
console.log(Object.getOwnPropertyNames(obj)) // ['obj:string-enumerable', 'obj:string-unenumerable']
// 5. Object.getOwnPropertySymbols() 自有symbol属性
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(obj:symbol-enumerable), Symbol(obj:symbol-unenumerable)]
2.3 Reflect.ownKeys()
这个方法用来获取一个对象所有的自由属性,相当于Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()的并集。
// 6. Reflect.ownKeys() 所有自有属性
console.log(Reflect.ownKeys(obj)) // ['obj:string-enumerable', 'obj:string-unenumerable', Symbol(obj:symbol-enumerable), Symbol(obj:symbol-unenumerable)]
2.4 for-in
for-in循环用来遍历一个对象及其原型链中的所有可枚举的、key为string类型的属性。
// 7. for-in 自有+继承的可枚举string属性
{
const keys = []
for(const key in obj) {
keys.push(key)
}
console.log(keys) // ['obj:string-enumerable', 'proto:string-enumerable']
}
2.5 for-of
把for-of写到这里感觉有点抬杠。。。
想要用for-of遍历对象属性,需要我们实现[@@iterator]方法。
// 8. for-of
function enableIterable(obj) {
const keys = Reflect.ownKeys(obj)
obj[Symbol.iterator] = function*() {
for(const key of keys) {
yield key
}
}
}
{
enableIterable(obj)
const keys = []
for(const key of obj) {
keys.push(key)
}
console.log(keys) // ['obj:string-enumerable', 'obj:string-unenumerable', Symbol(obj:symbol-enumerable), Symbol(obj:symbol-unenumerable)]
}