迭代器与生成器
为什么需要迭代器
在ES6之前,我们要遍历一个对象,只能使用for循环或者forEach方法。但是这两种方法都有一个缺点,就是无法中途停止或者继续遍历。而迭代器则可以解决这个问题。
原始的遍历方法,只能遍历数组或者类数组对象,需要预先知道对象的结构,并为每种结构编写不同的遍历方法(例如通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构)。而迭代器则可以遍历任意对象,只要该对象实现了迭代器接口。
迭代器
迭代器是一种特殊的对象,它包含一个next
方法,每次调用next方法都会返回一个对象,该对象包含两个属性:value和done。value表示当前的值,done表示是否迭代完成。
javascript
function makeIterator(arr) {
let nextIndex = 0;
return {
next: function() {
return nextIndex < arr.length ?
{ value: arr[nextIndex++], done: false } :
{ done: true };
}
};
}
let it = makeIterator(['a', 'b']);
console.log(it.next().value); // 'a'
console.log(it.next().value); // 'b'
console.log(it.next().done); // true
实现迭代器接口的对象
- Array
- Map
- Set
- String
- TypedArray
- arguments
- NodeList
javascript
let arr = [1, 2, 3];
// 通过Symbol.iterator获取迭代器
let it = arr[Symbol.iterator]();
console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // 3
console.log(it.next().done); // true
自定义迭代器
javascript
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1;
let limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
},
// 退出迭代器
return() {
console.log('Exiting early');
return { done: true };
},
};
}
}
let counter = new Counter(3);
let it = counter[Symbol.iterator]();
console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // 3
console.log(it.next().done); // true
for (let i of counter) {
console.log(i);
if (i === 2) {
break; // 退出迭代器 log: Exiting early
}
}
生成器
生成器是一种特殊的函数,它可以在函数内部暂停和恢复代码执行。生成器函数使用function*
关键字定义,函数内部使用yield
关键字暂停代码执行。
WARNING
箭头函数不能用来定义生成器函数。yield
关键字只能在生成器函数内部使用, 并且不能嵌套在普通函数内部。
javascript
function* gen() {
yield 1;
yield 2;
yield 3;
}
let it = gen();
console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // 3
console.log(it.next().done); // true
// 遍历生成器
for (let i of gen()) {
console.log(i);
}
生成器函数的执行过程
- 调用生成器函数时,函数内部的代码并不会立即执行,而是返回一个迭代器对象。
- 当调用迭代器的
next
方法时,生成器函数内部的代码开始执行,直到遇到yield
关键字。 - 当遇到
yield
关键字时,生成器函数会暂停执行,并返回yield
后面的值。 - 当再次调用迭代器的
next
方法时,生成器函数会从上次暂停的地方继续执行,直到遇到下一个yield关键字或者函数结束。 - 当生成器函数执行结束时,迭代器的
next
方法会返回{ done: true }
。 - 如果在生成器函数内部抛出异常,迭代器的
next
方法会抛出异常。 - 如果在生成器函数内部调用
return
方法,迭代器的next
方法会返回{ done: true }
。
生成器函数的参数
生成器函数可以接收参数,参数可以通过next
方法传递。
javascript
function* gen(init) {
console.log(init); // start
let a = yield 1;
console.log(a); // b
let b = yield 2;
console.log(b); // c
let c = yield 3;
console.log(c); // d
}
let it = gen('start');
// 第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数
console.log(it.next('a').value); // 1
console.log(it.next('b').value); // 2
console.log(it.next('c').value); // 3
console.log(it.next('d').done); // true
生成器的应用
异步编程
初步了解生成器函数的执行过程后,我们可以使用生成器函数来实现异步编程。
javascript
const delay = (ms) => new Promise((resolve) => setTimeout(() => {
resolve(ms);
}, ms));
function* gen() {
// yeild后面的异步操作会将按顺序执行
let result = yield delay(1000);
console.log(result); // 1000
let result2 = yield delay(2000);
console.log(result2); // 2000
console.log(result + result2); // 3000
}
let it = gen();
// 调用next后,返回的是一个promise对象 p
let p = it.next().value;
/**
* 对p进行then处理,将结果ms传递给it.next(),此时result = ms
* 对it.next()返回的promise对象进行then处理,将结果ms2传递给it.next(),此时result2 = ms2
* 最后打印result + result2 = ms + ms2 = 1000 + 2000 = 3000
*/
p.then((ms) => {
it.next(ms).value.then((ms2) => {
it.next(ms2);
})
})
将上面的代码进行封装,可以得到一个co
函数。
javascript
const delay = (ms) => new Promise((resolve) => setTimeout(() => {
resolve(ms);
}, ms));
function co(gen) {
let it = gen();
function next(data) {
let result = it.next(data);
if (result.done) {
return;
}
result.value.then((data) => {
next(data);
})
}
next();
}
co(function*(){
let result = yield delay(1000);
console.log(result); // 1000
let result2 = yield delay(2000);
console.log(result2); // 2000
console.log(result + result2); // 3000
})
npm package: co
使用例子
javascript
const co = require('co');
// 定义一个生成器函数
function* myGenerator() {
try {
// 使用 yield 关键字等待 Promise 解决
var result1 = yield Promise.resolve(1);
var result2 = yield Promise.resolve(2);
// 输出结果
console.log(result1 + result2); // 输出 3
} catch (err) {
// 如果有错误发生,打印错误堆栈
console.error(err.stack);
}
}
// 使用 co 执行生成器函数
co(myGenerator).then(function() {
console.log('Generator function has completed.');
}).catch(function(err) {
console.error('An error occurred:', err.stack);
});
无限序列
javascript
function* fibonacci() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
let it = fibonacci();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // 3
console.log(it.next().value); // 5
递归遍历树
javascript
function* traverseTree(node) {
yield node.value;
if (node.left) {
yield* traverseTree(node.left);
}
if (node.right) {
yield* traverseTree(node.right);
}
}
let tree = {
value: 1,
left: {
value: 2,
left: {
value: 4
},
right: {
value: 5
}
},
right: {
value: 3
}
};
for (let value of traverseTree(tree)) {
console.log(value);
}
柯里化
柯里化是一种将多参数函数转换为多个单参数函数的技术。
javascript
// curry 函数接受一个函数 fn 并返回一个新的函数 curried
function curry(fn) {
// curried 函数接受一些参数...
return function curried(...args) {
// 如果提供的参数个数足够,则直接调用原始函数 fn
if (args.length >= fn.length) {
// 使用 apply 来调用 fn 并设置正确的 this 上下文以及参数
return fn.apply(this, args);
} else {
// 如果参数个数不足,返回一个新的函数来接收剩余的参数
return function(...args2) {
// 新函数接收剩余参数 args2,并递归地调用 curried 函数
// 这次调用合并了之前接收到的参数 args 和新的参数 args2
return curried.apply(this, args.concat(args2));
}
}
};
}
// 使用示例:
// 假设有一个三参数的 sum 函数
function sum(a, b, c) {
return a + b + c;
}
// 使用 curry 函数将 sum 函数转换为柯里化函数
let curriedSum = curry(sum);
// 通过柯里化函数一次性传递所有参数
console.log(curriedSum(1, 2, 3)); // => 6
// 通过柯里化函数逐步传递参数
console.log(curriedSum(2)(4)(6)); // => 12
let add5 = curriedSum(2)(3); // 预先填充了前两个参数
console.log(add5(10)); // => 15,最后一个参数被传入,并执行原始的 sum 函数
柯里化的应用
参数复用
javascript
function add(a, b, c) {
return a + b + c;
}
let add5 = curry(add)(5);
console.log(add5(2, 3)); // 10
let add5And6 = curry(add)(5, 6);
console.log(add5And6(7)); // 18
延迟执行
javascript
function ajax(url, data) {
console.log(url, data);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data);
}, 1000);
});
}
let ajaxCurry = curry(ajax);
let post = ajaxCurry('http://www.example.com');
post({ name: '张三' }).then((data) => {
console.log(data);
});
post({ name: '李四' }).then((data) => {
console.log(data);
});
函数组合
javascript
function compose(...fns) {
return function (value) {
return fns.reduceRight((acc, fn) => fn(acc), value);
};
}
function toUpperCase(str) {
return str.toUpperCase();
}
function reverse(str) {
return str.split('').reverse().join('');
}
let reverseAndUpperCase = compose(reverse, toUpperCase);
console.log(reverseAndUpperCase('hello')); // OLLEH