1.迭代器

1.1 认识迭代器

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

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

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

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

  • 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式
  • 在JavaScript中这个标准就是一个特定的next方法

next方法要求:next方法是一个无参数或者有一个参数的函数,返回结果应当拥有以下两个属性的对象:

1.done(boolean) : done返回一个布尔值: 可能是false, 可能是true

  • 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
  • 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即这个值为迭代结束之后默认返回值。

2. value:value的返回值: 可能是一个具体的值, 可能是undefined

  • 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

1.2 理解迭代器

上面的概念可能会有些抽象 , 下面我们来看看代码帮助我们理解next方法需要满足的要求 :

  1. 创建一个满足要求的迭代器
const arr = [10, 20, 30]

// 给数组arr创建一个迭代器
// 1.迭代器是一个对象 首先我们创建一个对象
const arrIterator = {
  // 2.迭代器中有一个next方法, 这个函数要求无参数或者仅有一个参数, 在对象中添加next方法
  next: function() {
    // 3.next方法需要返回一个对象, 且对象中需要包含done和value两个属性
    return {done: false, value: 10}
  }
}
  1. 但是这个迭代器对象还并不能帮助我们对数组进行迭代, 我们需要实现如下功能:
// 第一次调用迭代器, 由于没有完成所有的迭代, done应该为false, value应该为10
console.log(arrIterator.next())
// 第二次调用迭代器, 由于没有完成所有的迭代, done应该为false, value应该为20
console.log(arrIterator.next())
// 第三次调用迭代器, 由于没有完成所有的迭代, done应该为false, value应该为30
console.log(arrIterator.next())
// 第四次调用迭代器, 由于已经完成所有的迭代, done应该为true, value应该为undefined
console.log(arrIterator.next())
  1. 添加一个if添加语句, 我们就可以实现如上功能, 但是这个迭代器是arr这个数组独有的迭代器, 不具备通用性
const arr = [10, 20, 30]

// 给数组arr创建一个迭代器
let index = 0
// 1.迭代器是一个对象 首先我们创建一个对象
const arrIterator = {
  // 2.迭代器中有一个next方法, 这个函数要求无参数或者仅有一个参数, 在对象中添加next方法
  next: function() {
    // 3.next方法需要返回一个对象, 且对象中需要包含done和value两个属性
    if (index < arr.length) {
      return { done: false, value: arr[index++] }
    } else {
      return { done:true, value: undefined }
    }
  }
}

console.log(arrIterator.next()) // {done: false, value: 10}
console.log(arrIterator.next()) // {done: false, value: 20}
console.log(arrIterator.next()) // {done: false, value: 30}
console.log(arrIterator.next()) // {done: true, value: undefined}
  1. 我们封装一个函数, 让上面我们实现的迭代器具有通用性
// 例如有两个数组
const nums = [10, 20, 30, 40]
const names = ["aaa", "bbb", "ccc", "ddd"]

// 封装一个用于创建迭代器的函数
function createArrayIterator(arr) {
  let index = 0
  return {
    next: function() {
      if (index < arr.length) {
        return { done: false, value: arr[index++] }
      } else {
        // value不写, 默认就是undefined
        return { done: true }
      }
    }
  }
}

// 调用函数创建nums构造器
const numsIterator = createArrayIterator(nums)
console.log(numsIterator.next()) // {done: false, value: 10}
console.log(numsIterator.next()) // {done: false, value: 20}
console.log(numsIterator.next()) // {done: false, value: 30}
console.log(numsIterator.next()) // {done: false, value: 40}
console.log(numsIterator.next()) // {done: true}

// 调用函数创建names构造器
const namesIterator = createArrayIterator(names)
console.log(namesIterator.next()) // {done: false, value: 'aaa'}
console.log(namesIterator.next()) // {done: false, value: 'bbb'}
console.log(namesIterator.next()) // {done: false, value: 'ccc'}
console.log(namesIterator.next()) // {done: false, value: 'ddd'}
console.log(namesIterator.next()) // {done: true}

2. 可迭代对象

2.1 认识可迭代对象

但是上面的代码整体来说看起来是有点奇怪的:

  • 我们获取一个数组的时候,需要自己创建一个index变量再创建一个所谓的迭代器对象

事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象

  • 我们再来看一下类似于刚刚的代码
