前言
这一节单独讲一下Dart中的迭代器,虽然讲解Dart中的迭代器,但是会从我们熟知却并不熟悉的javascript的迭代器进行讲起,并且比较一下区别和原理,顺带牵出一些其他小知识点。
知识点:@@Iterator
、Iterable
、caller
、callee
、List、Array
视频地址:传送门(视频后半部分)
javascript 迭代协议
javascript中的迭代协议是 ECMAScript 2015
的一组补充规范。
迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。
迭代协议具体分为两个协议:可迭代协议
和迭代器协议
。
可迭代协议 Iterable
可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of
结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array
或者 Map
,而其他内置类型则不是(比如Object
))。
这里注意,协议并不是一种数据类型,而是一种约定规则,可以自己实现。
要成为可迭代对象, 一个对象必须实现 @@iterator
方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator
的属性,可通过常量 Symbol.iterator
访问该属性:
[Symbol.iterator] 一个无参数的函数,其返回值为一个符合迭代器协议的对象。
当一个对象需要被迭代的时候(比如被置入一个 for...of
循环时),首先,会不带参数调用它的 @@iterator
方法,然后使用此方法返回的迭代器获得要迭代的值。
值得注意的是调用此零参数函数时,它将作为对可迭代对象的方法进行调用。 因此,在函数内部,this关键字可用于访问可迭代对象的属性,以决定在迭代过程中提供什么。
上图中我们调用了数组a
的Symbol.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 是一个内置的可迭代对象:
一些内置的语法结构——比如展开语法——其内部实现也使用了同样的迭代协议:
可以通过提供自己的 @@iterator
方法,重新定义迭代行为:
上面的例子点到为止,有点缺陷。
注意重新定义的 @@iterator
方法是如何影响内置语法结构的行为的:
debug Iterator
我们从断点的角度来看一下javascript中具有迭代协议的数据类型。
声明数组的原始数据类型[]
。顺着 __proto__
找一下继承父类Array。
可以看到有一个Symbol(Symbol.iterator): function values() { ... }
无参数的迭代器方法就在这里,其他都是Array类型上的成员变量及函数。
展开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”属性
这里又是另一个知识点了。扩展一下caller
及callee
这两个知识点。
我学习东西就喜欢把相关东西都搞懂,搞通透。其实可以跳过这个部分。
caller
arguments.callee
caller
用来判断是不是函数自身内部调用自身
callee
arguments.callee
arguments.callee
属性包含当前正在执行的函数。
callee
是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。这在函数的名称是未知时很有用,例如在没有名称的函数表达式 (也称为匿名函数
)内。
早期版本的 JavaScript不允许使用命名函数表达式,出于这样的原因, 你不能创建一个递归函数表达式。
例如,下边这个语法就是行的通的:
但是:
这个不行。匿名函数如果想做递归操作的话是无法调用的。为了解决这个问题, arguments.callee
添加进来了。然后你可以这么做
然而,这实际上是一个非常糟糕的解决方案,递归调用会获取到一个不同的 this 值,例如:
ECMAScript 3
通过允许命名函数表达式解决这些问题。例如:
Dart Iterable
在Dart
中的就不叫迭代协议了,专门有个数据类型为可迭代类型Iterable
,概念如下:
可以顺序访问的值或“元素”的集合。
从一个List类型
的例子来看:
可以看到List
类型中map
函数返回的值是Iterable可迭代类型
的,这里与javascript中Array
概念并不相同。
这里可能会有两个疑问?️。
为什么不直接返回List类型?
返回的Iterable
为映射元素的视图,返回带有由创建的元素的新的惰性Iterable
。
迭代顺序在此Iterable
的每个元素上调用function
。map
中的function
并未进行调用,转换后的元素将不会被缓存。反复进行调用可以在一个元素上多次执行function
如果上面没懂,可以通俗的解释一下:
设计最开始List对象类型
就是Iterable
类型继承下来的:
所以List类型
也是Iterable类型
:
这是从本质上进行的设计。所以可知List类型的循环迭代返回的Iterable类型上的概念。
第二个问题:javascript中的Array
是什么?
javascript Array
我们上面知道了一个流程,在Dart中List对象是从Iterable类型上继承下来的,所以List类型可以使用Iterable类型上的属性和方法,并且自身扩展了一些属性和方法。所以List类型中比如map等方法返回值是Iterable类型,处于性能方面考虑也讲得通。
但是回过头来一想,javascript中的Array是什么呢?javascript 2015之前是没有迭代器的,只有迭代协议,突然感觉不是很对劲,所以深挖了一下。
MDN概念:
数组是类似于列表的高阶对象。这里提及到了列表的概念,但是解释的非常不通透,只是单独提及了一嘴。
我们继续深挖,看一下ECMAScript文档。
在文档数据类型章节中我并没有看到Array类型,但是看到了List
类型,这时恍然大悟,原来js中是有List类型的。
但是不急着看里面内容,目录中还有一些Array类型的提及,比如测试类型中有IsArray
第七章,CreateArrayFromList
、CreateListFromArrayLike
。
看一下第六章:
规定中,规范类型包含List类型
、 Record类型
都不可以在正常表达式中使用。
List类型
可以使用«1, 2»
这样的方式表示。
为什么List类型不用[]
方括号呢? 因为[]
方括号被Array
用了。
再说Record类型
可以当作“key”使用。
比如:
var a = { [[field]]: value }
复制代码
我们上面还见过Record类型
:
[[Scopes]]: Scopes[0]
这个就是了,作用域链
的记录参数。
看一下第七章:
这里介绍的比较简单:通过List创建Array对象类型的抽象方法。这里可知,javascript设计的时候就是通过List类型创建Array类型。
看一下第22章:
这里面才解释了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类型。
这也解释了为什么List类型的数据会有toList()这个方法,因为本来toList()方法是给Iterable用的。
还一个注意点:
Dart中for-in
循环中,就是使用Iterator
测试迭代的结束。
END
这一节顺着第四节讲解了Iterable类型以及重新深入了解了一下javascript中的迭代器,并且深入挖掘了一下Array类型的原理,发现了一个大秘密。我记录一下单独讲解一下javascript Array的设计原理。
视频中讲解更内容更加丰富详细,传送门(视频后半部分)