前端学习Dart|Iterator

1,104 阅读10分钟

前言

这一节单独讲一下Dart中的迭代器,虽然讲解Dart中的迭代器,但是会从我们熟知却并不熟悉的javascript的迭代器进行讲起,并且比较一下区别和原理,顺带牵出一些其他小知识点。

知识点:@@IteratorIterablecallercalleeList、Array

视频地址:传送门(视频后半部分)

javascript 迭代协议

javascript中的迭代协议是 ECMAScript 2015 的一组补充规范。

迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。

迭代协议具体分为两个协议:可迭代协议迭代器协议

可迭代协议 Iterable

可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of 结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如Object))。

这里注意,协议并不是一种数据类型,而是一种约定规则,可以自己实现。

要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性:

image.png

[Symbol.iterator] 一个无参数的函数,其返回值为一个符合迭代器协议的对象。

当一个对象需要被迭代的时候(比如被置入一个 for...of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。

值得注意的是调用此零参数函数时,它将作为对可迭代对象的方法进行调用。 因此,在函数内部,this关键字可用于访问可迭代对象的属性,以决定在迭代过程中提供什么。

image.png

上图中我们调用了数组aSymbol.iterator无参数函数,返回了Array Iterator迭代器。

此函数可以是普通函数,也可以是生成器函数,以便在调用时返回迭代器对象。 在此生成器函数的内部,可以使用yield提供每个条目。

迭代器协议 Iterator

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。

只有实现了一个拥有(semantic)的 next() 方法,一个对象才能成为迭代器。

next方法返回参数有两个属性的对象:

done(boolean)

如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)

如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。

value

迭代器返回的任何 JavaScript 值。done 为 true 时可省略。 next() 方法必须返回一个对象,该对象应当有两个属性: done 和 value,如果返回了一个非对象值(比如 false 或 undefined),则会抛出一个 TypeError 异常("iterator.next() returned a non-object value")。

这里有个注意事项

不可能判断一个特定的对象是否实现了迭代器协议,然而,创造一个同时满足迭代器协议和可迭代协议的对象是很容易的(如下面的案例中所示)。

这样做允许一个迭代器能被各种需要可迭代对象的语法所使用。因此,很少会只实现迭代器协议,而不实现可迭代协议。

下方文章中统称javascript迭代协议含义为包含迭代器协议。

案例

这里看几个迭代对象的例子:

String 是一个内置的可迭代对象:

image.png

一些内置的语法结构——比如展开语法——其内部实现也使用了同样的迭代协议:

image.png

可以通过提供自己的 @@iterator 方法,重新定义迭代行为:

image.png

上面的例子点到为止,有点缺陷。

注意重新定义的 @@iterator 方法是如何影响内置语法结构的行为的:

image.png

debug Iterator

我们从断点的角度来看一下javascript中具有迭代协议的数据类型。

image.png

声明数组的原始数据类型[]。顺着 __proto__ 找一下继承父类Array。

image.png

可以看到有一个Symbol(Symbol.iterator): function values() { ... } 无参数的迭代器方法就在这里,其他都是Array类型上的成员变量及函数。

image.png

展开Symbol.iterator后我们可以看到arguments的内容为:

TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them

不能在严格模式函数或用于调用它们的arguments对象上访问“caller”、“callee”和“arguments”属性

这里又是另一个知识点了。扩展一下callercallee这两个知识点。

我学习东西就喜欢把相关东西都搞懂,搞通透。其实可以跳过这个部分。

caller

arguments.callee

caller 用来判断是不是函数自身内部调用自身

callee

arguments.callee

arguments.callee 属性包含当前正在执行的函数。

callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。这在函数的名称是未知时很有用,例如在没有名称的函数表达式 (也称为匿名函数)内。

早期版本的 JavaScript不允许使用命名函数表达式,出于这样的原因, 你不能创建一个递归函数表达式。

例如,下边这个语法就是行的通的:

image.png

但是:

image.png

这个不行。匿名函数如果想做递归操作的话是无法调用的。为了解决这个问题, arguments.callee 添加进来了。然后你可以这么做

image.png

然而,这实际上是一个非常糟糕的解决方案,递归调用会获取到一个不同的 this 值,例如:

image.png

ECMAScript 3 通过允许命名函数表达式解决这些问题。例如:

image.png

Dart Iterable