const info = {
  friends: ["aaa", "bbb", "ccc"]
}

let index = 0
// 创建一个info的迭代器
const infoIterator = {
  next: function() {
    if (index < info.friends.length) {
      return { done: false, value: info.friends[index++] }
    } else {
      return { done: true }
    }
  }
}

console.log(infoIterator.next()) // {done: false, value: 'aaa'}
console.log(infoIterator.next()) // {done: false, value: 'bbb'}
console.log(infoIterator.next()) // {done: false, value: 'ccc'}
console.log(infoIterator.next()) // {done: true}
  • 我们发现, 想要迭代的对象和我们的迭代器是分开的, 那么有没有办法就将要迭代的对象和迭代器合并起来呢?(往下面看)

什么又是可迭代对象呢?

  • **可迭代对象和迭代器是两个不同的概念, 其实我们将上面的代码中, 迭代的目标对象和迭代器合并起来就一个可迭代对象 **;
  • 一个对象实现了iterable protocol协议时,它就是一个可迭代对象
  • 可迭代对象的要求一 : 是必须实现 @@iterator (这是规范的名字) 方法,在代码中我们使用 [Symbol.iterator] (这是实际用的名字)访问该属性
  • 可迭代对象的要求二 : 这个[Symbol.iterator] 方法需要返回一个迭代器
  • 那么我们根据这两个要求, 试着将上面代码中的info对象变成一个可迭代对象
const info = {
  friends: ["aaa", "bbb", "ccc"],
  
  // 1.必须实现一个特定的方法[Symbol.iterator]   (名字是固定的)
  [Symbol.iterator] () {
    // 将我们创建的info对象的迭代器放过来
    let index = 0
    const infoIterator = {
      next: function() {
        if (index < info.friends.length) {
          return { done: false, value: info.friends[index++] }
        } else {
          return { done: true }
        }
      }
    }
    // 2.这个方法需要返回一个迭代器(这个迭代器用于迭代当前对象)
    return infoIterator
  }
  
}
  • 为了进一步优化, 我们想在next方法中使用this, 但是next方法中的this不一定指向info, 我们需要让this指向info
const info = {
  friends: ["aaa", "bbb", "ccc"],
  [Symbol.iterator] () {
    let index = 0
    const infoIterator = {
      // 将next方法改为箭头函数, 这样next方法中就不在绑定this, this回去上层作用域中寻找, 上层作用域中的this会指向info
      next: () => {
        if (index < this.friends.length) {
          return { done: false, value: this.friends[index++] }
        } else {
          return { done: true }
        }
      }
    }
    return infoIterator
  }
}

当然我们要问一个问题,我们转成这样的一个东西有什么好处呢?

  • 一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作

  • 比如 for…of 操作时(当然不仅限于for…of),其实就会调用它的 [Symbol.iterator] 方法(@@iterator);

    // 例如上面的info对象本来是没办法for...of操作的, 变成可迭代对象后就可以进行for...of操作
    for (item of info) {
      console.log(item) // aaa bbb ccc
    }
    

2.2 可迭代对象特点

上面我们将info变成可迭代对象可以发现可迭代对象会具备一下特点:

  1. 可迭代对象我们一定**可以访问它的[Symbol.iterator]**方法

    info[Symbol.iterator]
    
  2. 调用**[Symbol.iterator]方法一定会返回一个迭代器**

    const iterator = info[Symbol.iterator]()
    
  3. 可以调用返回的迭代器的next方法, 对 对象进行迭代

    const iterator = info[Symbol.iterator]()
    console.log(iterator.next()) // {done: false, value: 'aaa'}
    console.log(iterator.next()) // {done: false, value: 'bbb'}
    console.log(iterator.next()) // {done: false, value: 'ccc'}
    console.log(iterator.next()) // {done: true}
    

2.3 原生迭代器对象

事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的:

  • 比如 : String、Array、Map、Set、arguments对象、NodeList集合;

