53

Iterator:访问数据集合的统一接口

 5 years ago
source link: https://segmentfault.com/a/1190000015701263?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

导语

遍历器 Iterator 是ES6为访问数据集合提供的统一接口。任何内部部署了遍历器接口的数据集合,对于用户来说,都可以使用相同方式获取到相应的数据结构。如果使用的是最新版 Chrome 浏览器,那么你要知道——我们所熟悉的数组小姐,已悄悄的打开了另一扇可抵达她心扉的小径。

1 正题

某个数据集合部署了 Iterator 接口,是指其 Symbol.iterator 属性指向一个能返回 Iterator 接口的函数。任何默认使用遍历器访问数据集合的方法,都会调用此属性以得到遍历器对象,再按照设定的顺序依次访问该数据结构的成员(关于 Symbol.iterator 请看最后一节的延伸阅读)。比如原生数组的遍历器为 [][Symbol.iterator] ,也可以直接通过其构造函数的原型获取 Array.prototype[Symbol.iterator]

1.1 基本行为

调用 Iterator 接口会返回一个新的遍历器对象(指针对象)。

对象中必然有 next 方法,用于访问下一个数据成员。指针初始时指向当前数据结构的起始位置。

第一次调用对象的 next 方法,指针指向数据结构的第一个成员。

第二次调用对象的 next 方法,指针指向数据结构的第二个成员。

不断的调用对象的 next 方法,直到它指向数据结构的结束位置。

每次调用 next 方法,都会返回相同的数据结构: { value, done }

其中 value 表示当前指向成员的值,没有则为 undefined

其中 done 是一个布尔值,表示遍历是否结束,结束为 true ,否则 false

遍历器接口的标准十分简洁,不提供诸如:操作内部指针、判断是否有值等等方法。只需要一直不断的调用 next 方法,当 donefalse 时获取当时的 valuedonetrue 时停止即可。第一次接触遍历器的行为模式是在2016的冬天,那时底蕴不够鸡毛也没长全,理解不了简洁性的适用和强大。直到现在——在即将打包被迫离开公司的前夕才蓦然的醒觉。多么痛的领悟啊。

let iterator = [1, 2, 3][Symbol.iterator]();

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}

1.2 简单实现

面向不同的数据结构,有不同的遍历器实现方法,我们简单的实现下数组的遍历器方法。

let res = null;
let iterator = myIterator([3, 7]);

console.log( iterator.next() ); // {value: 3, done: false}
console.log( iterator.next() ); // {value: 7, done: false}
console.log( iterator.next() ); // {value: undefined, done: true}

function myIterator(array = []) {
  let index = 0;
  return {
    next() {
      return index < array.length 
        ? { value: array[index++], done: false }
        : { value: undefined, done: true };
    }
  };
}

1.3 return & throw

除了为遍历器对象部署 next 方法,还可以有 returnthrow 方法。其中 return 方法会在提前退出 for of 循环时(通常是因为出错,或触发了 break 语句)被调用。而 throw 方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法,所以不予介绍。

let obj = {
  [Symbol.iterator]() {
    let index = 0;
    let array = [1, 2, 3];

    return {
      next() {
        return index < array.length 
          ? { value: array[index++], done: false }
          : { value: undefined, done: true };
      },
      return() {
        console.log('Trigger return.');
        return {};
      }
    };
  }
};

for (let v of obj) {
  console.log(v); // 打印出:1, 2, 3,没触发 return 函数。
}

for (let v of obj) {
  if (v === 2) break;
  console.log(v); // 打印出:1,之后触发 return 函数。
}

for (let v of obj) {
  if (v === 3) break;
  console.log(v); // 打印出:1, 2,之后触发 return 函数。
}

for (let v of obj) {
  if (v === 4) break;
  console.log(v); // 打印出:1, 2, 3,没触发 return 函数。
}

for (let v of obj) {
  if (v === 2) throw Error('error');
  console.log(v); // 打印出:1,之后触发 return 函数,并报错停止执行。
}

2 原生支持

2.1 默认持有遍历器

原生默认持有遍历器接口的数据结构有:

基本类型: Array , Set , Map (四种基本数据集合: Array , Object , SetMap )。

类数组对象: arguments , NodeList , String

let iterator = '123'[Symbol.iterator]();

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}

遍历器与先前的遍历方法一个数据集合拥有遍历器接口,并不意味着所有遍历它的方法都是使用此接口。实际上,只有ES6新增的几种方式和某些方法会使用,下面会有介绍。以数组来说,对其使用 forfor of 虽然可访问到相同的成员,但是实际的操作方式却不同。

// 改变数组默认的遍历器接口。
Array.prototype[Symbol.iterator] = function () {
  let index = 0;
  let array = this;

  console.log('Use iterator');

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

let arr = [1, 2];

for (let v of arr) {
  console.log(v); // 打印出 Use iterator, 1, 2。
}

for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]); // 打印出 1, 2。
}

arr.forEach(d => {
  console.log(d); // 打印出 1, 2。
});

对象没有默认的遍历器接口为什么对象没有默认的遍历器接口?这要从两方面说明。一为遍历器是种线性处理结构,对于任何非线性的数据结构,部署了遍历器接口,就等于部署一种线性转换。二是对象本来就是一个无序的集合,如果希望其有序,可以使用 Map 代替。这即是各有其长,各安其职。屎壳郎如果不滚粪球而去采蜜,那,呃,花妹妹可能就遭殃咯。

自行生成的类数组对象(拥有 length 属性),不具备遍历器接口。这与 String 等原生类数组对象不同,毕竟人家是亲生的,一出生就含着金钥匙(也不怕误吞)。不过我们可以将数组的遍历器接口直接应用于自行生成的类数组对象,简单有效无副作用。

let obj = {
  0: 'a',
  1: 'b',
  length: 2,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};

let iterator = obj[Symbol.iterator]();
console.log( iterator.next() ); // {value: "a", done: false}
console.log( iterator.next() ); // {value: "b", done: false}
console.log( iterator.next() ); // {value: undefined, done: true}

为对象添加遍历器接口,也不影响之前不使用遍历器的方法,比如 for in , Object.keys 等等(两者不等同)。

let obj = {
  0: 'a',
  1: 'b',
  length: 2,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};

console.log( Object.keys(obj) ); // ["0", "1", "length"]

for (let v of obj) {
  console.log(v); // 依次打印出:"a", "b"。
}

for (let k in obj) {
  console.log(k); // 依次打印出:"0", "1", "length"。
}

2.2 默认调用遍历器

for of for of 是专门用来消费遍历器的,其遍历的是键值( for in 遍历的是键名)。

for (let v of [1, 2, 3])  {
  console.log(v); // 依次打印出:1, 2, 3。
}

扩展运算符无论是解构赋值或扩展运算都是默认调用遍历器的。

let [...a] = [3, 2, 1]; // [3, 2, 1]
let b = [...[3, 2, 1]]; // [3, 2, 1]

yield*

Generator 函数中有 yield* 命令,如果其后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

for (let v of G()) {
  console.log(v); // 依次打印出:1, 2, 3, 4, 5
}

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

其它场合有些接受数组作为参数的函数,会默认使用数组的遍历器接口,所以也等同于默认调用。比如 Array.from() , Promise.all()

延伸阅读

关于ES6的 Symbol链接


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK