深入理解ES6——迭代器与生成器

深入理解ES6——迭代器与生成器


前言

1、用循环语句迭代数据时,必须要初始化一个变量来记录每次迭代在数据集合中的位置。而在许多编程语言中,已经开始通过序列化的方式用迭代器对象返回迭代过程中集合的每一个元素。

2、迭代器的使用可以简化数据操作。新的数组方法、集合类型(set、map等)都依赖迭代器的实现。还有其他特性也有迭代器的身影:如for-of、展开运算符、异步编程。

一、什么是迭代器

迭代器是特殊对象,具有一些专门为迭代过程设计的专有接口。每个迭代器有一个next()方法,每次调用返回一个值。
用es5创建一个迭代器:

// 用es5的语法创建一个迭代器
function createIterator(items) {
    var i = 0;
    return {
        next: function () {
            var done = (i >= items.length);
            var value = !done? items[i++] : undefined
            return {
                done: done,
                value: value
            }
        }
    }
}

var iterator = createIterator([1,2,3])
console.log(iterator.next())// {done: false, value: 1}
console.log(iterator.next())//{done: false, value: 2}
console.log(iterator.next())//{done: false, value: 3}
console.log(iterator.next())//{done: true, value: undefined}
console.log(iterator.next())//{done: true, value: undefined}

上面的实例很复杂,在es6中,引入了生成器对象,可以让常见迭代器过程变得简单。

二、什么是生成器

生成器是返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield。如:

function *createIterator() {
    yield 1;
    yield 2;
    yield 3;
}
// 生成器的调用方式与普通函数相同,只不过返回的是一个迭代器
let iterator = createIterator();
console.log(iterator.next().value)//1
console.log(iterator.next().value)//2
console.log(iterator.next().value)//3

yield关键字可以返回任何值和表达式,所以可以批量给迭代器添加元素:

let createIterator1 = function *(items) {
    for(let i = 0; i < items.length; i++) {
        yield items[i];
    }
}
let iterator1 = createIterator1([1,2,3]);
// yield 关键字只能在生成器内使用,否则会抛异常

生成器函数最有趣的部分大概是,每执行完一次yield语句后,函数会自动停止执行。这种中止函数执行的能力有很多有趣的应用(见高级迭代器功能)。

三、可迭代对象和for-of循环

可迭代对象都有个Symbol.iterator属性,是一个和迭代器密切相关的对象。它可以通过特定函数返回一个作用于附属对象的函数。

由于生成器默认为Symbol.iterator属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象
在这里插入图片描述

使用迭代器和for-of循环,便不用跟踪整个集合的索引,只需关注集合要处理的内容,如:

let values = [1, 2, 3]
for (let num of values) {
    console.log(num)
}
// 1 2 3
// for-of用于不可迭代对象、null、undefined会抛异常

这段代码,for-of通过调用values背后的Symbol.iterator方法获取迭代器(js引擎背后完成的)。随后迭代器的next()方法被多次调用,去里面的value属性并存在变量num中。

(1)如何访问默认的迭代器?

可以通过Symbol.iterator来访问对象默认迭代器,如:

let values = [1, 2, 3]
let iterator = values[Symbol.iterator]();// values数组原型上的默认方法
console.log(iterator.next())// {done: false, value: 1}
console.log(iterator.next())//{done: false, value: 2}
console.log(iterator.next())//{done: false, value: 3}

由于Symbol.iterator属性的对象都有默认迭代器,因此可用它检测对象是否为可迭代对象:

