Javascript属性遍历与可迭代对象

429 阅读5分钟

属性遍历与可迭代对象

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的执行流程:

  1. 调用对象的[@@iterator]()方法,这个方法返回一个迭代器
  2. 调用迭代器的next()方法

也就是说,直接给某一个对象上添加一个key[@@iterator]的方法,这个方法用来返回一个迭代器。

注意,这里的[@@iterator]不是一个字符串keystring-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 iterableperson不可迭代。

有了之前的铺垫,我们很容易就可以想到将person声明为可迭代对象:

person[Symbol.iterator] = function() {
  return iterator; // javascript中怎么声明迭代器?
}

现在新的问题来了,我们需要返回一个迭代器;但怎么声明迭代器呢?

它的接口可以定义为:

interface Iterator {
  next(): IteratorResult
  // 按照文档的描述还应该有return()方法和throw()方法
  // 同时next()也可以接收参数,这部分内容和generator相关
  // 限于篇幅以及使用频率,这里不再展开,感兴趣的读者可以自行查看文档
}

interface IteratorResult {
  done: boolean;
  value: unknow;
}

迭代器有一个next()方法,可以用来返回一个IteratorResult对象,这个对象有两个属性:

  1. done: 迭代是否完成
  2. 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对象属性》

  1. 属性的key的类型:stringsymbol
  2. 属性是否可枚举
  3. 是否遍历继承来的属性(原型链中的属性)

我们构建一个例子用来覆盖这些情况:

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)

创建两个对象protoobj,将obj的原型对象设置为proto。同时,每个对象都有四个属性,覆盖了key的类型以及是否可枚举。

2.1 Object.keys()、Object.values()和Object.entries()

这三个方法我们放到一起说,它们都是用来获取满足以下条件的属性:

  1. 自有的(不包含原型链中的属性)
  2. 可枚举的
  3. keystring类型的

这三个方法的不同之处在于,它们的获取到属性的不同部分。从名字也可以很清楚的看到:

  1. Object.keys()用来获取属性的key
  2. Object.values()用来获取属性的value
  3. Object.entries()用来获取属性的keyvalue
// 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()

这两个方法都是用来用来获取自有属性,包括不可枚举的属性。它们之间的区别就是:

  1. getOwnPropertyNames用来获取keystring类型的属性
  2. getOwnPropertySymbols用来获取keySymbol类型的属性
// 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循环用来遍历一个对象及其原型链中的所有可枚举的、keystring类型的属性。

// 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)]
}