我们都知道数组是一个可迭代对象, 那么数组是不是有[Symbol.iterator]方法并且满足我们刚刚总结的特点呢? 我们来验证一下

  1. 打印数组的[Symbol.iterator]方法, 我们发现确实有这样一个函数

    const nums = [10, 20, 30]
    console.log(nums[Symbol.iterator]) // ƒ values() { [native code] }
    
  2. 调用数组的[Symbol.iterator]方法, 我们发现会返回一个数组的迭代器

    const nums = [10, 20, 30]
    console.log(nums[Symbol.iterator] ()) // Array Iterator {}
    
  3. 并且我们可以拿到这个数组的迭代器, 通过迭代器的next方法访问数组元素

    const nums = [10, 20, 30]
    const iterator = nums[Symbol.iterator]()
    console.log(iterator.next()) // {value: 10, done: false}
    console.log(iterator.next()) // {value: 20, done: false}
    console.log(iterator.next()) // {value: 30, done: false}
    console.log(iterator.next()) // {value: undefined, done: true}
    

由此发现, 数组中是有一个实现好了的迭代器, 因此数组是一个可迭代对象, 数组可迭代的原理我们就很清楚了, 其他可迭代对象同理

2.4 可迭代对象场景

**上面我们已经讲过, 可迭代对象可以使用for…of, 那么除此之外还可以应用在以下场景 **:

  • 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);

简单举几个例子:

// 例如两个对象, 一个转为可迭代对象的info, 一个是默认对象obj
const obj = {
  name: "kaisa",
  age: 18
}
const info = {
  friends: ["aaa", "bbb", "ccc"],
  [Symbol.iterator] () {
    let index = 0
    const infoIterator = {
      next: () => {
        if (index < this.friends.length) {
          return { done: false, value: this.friends[index++] }
        } else {
          return { done: true }
        }
      }
    }
    return infoIterator
  }
}

// 1.可迭代对象可以使用展开语法
console.log(...info) // aaa bbb ccc
// 普通对象不可使用展开语法
console.log(...obj) //  Found non-callable @@iterator

// 2.set方法也是要求传入可迭代对象
const set1 = new Set(info)
console.log(set1) // Set(3) {'aaa', 'bbb', 'ccc'}
// 普通对象传入就会报错
const set2 = new Set(obj)
console.log(set2)

// 3.转为数组的方法Array.from也是要求传入可迭代对象
const arr1 = Array.from(info)
console.log(arr1) // ['aaa', 'bbb', 'ccc']
// 不可迭代对象无法正确转为数组
const arr2 = Array.from(obj)
console.log(arr2) // []

2.5 自定义类的迭代

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

  • 在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象
  • 如果我们也希望自己的类创建出来的对象默认是可迭代的,那么在设计类的时候在类的实例方法上添加迭代器

自定义类的迭代的实现: 写一个案例尝试创建一系列可迭代对象

class Person {
  constructor(name, age, friends) {
    this.name = name
    this.age = age
    this.friends = friends
  }

  // 添加实例方法, 迭代器
  [Symbol.iterator] () {
    let index = 0
    return {
      next: () => {
        if (index < this.friends.length) {
          return { done: false, value: this.friends[index++] }
        } else {
          return { done: true }
        }
      }
    }
  }
}

// 这样Person类创建出来的对象都是可迭代对象
const p1 = new Person("kaisa", 18, ["aaa", "bbb", "ccc"])
const p2 = new Person("coder", 19, ["ddd", "eee", "fff"])

// 可以进行for...of操作
for (item of p1) {
  console.log(item) // aaa bbb ccc
}
for (item of p2) {
  console.log(item) // ddd eee fff
}

2.6 迭代器的中断

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

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

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

  • 例如我们用上面的代码举例
class Person {
  constructor(name, age, friends) {
    this.name = name
    this.age = age
    this.friends = friends
  }

  // 添加实例方法, 迭代器
  [Symbol.iterator] () {
    let index = 0
    return {
      next: () => {
        if (index < this.friends.length) {
          return { done: false, value: this.friends[index++] }
        } else {
          return { done: true }
        }
      },
      // 添加一个return方法, 用于监听迭代中断, 当迭代器中断就会执行return方法
      return: () => {
        console.log("监听到迭代器中断")
        // 迭代器需要返回对象
        return { done:true }
      }
    }
  }
}

const p1 = new Person("kaisa", 18, ["aaa", "bbb", "ccc"])

for (item of p1) {
  console.log(item) // aaa bbb

  // 如果在某种情况写退出了循环, 我们需要告知迭代器
  if (item === "bbb") {
    break
  }
}
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