canvascat / note

Learning Notes 🥱
3 stars 1 forks source link

Iterable / Generator #22

Open canvascat opened 3 years ago

canvascat commented 3 years ago

Iterable / for...of

可迭代(Iterable) 对象是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of 循环中使用的对象。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,下面将一个普通对象转化为可迭代对象:

// 比如一个 `range` 对象,它代表了一个数字区间:
const range = {
  from: 1,
  to: 5,
};

// 我们希望 for..of 这样运行:
// for(let num of range) ... num=1,2,3,4,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) {
  console.log(num); // 依次输出 1, 2, 3, 4, 5
}

为了让 range 对象可迭代(也就让 for..of 可以运行)我们需要为对象添加一个名为 Symbol.iterator 的方法(一个专门用于使对象可迭代的内置 symbol)。

  1. for..of 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有 next 方法的对象。
  2. 从此开始,for..of 仅适用于这个被返回的对象
  3. for..of 循环希望取得下一个数值,它就调用这个对象的 next() 方法。
  4. next() 方法返回的结果的格式必须是 {done: Boolean, value: any},当 done=true 时,表示迭代结束,否则 value 是下一个值。

请注意可迭代对象的核心功能:关注点分离。

因此,迭代器对象和与其进行迭代的对象是分开的。

从技术上说,我们可以将它们合并,并使用 range 自身作为迭代器来简化代码:

const range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  },
};

现在 range[Symbol.iterator]() 返回的是 range 对象自身:它包括了必需的 next() 方法,并通过 this.current 记忆了当前的迭代进程。这样更短,对吗?是的。有时这样也可以。

但缺点是,现在不可能同时在对象上运行两个 for..of 循环了:它们将共享迭代状态,因为只有一个迭代器,即对象本身。但是两个并行的 for..of 是很罕见的,即使在异步情况下。

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

对于一个字符串,for..of 遍历它的每个字符:

for (let char of 'test') {
  // 触发 4 次,每个字符一次
  console.log(char); // t, then e, then s, then t
}
// 对于Unicode字符
for (let char of '𝒳😂') {
  console.log(char); // 𝒳,然后是 😂
}

显式调用迭代器

const str = 'Hello';
// 和 for..of 做相同的事
// for (let char of str) console.log(char);

const iterator = str[Symbol.iterator]();
while (true) {
  const result = iterator.next();
  if (result.done) break;
  console.log(result.value); // 一个接一个地输出字符
}

显式调用迭代器比使用 for..of 更能精细控制迭代过程。例如,我们可以拆分迭代过程:迭代一部分,然后停止,做一些其他处理,然后再恢复迭代。

可迭代(iterable)和类数组(array-like)

有两个看起来很相似,但又有很大不同的正式术语。请你确保正确地掌握它们,以免造成混淆。

实际任务中我们可能会遇到可迭代对象或类数组对象,或两者兼有。例如,字符串即是可迭代的(for..of 对它们有效),又是类数组的(它们有数值索引和 length 属性)。但是一个可迭代对象也许不是类数组对象。反之亦然,类数组对象可能不可迭代。

可迭代对象和类数组对象通常都 不是数组,它们没有 pushpop 等方法。如果我们有一个这样的对象,并想像数组那样操作它,就可以通过一些其他方法将其转化为数组。

Array.from

const arr = Array.from({
  0: 'Hello',
  1: 'World',
  length: 2,
});
console.log(arr.pop()); // World(pop 方法有效)
// range 来自上文的例子中
console.log(Array.from(range).toString()); // 1,2,3,4,5

另外用Array.from处理带 Unicode 的字符是非常方便的,与 str.split 方法不同,它依赖于字符串的可迭代特性。

可以基于 Array.from 创建 UTF-16 扩展字符的slice 方法:

const slice(str, start, end) => Array.from(str).slice(start, end).join('');

const str = '𝒳😂𩷶';
console.log(slice(str, 1, 3)); // 😂𩷶
// 原生方法不支持识别UTF-16 扩展字符
console.log( str.slice(1, 3) ); // "\udcb3\ud83d"(两个不同 UTF-16 扩展字符碎片拼接的结果)

另外解构也是类似原理:

console.log([...'𝒳😂𩷶']); // => ["𝒳", "😂", "𩷶"]