JavaScript 可迭代对象

javascript

JavaScript 可迭代对象

Tip

ES6 新增了一种对象,可迭代对象 Iterable(它不是一种新的语法或新的内置对象),而是一种遵循迭代器协议 Iteration protocols 的对象。

Iterable 可迭代对象是数组的泛化,它们是通过「定制」的对象以适用于迭代循环的结构,如在 for-of 循环结构,在迭代时从对象中「自动」地一次一个的方式提取数据,是一个结构化的数据模型。内置的数据类型,如数组 Array、集合 Set 就是可迭代对象。

迭代器协议

ES6 Iteration protocols 迭代器协议由两部分组成:

  • 可迭代协议 iterable protocol 规定如何让对象变成可遍历对象,一般是在对象中添加一个名为 Symbol.iterator 的方法(一个专门用于使对象可迭代的内置 symbol),该方法返回一个对象,且该对象满足迭代器协议 iterator protocol(因此返回的对象称为迭代器)
  • 迭代器协议 iterator protocol 定义了一个标准的方法用来产生一系列的值,如迭代器都含有 next() 方法,该方法返回一个对象,对象中包含两个属性 valuedone,其中属性 value 指向迭代序列中当前迭代使用的值,属性 done 判断是否迭代完成。
Tip

协议就是一组特定的键值对的集合(以让对象具有特定的功能),一个对象包含了这些属性就实现了该协议。一个协议可以被多个对象实现,一个对象也可以实现多个协议。

让对象 range 可迭代,需要添加一个名为 Symbol.iterator 方法(有方法 next()),在迭代结构,如 for-of 中,会调用对象中该方法,如果没有这种方法就会报错。

js
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
js
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 指代的影响

字符串是可迭代的,可以通过手动显式调用字符串内置的迭代器了解其原理

js
// 字符串就是一个内置的可迭代的对象实例
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 的区别

MapObject
调用语法map.keys()Object.keys(obj),而不是 obj.keys()
返回值可迭代项真正的数组
js
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),它会返回 所有 键。


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes