for-of循环
首先看看传统的数组遍历方法:
var myArray=[“a”,”b”,”c”]; for(var index=0;index<myArray.length;index++){ console.log(myArray[index]); } |
然后到了ECMAScript5之后,发明了数组内建的forEach方法:
var myArray=[“a”,”b”,”c”]; myArray.forEach(function(value){console.log(value)}); |
随着内建forEach普及,突然发现该方法存在如下不方便使用的地方:无法使用break/return来中断循环/退出循环。
鉴于此又使用for-in循环:
var myArray=[“a”,”b”,”c”]; for(var index in myArray){ console.log(myArray[index]); } |
但是for-in在使用过程中发现如下问题:
①这里的index不再是索引,而是数组中的具体值,如果具体值是字符串的话,那么,对index的处理格外注意,例如:+运算等需要格外注意。
②for-in除了遍历myArray中的值之外,还遍历开发者对myArray的自定义属性,特别是枚举自定义属性。
③特殊情况下,每次for-in打印值的顺序均不一样。
④for-in在设计之初是为了遍历普通对象用的,因此可以用for-in遍历普通对象的字符串类型的键key。
到了ECMAScript6之外,针对forEach无法退出、for-in遍历普通对象字符串类型键key,发展出了for-of语法:
var myArray=[“a”,”b”,”c”]; for(var index of myArray){ console.log(myArray[index]); } |
这里,鉴于一些老项目已经使用for-in来遍历数组,这就导致ECMAScript6不想修改for-in的逻辑,进而新语法for-of来解决forEach、for-in缺陷。
for-of可以遍历:数组、大部分类数组对象(DOM对象节点)、字符串、Map、Set。
迭代器
迭代器,多么耳熟的名词,在C++、Java、C#、Python均得到了大量、实践、实用性的检验。
既然这么多语言使用了迭代器,这里可以用Java来做案例来讲解ES6的迭代器。
Java中的Map、Set、List除了实现自己的集合接口之外,还实现了迭代器接口,这就意味着Map、Set、List除了存储数据之外,还可以按照统一的迭代器接口来访问它们。
ECMAScript6也是同样的逻辑!!!!
首先,开发者可以给任意类型的对象添加迭代器方法进而可以被for-of来遍历。
如何给任意对象添加迭代器方法???
通过向任意对象添加myObject[Symbol.iterator]()即可完成。
[Symbol.iterator]如何理解,看起来好奇怪???
可能一些第三方插件、开发者已经使用了iterator关键字,为了避免属性冲突,所以,ECMAScript使用了Symbol.iterator来避免属性名冲突。
for-of是如何调用迭代器对象的???
for-of循环首先调用对象的[Symbol.iterator]()方法,然后该方法返回一个迭代器对象,for-of循环将重复调用迭代器对象的next()方法。这里再让我们梳理下脉络:
首先:给对象添加[Symbol.iterator]属性。
其次:[Symbol.iterator]对应的是一个方法,该方法返回一个迭代器对象。
迭代器对象有哪些注意事项???
迭代器对象一共有3个方法需要开发者自己去实现:next()/return()/throw(exc)方法。
next()方法会被for-of循环调用。
return()方法就是for-of循环通过break、return、异常等方式退出循环时就会触发return方法
throw(exc)方法有点特殊,因为for-of永远不会调用该方法。
生成器Generators
什么叫做生成器,这玩意又是干什么用的,很难用文字来说明,这里通过一个案例来作为引子来进行说明:
上面说到“迭代器”,要实现for-of就必须实现如下要求:
①对象必须要有[Symbol.iterator]()属性;
②[Symbol.iterator]()属性返回一个迭代器对象;
③迭代器对象至少、必须实现next()方法;
也就是说,要想让一个普通对象使用for-of就必须依次做上述3个步骤,然后步骤是固话的,那么,有没有一种可能性:用更加简练的代码来取代上述3个步骤,进而实现for-of功能呢?
答案就是:生成器。生成器具有如下特点:所有生成器均内建有.next()、[Symbol.iterator]()方法的实现,因此你只需要编辑循环部分的行为即可。
那么下面就认证介绍下生成器规则:
生成器函数跟普通function函数有如下区别:
①普通函数使用function声明,而生成器函数则使用function*来声明。
②普通函数使用return来终止,而生成器函数内部包含1或者多个yield关键字,在执行过程中遇到yield关键字时表达式停止执行,直到调用方触发.next()方法后则继续执行。
当调用生成器函数时,函数返回一个处于已暂停状态的生成器对象,此时生成器对象暂停在yield关键字的上一行代码,通过调用生成器对象的.next()方法来执行yield后续代码,直到再次遇到yield,然后再调用.next()方法再继续执行yield代码直到再再遇到yield关键字。
生成器内存工作过程就是:每当生成器执行yield语句,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置)被移出堆栈。但是生成器对象保留了对这个堆栈结构的引用(备份),所以通过调用next()方法可以重新激活堆栈结构并且继续执行。
扩展
这里,由于生成器实现了迭代器接口,因此,生成器也包含有return()和throw(exc)方法。
①return()方法
这里涉及到了return()方法。这里就是外部调用者通过for-of方式来循环调用next()方法来间接调用代码,让代码继续执行下去,既然是外部for-of来控制程序的运行,那么会不会存在这种情况:在for-of中存在某个if判断,对next()返回值进行业务处理,当返回值符合某个条件时,通过break/return来终止for-of,进而终止next()方法的调用。既然存在这种情况,那么最终决定程序是否继续执行的人是外部调用者,那外部调用者怎么确保此时此刻生成器代码所占内存确保能被回收呢?
这里迭代器接口支持一个可选的.return()方法,每当迭代在迭代器返回{done:true}之前退出都会自动调用这个.return()方法。而生成器也支持这个规则,即接口的.return()会触发生成器来执行生成器自己所编写的任一finally代码块然后退出。
注意:.return()方法并不是在所有的上下文中都会被自动调用,只有当使用了迭代协议的情况下才会触发该机制。
②可选参数的.next()方法与yield关键字
function* test(name){ yield ‘LiSi’ var name1 = yield getName(name); } function getName(name){ return name; } 调用方式1: var genenrate1 = test(‘ZhangSan’); genenrate1.next(); genenrate1.next(‘ZhangSan’); 调用方式2: var genenrate2 = test(‘ZhangSan’); genenrate2.next(); genenrate2.next(); |
yield关键字就2个作用:暂停生成器、把返回值返给调用者。
现在的问题在于:genenrate1.next(‘ZhangSan’);与genenrate2.next();区别?
genenrate1.next(“ZhangSan”):next()方法接受一个可选参数,参数稍后会作为yield表达式的返回值出现在生成器中。即可选参数ZhangSan作为yield getName(name);表达式的返回值,赋值给变量name1,即yield表达式的返回值出现在生成器中。
③throw(error)方法
如果调用者在调用过程中发生异常,则调用者无法继续调用生成器.next()方法,此时应该调用生成器.throw(error)方法来抛出yield表达式来终止生成器的后续执行。
这里有几个问题:
问题1:生成器.throw(error)方法所抛出的yield表达式在生成器的try-catch代码段中,则由try-catch来捕获该异常并最后执行finally代码块,因此生成器此时还是继续执行的。
问题2:生成器内部抛出的异常最终总是会传播到调用者,因此,无论yield是否在try-catch代码块中,生成器.throw(error)方法最后还是给调用者抛出error对象。
④yield与yield*区别
yield表达式只生成一个值
yield*表达式可以通过迭代器进行迭代生成所有的值。即yield*可以使用迭代器对另外一个生成器对象进行遍历,并一次性返回所有遍历值。