Dart中的就不叫迭代协议了,专门有个数据类型为可迭代类型Iterable,概念如下:

可以顺序访问的值或“元素”的集合。

从一个List类型的例子来看:

image.png

可以看到List类型中map函数返回的值是Iterable可迭代类型的,这里与javascript中Array概念并不相同。

这里可能会有两个疑问🤔️。

为什么不直接返回List类型?

返回的Iterable为映射元素的视图,返回带有由创建的元素的新的惰性Iterable

迭代顺序在此Iterable的每个元素上调用functionmap中的function并未进行调用,转换后的元素将不会被缓存。反复进行调用可以在一个元素上多次执行function

如果上面没懂,可以通俗的解释一下:

设计最开始List对象类型就是Iterable类型继承下来的:

image.png

所以List类型也是Iterable类型

image.png

这是从本质上进行的设计。所以可知List类型的循环迭代返回的Iterable类型上的概念。

第二个问题:javascript中的Array是什么?

javascript Array

我们上面知道了一个流程,在Dart中List对象是从Iterable类型上继承下来的,所以List类型可以使用Iterable类型上的属性和方法,并且自身扩展了一些属性和方法。所以List类型中比如map等方法返回值是Iterable类型,处于性能方面考虑也讲得通。

但是回过头来一想,javascript中的Array是什么呢?javascript 2015之前是没有迭代器的,只有迭代协议,突然感觉不是很对劲,所以深挖了一下。

MDN概念:

image.png

数组是类似于列表的高阶对象。这里提及到了列表的概念,但是解释的非常不通透,只是单独提及了一嘴。

我们继续深挖,看一下ECMAScript文档。

image.png

在文档数据类型章节中我并没有看到Array类型,但是看到了List类型,这时恍然大悟,原来js中是有List类型的。

但是不急着看里面内容,目录中还有一些Array类型的提及,比如测试类型中有IsArray

image.png

第七章,CreateArrayFromListCreateListFromArrayLike

image.png

看一下第六章:

image.png

规定中,规范类型包含List类型Record类型都不可以在正常表达式中使用。

List类型可以使用«1, 2»这样的方式表示。

为什么List类型不用[]方括号呢? 因为[]方括号被Array用了。

再说Record类型可以当作“key”使用。

比如:

var a = { [[field]]: value }

我们上面还见过Record类型

image.png

[[Scopes]]: Scopes[0]这个就是了,作用域链的记录参数。

看一下第七章:

image.png

这里介绍的比较简单:通过List创建Array对象类型的抽象方法。这里可知,javascript设计的时候就是通过List类型创建Array类型。

看一下第22章:

image.png

这里面才解释了Array的起源,后面有时间单独讲解一下,单独总结一下javascript Array。感兴趣的可以自己了解一下,先回归正题。


我们继续来讲Dart中的Iterable类型

Iterable类型是可以访问Iterator迭代器的类型,通过迭代器获取一个Iterator并且逐步遍历值。通过调用iterator.moveNext来完成迭代器的进步,如果调用返回false,则迭代器已经移动至下一个元素。如果返回false,那么证明没有其他元素。

原理听起来与javascript是一至的,只不过javascript中的迭代器协议规定的进步函数为next()而非moveNext()。但是需要明确的是,javascript的迭代器借鉴了严格类型语言的迭代器。

官方文档上还有一句话:

由于一个Iterable可能会被迭代一次以上,因此不建议在Iterator中具有可检测到的副作用。对于诸如map和where之类的方法,返回的iterable将在每次迭代时执行参数函数,因此这些函数也不应有副作用。

这里提及到了副作用的概念,可知Dart中是拥抱函数式编程的,如果有想理解副作用概念的,可以阅读以下我写的《聊一聊函数式编程中的副作用概念》

Iterable类型元素中的属性和方法与List类型基本一致,因为List类型就是继承于Iterable类型。

image.png

这也解释了为什么List类型的数据会有toList()这个方法,因为本来toList()方法是给Iterable用的。

image.png

还一个注意点:

Dart中for-in循环中,就是使用Iterator测试迭代的结束。

END

这一节顺着第四节讲解了Iterable类型以及重新深入了解了一下javascript中的迭代器,并且深入挖掘了一下Array类型的原理,发现了一个大秘密。我记录一下单独讲解一下javascript Array的设计原理。

视频中讲解更内容更加丰富详细,传送门(视频后半部分)