function isIterable(object) {
    return typeof object[Symbol.iterator] == 'function';
}
console.log(isIterable([1,2,3]));// true
console.log(isIterable("Hello"));// true
console.log(isIterable(new Map());// true
console.log(isIterable(new Set());// true
console.log(isIterable(new WeakMap());// false
console.log(isIterable(new WeakSet());// false

(2)创建自己的可迭代对象

默认情况下,自己定义的对象都是不可迭代的,但如果给Symbol.iterator属性添加一个生成器,则可以变成 可迭代对象:

let collection = {
    items: [],
    *[Symbol.iterator]() {
        for (let item of this.items) {
            yield item;
        }
    }
};
collection.items.push(1)
collection.items.push(2)
collection.items.push(3)
for(let x of collection) {// 对象不可迭代的噢,这里可以迭代了呢
    console.log(x) // 1 2 3
}
// 这只是对象迭代器的一种使用方式,后面有另一种使用方式:委托生成器

四、内建迭代器

迭代器是ES6的一个重要组成部分,在ES6中,已经默认为许多内建类型提供了内建迭代器,只有这些内建迭代器无法实现目标时才需要自己创建。

(1)集合对象迭代器

ES6种有3类型集合对象:数组、map和set。它们有三种迭代器:

  • entries() 返回一个迭代器,值为多个键值对。
  • values() 返回一个迭代器,其值为集合的值。
  • keys() 返回一个迭代器,其值为集合中的所有键名。
let colors = ['red', 'green', "blue"]
 for(let entry of colors.entries()) {
     console.log(entry)// [0, 'red']  [1, 'green'] [2, 'blue']
 }
let tracking = new Set([1234, 5678, 9012])
 for(let entry of tracking.entries()) {
     console.log(entry)// [1234, 1234] [5678, 5678] [9012, 9012]
 }
let data = new Map();
data.set('title', "understanding ECMAScript 6")
data.set('format', "ebook")
for(let entry of data.entries()) {
    console.log(entry) // ['title', 'understanding ECMAScript 6']  ['format', 'ebook']
}
//同理, 遍历data.values 得到value值;遍历data.keys 得到key值
// for-of中没有显示指定则使用默认迭代器。数组和set默认values,map默认时entries
// 可以用结构+for-of来将默认的values变成entries功能,如:
for(let [key,value] of data) {
	console.log(key+ "=" + value);
}

(2)字符串迭代器

(略)

(3)NodeList迭代器

var divs = document.getElementsByTagName("div");
for (let div of divs) {
	console.log(div.id)
}

五、展开运算符与非数组可迭代对象

展开运算符可操作所有可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器读取所有的值。然后按返回顺序将它们一次插入数组中。如:

let set = new Set([1,2,3,3,3,4,5])
let array1 = [...set]
console.log(array1)

let map = new Map([["name", "Nicholas"], ["age", 25]])
let array2 = [...map];
console.log(array2)

六、高级迭代器

(1) 给迭代器传递参数

function *createIterator() {
	let first = yield 1;
	let second = yield first + 2; // 4 + 2
	yield second + 3;// 5 + 3
}
let iterator = createIterator();
console.log(iterator.next());// "{value:1, done:false}"
console.log(iterator.next(4));// "{value:6, done: false}"
console.log(iterator.next(5)); // "{value:8, done: false}"
// 第一次调用next()方法时无论传入什么参数都会被丢弃。

(2)在迭代器中抛出错误

除了给迭代器传递数据外,还可以传递错误条件。通过throw方法,当迭代器回复执行时可令其抛出一个错误。这种主动抛出错误的能力对于异步编程来说至关重要。

function *createiterator4() {
    let first = yield 1;
    let second;
    try{
        second = yield first + 2;// yield 4 + 2,然后抛出错误
    } catch(ex) {
        second = 6;
    }
    yield second + 3;
}
let iterator4 = createiterator4();
console.log(iterator4.next());
console.log(iterator4.next(4))
console.log(iterator4.throw(new Error("lisa de error")))

(3)生成器返回语句

function *createIterator() {
    yield 1;
    return 42;// 可以返回值,也可以不返回值
}
let iterator6 = createIterator();
console.log(iterator6.next())// {value: 1, done: false}
console.log(iterator6.next())// {value: 42, done: true}
console.log(iterator6.next())//{value: undefined, done: true}

迭代器的返回值依然是一个非常有用的特性,比如接下来的委托生成器

(4)委托生成器

在某些情况下,需要将两个迭代器合二为一。这时可以创建一个生成器,再给yield语句添加一个星号,就可以将生成数据的过程委托给其他人。

function *createNumberIterator() {
    yield 1;
    yield 2;
}
function *createColorIterator() {
    yield "red";
    yield "green";
}
function *createCombinedIterator() {
    yield *createNumberIterator();
    yield *createColorIterator();
    yield true;
}
let iterator7 = createCombinedIterator();
 console.log(iterator7.next())//{value: 1, done: false}
console.log(iterator7.next())// {value: 2, done: false}
console.log(iterator7.next())// {value: 'red', done: false}
console.log(iterator7.next())// {value: 'green', done: false}
console.log(iterator7.next())//{value: true, done: false}

有了委托功能,可以进一步利用返回值处理复杂任务。如把一个生成器的返回值传入另一个生成器并执行。

七、异步任务执行

复杂的异步化会带来很多管理代码的挑战。由于生成器支持在函数中暂停代码执行,因而可以深入挖掘异步处理的更多用法。
执行异步操作的传统方式一般时盗用一个函数并执行相应的回调函数,如:

let fs = require('fs');
fs.readFile("config.json", function(err, contents) {
	if(err) {
		throw err;
	}
	doSomethingWith(contents);
	console.log("Done");
}

像这样,执行的任务少,没毛病,但是嵌套回调或序列化一系列异步操作,就非常复杂啦。此使,生成器和yield语句就可以用上了。

(1)实现简单的异步执行器

function run(taskDef) {
	// 创建迭代器
	let task = taskDef();
	// 开始任务
	let result = task.next();
	// 循环调用next()函数
	function step() {
		// 任务未完成,则继续执行
		if (!result.done) {
			result = task.next();
			step();
		}
	}
	// 开始迭代执行
	step();
}

借助这个run函数,可以像这样执行一个包含多条yield语句的生成器:

run(function*() {
	console.log(1);
	yield;
	console.log(2);
	yield;
	console.log(3);
})

控制台输出多次调用next方法的结果,分别为1,2,3。简单输出不足以展示迭代器的高级功能,接下来看迭代器与调用者之间相互传值。

(2)向任务执行器传递参数

还是上面的run方法,只不过next()方法里面传递参数:

function run(taskDef) {
	// 创建迭代器
	let task = taskDef();
	// 开始任务
	let result = task.next();
	// 循环调用next()函数
	function step() {
		// 任务未完成,则继续执行
		if (!result.done) {
			result = task.next(result.value);
			step();
		}
	}
	// 开始迭代执行
	step();
}

调用方式改为:

run(function*(){
	let value = yield 1;
	console.log(value);
	value = yield value + 3;
	console.log(value);
})

输出1,4。现在数据能砸yield调用间互相传递了,只需小小的改变就能支持异步调用。

(3)异步任务执行器

之前只是在多个yield间传递静态数据,而等待一个异步过程有些不同。异步任务需要知晓回调函数是什么以及怎么使用。
定义一个异步操作:

function fetchData() {
	// 返回一个函数,并在调用时传入回调函数
	return function(callback){
		callback(null, 'hi');
	}
}

尽管fetchData是同步函数,但加个定时器,就变成异步函数:

function fetchData() {
	// 返回一个函数,并在调用时传入回调函数
	return function(callback){
		setTimeout(function(){
			callback(null, 'hi');
		})
	}
}

当result.value()是函数时,任务执行器应该先执行这个函数,然后把结果传入next() 方法:

function run(taskDef) {
	// 创建迭代器
	let task = taskDef();
	// 开始任务
	let result = task.next();
	// 循环调用next()函数
	function step() {
		// 任务未完成,则继续执行
		if (!result.done) {
			if(typeof result.value === 'function') {
				// 调用result.value方法时传入回调函数functon作为参数
				result.value(function(err, data) {
					if (err) {
						result = task.throw(err);
						return;
					}
					result = task.next(data);
					step();
				})
			} else {
				result = task.next(result.value);
				step();
			}	
		}
	}
	// 开始迭代执行
	step();
}

通过上面这个任务执行器以及可以用于所有的异步任务啦。Node.js中,从文件读取数据,需在fs.readFile()中创建一个包装器(wrapper),并返回一个与fetchData()类似的函数,如:

let fs = require('fs');
functon readFile(filename){
	return function(callback){
		fs.readFile(filename, callback);
	}
}

下面是一段通过yield执行这个任务的代码:

run(function*() {
	let contents = yield readFile("config.json");
	doSomethingWith(contents);
	console.log("Done");
})

这段代码看上去像同步,异步的readFile() 操作能正常进行。

总结

  1. 迭代器是ES6一个重要组成部分,是某些关键语言元素的依赖。
  2. Symbol.iterator被用来定义对象的默认迭代器,内建对象合开发者定义的对象中都支持这个特性。通过Symbol定义的方法返回一个迭代器。如果对象中有这个属性,则它是可迭代的。
  3. for-of循环可持续获取可迭代对象的值,与传统for相比,它不需要追踪值在循环中的位置,也不需要控制循环结束的时机。
  4. 为了降低使用成本,ES6中许多值都有默认迭代器。所有集合类型(set、数组、map等)有默认迭代器。字符串也有,它可以直接迭代字符串中的字符,避免遍历编码单元带来的诸多问题。
  5. 展开运算符也可以作用于可迭代对象,通过迭代器从对象中读取相应的值并插入到一个数组中。
  6. 生成器也是一类特殊函数。定义时需要(*),调用时返回一个迭代器,并通过yield关键字标识每次调用迭代器next()方法时的返回值。
  7. 借助生成器委托这个新特性,可重用多个已有的生成器来创建新的生成器,从而进一步封装更复杂的迭代器行为。
  8. 在生成器合迭代器的应用场景中,最有趣且最兴奋的可能是用来创建更简洁的异步代码。这种方式让代码看起来像同步代码,但实际上时yield特性来等待异步操作的完成。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值