JS中的迭代器

166 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

什么是迭代器

迭代器(iterator)是确使用户可在容器对象(container,如:链表或数组)上遍历的对象,使用该接口无需关系对象的内部实现细节:

  • 其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
  • 在各种编程语言实现中,迭代器的实现方法各不相同,但基本都有迭代器,比如:Java,Python等;

从迭代器的定义中我们可以看出:迭代器是帮助我们对某个数据结构进行遍历的对象。

在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):

  • 迭代器协议定义了产生一系列值(有限个或无限个)的标准方式;
  • 在JS中这个标准就是迭代器对象中需要有一个特点的next方法;

next方法有如下的要求:

  1. 它是一个无参或者一个参数的函数,并且返回一个拥有done和value属性的对象;
  2. 对于done属性:
    1. 它是一个boolean类型的值;
    2. 如果迭代器可以产生序列中的下一个值,则为false( 这等价于没有指定 done 这个属性 );
    3. 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值;
  1. 对于value属性:
    1. 迭代器返回的任何 JavaScript 值。done 为 true 时可省略;

迭代器的代码练习

下面,我们有一个names的数组对象,我们来实现这个对象的迭代器:

const names = ['lzh', 'cjh', 'cks']

let index = 0
const namesIterator = {
  next: function() {
    if (index < names.length) {
      return { done: false, value: names[index++] }
    } else {
      return { done: true, value: undefined }
    }
  }
}

console.log(namesIterator.next()); // { done: false, value: 'lzh' }
console.log(namesIterator.next()); // { done: false, value: 'cjh' }
console.log(namesIterator.next()); // { done: false, value: 'cks' }
console.log(namesIterator.next()); // { done: true, value: undefined }
console.log(namesIterator.next()); // { done: true, value: undefined }
console.log(namesIterator.next()); // { done: true, value: undefined }

那么,如果我们希望对于每一个数组对象,都能够生成对应的迭代器呢?

我们可以将上面创建数组迭代器的代码封装成一个函数:

function createArrayIterator(arr) {
  let index = 0;
  return {
    next: function () {
      if (index < arr.length) {
        return {
          done: false,
          value: arr[index++],
        };
      } else {
        return {
          done: true,
          value: undefined,
        };
      }
    },
  };
}

const names = ["lzh", "cjh", "cks"];
const leasons = ["js", "vue", "react"];

const namesIterator = createArrayIterator(names);
const leasonsIterator = createArrayIterator(leasons);

可迭代对象

它和迭代器是不同的概念:

  • 当一个对象实现了iterable protocol(可迭代协议)时,它就是一个可迭代对象;
  • 这个对象的要求是必须实现@@iterator方法,在JS代码中我们使用Symbol.iterator访问该属性;
  • 当一个对象变成一个可迭代对象时,它可以进行某些迭代操作,比如:for...of操作其实就会调用它的@@iterator方法;

可迭代对象的代码

const iterableObj = {
  names: ["lzh", "cjh", "cks"],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.names.length) {
          return { done: false, value: this.names[index++] };
        } else {
          return { done: true, value: undefined };
        }
      },
    };
  },
};

const namesIterator = iterableObj[Symbol.iterator]();
console.log(namesIterator.next()); // { done: false, value: 'lzh' }
console.log(namesIterator.next()); // { done: false, value: 'cjh' }
console.log(namesIterator.next()); // { done: false, value: 'cks' }
console.log(namesIterator.next()); // { done: true, value: undefined }

for (const item of iterableObj) {
  console.log(item);// lzh cjh cks
}

原生可迭代对象

事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的:String、Array、Map、Set、arguments对象、NodeList集合;

// 1.数组
const names = ["lzh", "cks", "wws"];

for (const name of names) {
  console.log(name);
}
// 2.Map/Set
const map = new Map([
  ["java", 111],
  ["js", 222],
]);
for (const item of map) {
  console.log(item); // ['java', 111] ['js', 222]
}
const set = new Set([111, 222]);
for (const item of set) {
  console.log(item); // 111 222
}
// 3.argument
function foo() {
  for (const argument of arguments) {
    console.log(argument);
  }
}

foo(1, 2, 3, 4, 5);

可迭代对象的应用

可迭代对象可以被用在哪里呢?

  • JavaScript中的语法:for...of,展开语法(spread syntax),yield*,解构赋值(destructuring assignment);
  • 创建一些对象时:new Map([Iterable]),new WeakMap([Iterable]),new Set([Iterable]),new WeakSet([Iterable]);
  • 一些方法的调用:Promise.all(iterable),Promise.race(iterable),Array.from(iterable);
const iterableObj = {
  names: ["lzh", "cjh", "cks"],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.names.length) {
          return { done: false, value: this.names[index++] };
        } else {
          return { done: true, value: undefined };
        }
      },
    };
  },
};
// 1.for...of
// 2.展开语法
const newArr = [...iterableObj];
console.log(newArr); // [ 'lzh', 'cjh', 'cks' ]

// 3.Promise.all/allSettled/race/any方法传参时要求是个可迭代对象
Promise.all(iterableObj).then((value) => {
  console.log(value); // [ 'lzh', 'cjh', 'cks' ]
});

// 4.Set构造函数
const set = new Set(iterableObj);
console.log(set); // Set(3) { 'lzh', 'cjh', 'cks' }

// 5.解构赋值
const [name1, name2, name3] = [...iterableObj];
console.log(name1, name2, name3); // lzh cjh cks

注意

  • 普通对象的展开语法使用的并不是迭代器,而是ES9(ES2018)中新增的特性;
  • 普通对象的解构赋值使用的并不是迭代器,而是ES9(ES2018)中新增的特性;

自定义类的迭代

在前面我们看到Array、Set、String、Map等类创建出来的对象都是可迭代对象;

在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象;

如果我们也希望自己的类创建出来的对象默认是可迭代的,那么在设计类的时候我们就可以添加上 @@iterator 方法;

案例

创建一个Classroom的类:

  • 教室有自己的位置,名称,当前教室中的学生,这个教室可以进来(entry)新学生 ;
  • 创建的教室对象是可迭代对象,能够遍历教室中的每个学生;
class ClassRoom {
  constructor(address, name) {
    this.address = address;
    this.name = name;
    this.students = [];
  }
  entry(...students) {
    this.students.push(...students);
  }
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.students.length) {
          return { done: false, value: this.students[index++] };
        } else {
          return { done: true, value: undefined };
        }
      },
    };
  }
}

const classroom = new ClassRoom("广州市天河区", "js高级课程");
classroom.entry("111", "222", "333");
for (const student of classroom) {
  console.log(student);
}

迭代器的中断

迭代器在某些情况下会在没有完全迭代的情况下中断:

  • 比如遍历的过程中通过break、continue、return、throw中断了循环操作;
  • 比如在解构的时候,没有解构所有的值;

那么这个时候我们想要监听中断的话,可以添加return方法:

const iterableObj = {
  names: ["cks", "wws", "cjh"],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.names.length) {
          return { done: false, value: this.names[index++] };
        } else {
          return { done: true, value: undefined };
        }
      },
      return() {
        console.log("迭代中断了");
        return { done: true };
      },
    };
  },
};

for (const name of iterableObj) {
  console.log(name);
  if (name === "wws") {
    break;
  }
}