let colors = ['red','yellow'];
for (let i = 0; i < colors.length; i++) {
const element = colors[i];
}
缺点: 循环嵌套之后复杂度就会增加,同时需要追踪多个索引变量。
迭代器是带有特殊接口的对象来程序化的返回下一位置的项。
所有的迭代器对象都带有next()方法,并返回一个包含两个属性的结果对象。
{
value: 代表下一个位置的值
done: 下面是否还有值可供迭代 true/false
}
原理:迭代器使用内部指针,来指向集合中某个值的位置,next()方法被调用时,返回下一位置的值。
例子: 在 ECMAScript 5 中创建一个迭代器:
function createInterator(items) {
var i = 0;
return {
next: function() {
var done = i >= items.length;
var value = !done ? items[i++] : undefined;
return {
done: done,
value: value
};
}
};
}
var interator = createInterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"
以上示例中,根据 ECMAScript 5 规范模拟实现的迭代器还是有些复杂。
幸运的是,ECMAScript 6 中还提供了生成器,简化了迭代器对象创建的过程。
生成器是返回迭代器的函数。
特点: function * ,同时还使用yield关键字。
原理:当执行流遇到yield语句时,该生成器就停止运转了,不会执行其他任何部分的代码,指导迭代器再次调用next()方法,yield再执行。
// 生成器 同样一个例子,使用生成器来创建迭代器就显得很简洁了
function* createInterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
// 调用生成器类似于调用函数,但是前者返回一个迭代器
var interator = createInterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"
在该例子中,createInterator生成器函数被传入一个数组。在函数内部,一个循环正在执行并且把数组中的值返回给迭代器。当遇到yield时,循环就会停止,每次调用next()时,循环会继续执行直到再次遇到yield语句。
注意: yield 只能用在生成器内部。yield语句是无法跨越函数边界的。
// 生成器
function* createInterator(items) {
items.forEach(function(item){
// 语法错误
yield item + 1;
});
}
let createInterator = function* (items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
let iterator = createIterator([1, 2, 3]);
注意:无法使用箭头函数来创建生成器。
let o = {
createInterator:function* (items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
},
// 或者
*createInterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
}
let iterator = o.createIterator([1, 2, 3]);
可迭代类型是指包含Symbol.iterator 属性的对象。
属于可迭代类型的对象: 数组,set,map,字符串 , 因为他们都有默认的迭代器
目的: 可迭代类型是为了es6新增的for-of循环而设计的
所有由生成器创建的迭代器都是可迭代类型,因为生成器在默认情况下会自赋值给 Symbol.iterator 属性。
优点: 完全不需要在集合中追踪索引,让你更专注于集合内容的操作
原理:for-lof 循环会在可迭代类型每次迭代执行后调用 next() 并将结果对象存储在变量中。循环会持续进行直到结果对象的 done 属性为 true。
let values = [1,2,3];
for (const num of values) {
console.log(num);
}
// 依次输出 1 2 3
过程: for-of循环会调用values数组的Symbol.iterator方法来获取迭代器iterator(Symbol.iterator 方法由幕后的 JavaScript 引擎调用)。之后再调用iterator.next()并将结果对象中的value属性值依次赋给num变量。当检测到结果对象中的done为true时,循环退出。
结论: 如果你只想简单的迭代数组或集合中的元素,那么 for-of 循环比 for 要更好。for-of 一般不容易出错,因为要追踪的条件更少。所以还是把 for 循环留给复杂控制条件的需求吧。
你可以使用Symbol.iterator来访问对象默认的迭代器,like this:
let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
既然 Symbol.iterator 定义了默认的迭代器,你可以如下使用它来确定一个对象是否可迭代:
function isIterator(object) {
return typeof object[Symbol.iterator] === 'function';
}
console.log(isIterator([1,2,3])); // true
console.log(isIterator('hello')); // true
console.log(isIterator(new Map())); // true
console.log(isIterator(new Set())); // true
console.log(isIterator(new WeakMap())); // false
console.log(isIterator(new WeakSet())); // false
isIterator() 函数可以简单的查看对象是否有默认的并且类型为函数的迭代器。for-of在执行前也会做相似的检查。
情景: 开发者自定义的对象默认是不可迭代类型,但是你可以为他们创建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 item of collection) {
console.log(item);
}
// 依次输出 1 2 3
现在你已经见识了数组默认迭代器的用法,然而 ECMAScript 6 还内置了许多迭代器使得操作集合中的数据更加轻松。
ECMAScript 6 内置了三种类型的集合对象:数组,map 和 set 。它们都有如下内置的迭代器供你浏览数据。
规则:
- 数组 键是索引
- set 键和值相同
- map 正常返回每一项
let data = new Map();
data.set("title", "hello");
data.set("content", "nihao");
for (const entry of colors.entries()) {
console.log(entry);
}
for (const entry of tracking.entries()) {
console.log(entry);
}
for (const entry of data.entries()) {
console.log(entry);
}
输出:
[0, "red"]
[1, "yellow"]
[12, 12]
[23, 23]
["title", "hello"]
["content", "nihao"]
返回集合中每一项的值。
let color = ["red", "yellow"];
let tracking = new Set([12, 23]);
let data = new Map();
data.set("title", "hello");
data.set("content", "nihao");
for (const value of colors.values()) {
console.log(value);
}
for (const value of tracking.values()) {
console.log(value);
}
for (const value of data.values()) {
console.log(value);
}
输出:
"red"
"yellow"
12
23
"hello"
"nihao"
返回集合中每一项的键。
let color = ["red", "yellow"];
let tracking = new Set([12, 23]);
let data = new Map();
data.set("title", "hello");
data.set("content", "nihao");
for (const key of colors.keys()) {
console.log(key);
}
for (const key of tracking.keys()) {
console.log(key);
}
for (const key of data.keys()) {
console.log(key);
}
输出:
0
1
12
23
"title"
"content"
规则:
- 数组和set默认values()方法
- map默认entries() 方法
let color = ["red", "yellow"];
let tracking = new Set([12, 23]);
let data = new Map();
data.set("title", "hello");
data.set("content", "nihao");
// 等效于调用 colors.values()
for (const value of colors) {
console.log(value);
}
// 等效于调用 tracking.values()
for (const value of tracking) {
console.log(value);
}
// 等效于调用 data.entries()
for (const entry of data) {
console.log(entry);
}
输出:
"red"
"yellow"
12
23
["title", "hello"]
["content", "nihao"]
解构与for-of循环 destructuring and for-of loops Map 构造函数的默认行为有助于在 for-of 循环中使用解构。如下所示:
let data = new Map();
data.set("title", "hello");
data.set("content", "nihao");
for (const [key, value] of data) {
console.log(key + "=" + value);
}
ECMAScript 5原理:对字符串启用了方括号语法来访问字符(例如,text[0] 可以获得该字符串中的首个字符,等等)。
方括号语法访问的是编码单元(code unit)而非字符本身,所以当获取双字节字符时会有意想不到的结果。
双字节字符会被当做两个编码单元对待。
var message = "A ð ®· B";
for (let i = 0; i < message.length; i++) {
console.log(message[i]);
}
输出:
A
(blank)
(blank)
(blank)
(blank)
B
ECMAScript 6: 目标是完全支持 Unicode,所以字符串的默认迭代器就是为了解决字符的迭代问题而做的努力。
字符串默认迭代器作用的是字符本身而非编码单元。
var message = "A ð ®· B";
for (const c of message) {
console.log(c);
}
输出:
A
(blank)
ð ®·
(blank)
B
文档对象模型(DOM)中包含了一个 NodeList 类型用来表示一些 DOM 元素的集合。
在ECMAScript6中使用for-of循环或在任何对象默认的迭代器的内部来迭代NodeList
var divs = document.getElementsByTagName('div');
for (const div of divs) {
console.log(div.id);
}
使用扩展运算符将 set 转换为数组:
let set = new Set([1,2,3,4,3,3,2,5]),
array = [...set];
console.log(array);
使用扩展运算符将 map 转换为数组:
let map = new Map([['name','kk'],['age',23]]),
array = [...map];
console.log(array);
既然扩展运算符可以用在任意的可迭代类型上,那么它就成为了将可迭代类型转换为数组最简单的办法。你可以将字符串和浏览器中的 NodeList 对象分别转换为包含字符(不是编码单元)或 DOM 节点的数组。
原理: 当你向next方法传入参数时,生成器使用该参数作为yield语句的值。
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 }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
首次调用next()有些特殊,传给他的任何参数都会被忽略。因为传给next()的参数是作为已返回的yield语句的值,那么首次调用传给 next() 的参数必须要在返回首个 yield 语句之前可供访问。显然这是不可能的,所以没有理由给首次调用的 next() 方法传参。
如果考虑生成器函数内部在每次运行时都执行了哪些代码,思路可能会清晰一些。图片用颜色区分了每次 yield 之前代码的执行情况。
function* createIterator() {
let first = yield 1;
let second = yield first + 2; // 4 + 2 抛出错误
yield second + 3; // 不执行
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("boom"))); // 由生成器抛出错误
这种中断代码执行的行为类似于直接抛出错误
function* createIterator() {
let first = yield 1;
let second;
try {
second = yield first + 2; // yield first + 2 throw error 并且迭代器继续执行
} catch (error) {
second = 6; // 捕捉到错误,给second赋其他值
}
yield second + 3;
}
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("boom"))); // "{ value: 9, done: false }"
console.log(iterator.next()); // "{value: undefined, done: true}"
throw() 指示迭代器继续执行的同时并抛出错误。至于这些指令如何处理,这就由生成器内部的代码来决定。
作用:使用return语句让生成器函数提前执行完毕并针对next的调用来指定一个返回值。
原理:生成器会将return语句的出现判断为所有任务已经处理完毕,done被赋值为true。
function* createGenerator() {
yield 1;
yield 2;
return;
yield 3; // 不可达的
}
let iterator = createGenerator();
iterator.next(); // { value: 1, done : false}
iterator.next(); // { value: 2, done : false}
iterator.next(); // { value: undefined, done : true}
iterator.next(); // { value: undefined, done : true}
指定一个返回值来赋给结果对象中的value属性:例如:
function* createGenerator() {
yield 1;
yield 2;
return 'i`m back'; // 指定的返回值
yield 3; // 不可达的
}
let iterator = createGenerator();
iterator.next(); // { value: 1, done : false}
iterator.next(); // { value: 2, done : false}
iterator.next(); // { value: 'i`m back', done : true}
iterator.next(); // { value: undefined, done : true}
// 任何指定的返回值都只能被结果对象使用一次。所以value为undefined
用法: 生成器可以使用 yield 和星号(*)这种特殊形式来代理其它生成器。
例如:
function* createNumberGenerator() {
yield 1;
yield 2;
}
function* createColorGenerator() {
yield "red";
yield "yellow";
}
function* createCombinedGenrator() {
yield* createNumberGenerator;
yield* createColorGenerator;
yield true;
}
let iterator = createCombinedGenrator();
iterator.next(); // { value: 1,done: false}
iterator.next(); // { value: 2,done: false}
iterator.next(); // { value: 'red',done: false}
iterator.next(); // { value: 'yellow',done: false}
iterator.next(); // { value: true,done: false}
iterator.next(); // { value: undefined,done: true}
从迭代器返回的值来看,他等价于只使用了一个迭代器并返回了所有的值。
进一步使用生成器返回的值处理复杂的任务,例如:
function* createNumberGenerator() {
yield 1;
return 2;
}
function* createRepeatGenerator(count) {
for (let i = 0; i < count; i++) {
yield `repeat${count}`
}
}
function* createCombinedGenrator() {
let result = yield* createNumberGenerator;
yield result; // 2 显式添加了额外的 yield 语句来输出 createNumberIterator() 生成器的返回值。
yield* createRepeatGenerator(result);
yield true;
}
let iterator = createCombinedGenrator();
iterator.next(); // { value: 1,done: false}
iterator.next(); // { value: 2,done: false}
iterator.next(); // { value: 2,done: false}
iterator.next(); // { value: 'repeat2',done: false}
iterator.next(); // { value: 'repeat2',done: false}
iterator.next(); // { value: true,done: false}
iterator.next(); // { value: undefined,done: true}
异步操作的传统做法是在它结束之后调用回调函数。例如,考虑如下 Node.js 读取文件的代码:
let fs = require("fs");
fs.readFile("readme.md", function(error, res) {
if (error) throw error;
doSomethingWith(res);
console.log("done");
});
因为 yield 可以中断执行,并在继续运行之前等待 next() 方法的调用,你可以不使用回调函数来实现异步调用。首先,你需要一个函数来调用生成器以便让迭代器开始运行,例如这样:
function runTask(task) {
// 创建一个迭代器
let taskIterator = task();
// 任务开始运行,并且把返回的结果赋值给result
let result = taskIterator.next();
// 递归函数持续调用next方法直到result的返回值的done为true就停止递归
function step() {
if (!result.done) {
result = taskIterator.next();
step();
}
}
// 开始递归执行任务
step();
}
runTask(function*() {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
});
function runTask(task) {
let taskIterator = task();
let result = taskIterator.next();
function step() {
if (!result.done) {
// 让数据在yield之间流动只需要给next传入参数
result = taskIterator.next(result.value);
step();
}
}
step();
}
runTask(function *() {
let value = yield 1;
console.log(value); // 1
value = yield value + 3;
console.log(value); // 4
});
标识异步操作的方法:
function fetchData() {
return function(callback) {
callback(null, "hi");
};
}
可以使用延迟函数的执行以将它改造为异步函数,例如:
function fetchData() {
return function(callback) {
setTimeout(() => {
callback(null, "hi");
}, 30);
};
}
将任务运行器改造成包含异步操作的函数,例如:
function runTask(task) {
// 创建迭代器
let taskIterator = task();
// 任务开始执行
let result = taskIterator.next();
// 递归调用next
function step() {
// 如果任务未完成
if (!result.done) {
if (typeof result.value === "function") {
result.value(function(err, data) {
if (err) result = task.throw(err);
result = taskIterator.next(data);
step();
});
} else {
result = taskIterator.next(result.value);
step();
}
}
}
// 开始递归
step();
}
这样这个任务运行器就做好了处理异步任务的准备了。再来看读取文件:
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback);
};
}
readfile() 方法接收 filename 参数,并返回一个内部调用回调函数的函数。该回调函数会直接传给 fs.readFile() 方法并在异步方法结束后执行。你可以像下面这样使用 yield 来运行这个任务:
runTask(function*() {
let contents = yield readFile("readme.md");
doSomthingWith(contents);
console.log("done");
});
优点:在灭于显示的书写回调函数的同时实现了异步的readFile操作。
只要包含异步操作的函数和上述 fs.readFile() 接口一致,你就可以使用该例来书写从视觉上认为是同步的逻辑。
参考书籍:《Understanding ECMAScript 6》
https://www.gitbook.com/book/oshotokill/understandinges6-simplified-chinese/details