JavaScript的Iterator和for…of循环

听说你还在为JavaScript的遍历而困扰?看到[Symbol.iterator]还三脸懵B?来了解一下迭代器模式、遍历器的实现、协议规范、应用场景及常见遍历方法吧!

深入了解Symbol,比如Symbol.toStringTagSymbol.toPrimitive等其他Symbol属性可以看看我之前的文章:
JavaScript中的Symbol类型

Iterator(遍历器)

迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素

Iterator的概念

遍历器(Iterator)就是一种统一的接口机制,用来处理遍历所有不同的数据结构。 任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

  • Iterable 是指有 [Symbol.iterator] 属性的对象,这个属性 obj[Symbol.iterator] 就是 Iterator(迭代器)。也可以说可迭代对象是实现了 Symbol.iterator 方法的对象

  • 凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。

  • 如果 Symbol.iterator 方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。 遍历器对象的根本特征就是具有 next 方法。

原生具备Iterator接口的数据结构

  • Array
  • Set
  • Map
  • String
  • TypeArray: 类似于数组的结构,对底层的二进制进行描述的一种数据结构
  • 函数的argiuments对象
  • NodeList对象

以上数据类型,都有 Symbol.iterator 属性,属性值是一个函数,执行这个函数就会返回一个迭代器。这个迭代器就有 next 方法可顺序迭代子元素


判断某个对象是否可迭代:

const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';

Iterator 的作用

  • 迭代器Iterator是一种接口机制,为各种不同的数据结构提供统一的访问机制
  • 主要就是供for...of.. 使用
  • 使数据结构的成员按照某种顺序排列
  • 用一句话总结就是:让不可迭代的数据结构变得可迭代

Iterator 的遍历过程

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的 next 方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 value done 两个属性的对象。其中, value 属性是当前成员的值, done 属性是一个布尔值,表示遍历是否结束。

模拟next()实现一个遍历器

JavaScript的Iterator和for...of循环

注意:Iterator 只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象

function idMaker() {
  let index = 0;

  return {
    next: function () {
      return {value: index++, done: false}
    }
  }
}

//这是一个可以无限运行的遍历器对象
let it = new idMaker(); // {value: 0, done: false}
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 2, done: false}
console.log(it.next()); // {value: 3, done: false}
console.log(it.next()); // {value: 4, done: false}
// ......

使用ES6的class语法模拟

class MakeIterator {
  constructor(list) {
    this.list = list;
    this.index = 0;
  }

  next() {
    if (this.index < this.list.length) {
      return {
        value: this.list[this.index++],
        done: false
      }
    } else {
      return {
        value: undefined,
        done: true
      }
    }
  }
}

// 测试
let iterator = new MakeIterator([1,2,3]);
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

使用function来模拟

//自己定义的遍历器生成函数
function makeIterator(list) {
  let index = 0;
	
  // 返回一个遍历器对象
  return {
    next: function () {
      //三元表达式
      return index < list.length ? 
      	{value: list[index++], done: false} : {value: undefined, done: true}
    }
  }
}

const iterator = makeIterator([1,3,5]);
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 5, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

使用Generator函数来模拟

Symbol.iterator()方法的最简单实现:

let myIterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
    yield 3;
  }
};
[...myIterable] // [1, 2, 3]

// 或者采用下面的简洁写法

let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) {
  console.log(x);
}
// "hello"
// "world"

遍历器对象的 return(),throw()

遍历器对象除了具有next()方法,还可以具有return()方法和throw()方法。

如果自己写遍历器对象生成函数,那么next() 方法是必须部署的return()方法和throw()方法是否部署是可选的。

  • return() 方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return() 方法。 注意 ,return()方法必须返回一个对象 ,这是 Generator 语法决定的。 如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return()方法。
  • throw()方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。
function readLinesSync(file) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return { done: false };
        },
        return() {
          file.close();
          return { done: true };
        }
      };
    },
  };
}
// 函数readLinesSync接受一个文件对象作为参数,返回一个遍历器对象,
//   其中除了next()方法,还部署了return()方法。下面的两种情况,都会触发执行return()方法。

// 情况一
// 输出文件的第一行以后,就会执行return()方法,关闭这个文件
for (let line of readLinesSync(fileName)) {
  console.log(line);
  break;
}

// 情况二
// 会在执行return()方法关闭文件之后,再抛出错误
for (let line of readLinesSync(fileName)) {
  console.log(line);
  throw new Error();
}

可迭代协议和迭代器协议

通俗来理解,具有Symbol.iterator的就是遵循的可迭代协议。凡是按照next() 调用这种就都可以称之为迭代器协议。

  • 迭代器协议(iterator protocol)iterator协议定义了产生value序列的一种标准方法。只要实现符合要求的next函数,该对象就是一个迭代器。相当遍历数据结构元素的指针,类似数据库中的游标。

    • 在 JavaScript 中迭代器是一个对象,它提供了一个 next() 方法用来返回序列中的下一项。这个方法返回包含两个属性: done value
  • 可迭代协议(iterable protocol) : 一旦支持可迭代协议,意味着该对象可以用for-of来遍历,可以用来定义或者定制 JS 对象的迭代行为。常见的内建类型比如Array & Map都是支持可迭代协议的。对象必须实现@@iterator方法,意味着对象必须有一个带有@@iterator key的可以通过常量Symbol.iterator访问到的属性。

将一段不可遍历的数据结构变得可遍历:

JavaScript的Iterator和for...of循环

可以用Generator函数进行改造:

JavaScript的Iterator和for...of循环

实现一个迭代器

基于迭代器协议

// 实现
function createArrayIterator(arr) {
  let index = 0
  return {
    next: () =>
    index < arr.length
    ? { value: arr[index++], done: false }
    : { value: undefined, done: true },
  }
}

// 测试
const nums = [11, 22, 33, 44]
const numsIterator = createArrayIterator(nums)
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())

基于可迭代协议

实现了生成迭代器方法的对象称为 可迭代对象 也就是说这个对象中包含一个方法, 该方法返回一个迭代器对象

一般使用 Symbol.iterator来定义该属性, 学名叫做 @@iterator 方法

// 一个可迭代对象需要具有[Symbol.iterator]方法,并且这个方法返回一个迭代器
const obj = {
  names: ['111', '222', '333'],
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () =>
      index < this.names.length
      ? { value: this.names[index++], done: false }
      : { value: undefined, done: true },
      return: () => {
        console.log('迭代器提前终止了...')
        return { value: undefined, done: true }
      },
    }
  },
}

// 测试
for (const item of obj) {
  console.log(item)
  if (item === '222') break
}

基于生成器Generator

// 生成器
function* creatIterator (){
    yield 1
    yield 2
    yield 3
}
const iterator = creatIterator()
console.log(iterator.next()) // {value:1,done:false}
console.log(iterator.next()) // {value:2,done:false}
console.log(iterator.next()) // {value:3,done:false}
console.log(iterator.next()) // {value:undefined,done:true}

调用 Iterator 接口的场合

解构赋值

let set = new Set().add('a').add('b').add('c');

let [x,y] = set;
// x='a'; y='b'

let [first, ...rest] = set;
// first='a'; rest=['b','c'];

扩展运算符

// 例一
const str = 'hello';
[...str] //  ['h','e','l','l','o']

// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。即let arr=[...iterable];

yield*

let generator = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

const iterator = generator();

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }

其他场合

  • for...of
  • Array.from()
  • Map()Set()WeakMap()WeakSet()(比如new Map([['a',1],['b',2]])
  • Promise.all()
  • Promise.race()

for...of...循环

function each(data){
  const iterator = data[Symbol.iterator]()
  let item = iterator.next()
  while(!item.done){
    console.log(item.value)
    item = iterator.next()
  }
}

each([1,2,3,4,5])
// 输出:
// 1
// 2
// 3
// 4
// 5

ES6 借鉴 C++、Java、C# 和 Python 语言,引入了 for…of 循环,作为遍历所有数据结构的统一的方法。一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for...of 循环遍历它的成员

  • Array
  • Set
  • Map
  • String
  • TypeArray:类似于数组的结构,对底层的二进制进行描述的一种数据结构
  • 函数的argiuments对象
  • NodeList对象
  • Generator 对象

以上这些可以直接使用for...of 循环。

除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历

数组

数组原生具备 iterator 接口(即默认部署了 Symbol.iterator 属性) ,for…of 循环本质上就是调用这个接口产生的遍历器。

let arr = ['red','green','blue'];
for (let item of arr) {
  console.log(item); // red green blue
}

let obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);
for (let item of obj) {
  console.log(item); // red green blue
}
// 空对象 obj 部署了数组 arr 的 Symbol.iterator 属性,
// 结果 obj 的 for...of 循环,产生了与 arr 完全一样的结果。

注意: for...of 循环 调用遍历器接口,数组的遍历器接口 只返回具有数字索引的属性 。这一点跟 for...in 循环不一样。

let arr = [1,2,3];
arr.foo = 'webchang';

for (let i in arr) {
  console.log(i); // "0" "1" "2" "foo"
}

for (let i of arr) {
  console.log(i); // 1 2 3
}

