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

BitCipherX
• 阅读 7410

导语

遍历器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链接

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
BitCipherX
BitCipherX
Lv1
再相逢,相顾无言,却无泪千行。
文章
3
粉丝
0
获赞
0