Set 和 Map 结构

Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用 for...of 循环。

  • 遍历的顺序是按照各个成员被添加进数据结构的顺序
  • Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
// 遍历 Set 结构
let names = new Set(['小明','webchang','小红','webchang']);
for (let item of names) {
  console.log(item); // 小明 webchang 小红
}
// 遍历 Map 结构
let students = new Map();
students.set('001','小明');
students.set('002','webchang');
students.set('003','小红');
for (let item of students) {
  console.log(item);
}

for (let [num,name] of students) {
  console.log(num,name);
}

JavaScript的Iterator和for...of循环

计算生成的数据结构

  • entries() ****返回一个遍历器对象,用来遍历 [键名, 键值] 组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用 entries 方法。
  • keys() 返回一个遍历器对象,用来遍历所有的键名
  • values() 返回一个遍历器对象,用来遍历所有的键值
let arr = ['a', 'b', 'c'];
for (let pair of arr.entries()) {
  console.log(pair);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']

类似数组的对象

for...of 循环用于字符串、DOM NodeList 对象、arguments 对象的例子:

// 字符串
let str = "hello";

for (let s of str) {
  console.log(s); // h e l l o
}

// DOM NodeList对象
let paras = document.querySelectorAll("p");

for (let p of paras) {
  p.classList.add("test");
}

// arguments对象
function printArgs() {
  for (let x of arguments) {
    console.log(x);
  }
}
printArgs('a', 'b');
// 'a'
// 'b'

注意:并不是所有类似数组的对象(存在数值键名和length属性)都具有 Iterator 接口:

// 这是一个类似数组的对象,但是它没有 Iterator 接口
let arrayLike = { length: 2, 0: 'a', 1: 'b' };

可以对这些类似数组的对象部署 Iterator 接口,有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Iterator 接口,例如:(NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。)

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

注意:类数组对象才会有Symbol.iterator 方法,普通对象部署数组的 Symbol.iterator 方法,并无效果。

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}
let iterable = {
  a: 'a',
  b: 'b',
  c: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // undefined, undefined, undefined
}

对于普通的对象,for…of 结构不能直接使用

对于普通的对象for...of 结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。

一个对象如果要具备可被 for...of循环调用的 Iterator 接口,就必须在Symbol.iterator 的属性上部署遍历器生成方法( 原型链上的对象具有该方法也可

class RangeIterator {
  constructor(start, end) {
    this.value = start;
    this.stop = end;
  }

  [Symbol.iterator]() {
    return this;
  }

  next() {
    let value = this.value;
    if (value < this.stop) {
      this.value++;
      return {value: this.value, done: false};
    }

    return {value: undefined, done: true}
  }
}

let range = new RangeIterator(0,3);
for (let item of range) {
  console.log(item);
}
console.log(range);
let obj = {
  data: ['web', 'chang'],
  [Symbol.iterator]: function () {
    let self = this;
    let index = 0;
    return {
      next: function () {
        if (index < self.data.length) {
          return {value: self.data[index++], done: false}
        }
        return {value: undefined, done: true}
      }
    }
  }
}
for (let item of obj) {
  console.log(item); // "web" "chang"
}
function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}

for (let [key, value] of entries(obj)) {
  console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3

案例:

// 想要通过 for of 循环直接遍历出 price 对象中 a、b、c 三个属性对应数组的最小值
let price = {
  a: [1, 2, 3],
  b: [6, 5, 4],
  c: [8, 7, 9],
  [Symbol.iterator]() {

    let arr = []
    for (let [key, value] of Object.entries(this)) {
      arr.push(Math.min(...value));
    }

    let index = 0;

    return {
      next() {
        if (index < arr.length) {
          return {value: arr[index++], done: false}
        }
        return {value: undefined, done: true}
      }
    }
  }
}

for (let item of price) {
  console.log(item); // 1 4 7
}

与其他遍历语法的比较

for循环

for (let index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

数组的forEach方法

这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。

myArray.forEach(function (value) {
  console.log(value);
});

for...in...循环

for...in 循环可以遍历数组的键名。for...in 循环主要是为遍历对象而设计的,不适用于遍历数组

for (let index in myArray) {
  console.log(myArray[index]);
}

for...in 循环有几个缺点:

  • 数组的键名是数字,但是for...in 循环是以字符串作为键名的,“0”、“1”、“2”等等。
  • for...in 循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键
  • 某些情况下, for...in 循环会以 任意顺序 遍历键名

原文链接:https://juejin.cn/post/7405147677524017179 作者:灰太羊

(1)
上一篇 2024年8月21日 上午10:02
下一篇 2024年8月28日 上午10:03

相关推荐

发表回复

登录后才能评论