要遍历处理一些数据,我们一般会用到 for 或者 for in 循环,但它们存在着一些缺点,比如,对于变量值大于循环值时,for 会出错。

另外,对于一些类似数组对象的处理,我们又不能直接采用数组的方法,需要进行格式转换,处理完后,又需要再转换成原来的格式,这样操作起来非常的繁琐。

针对这些问题,ES6 引入了 iterators 和 generators 。

一、Iterators

Iterator 简称 迭代器,它为 ES 中不同类型的数据集合提供了一种访问机制。也就是说,只要数据结构具有 Iterator 接口,便可以对该数据进行遍历操作。

其实判断是否具有 Iterator 接口,主要是看数据结构是否原生就具有 Symbol.iterator 属性,这个属性也被表示为 默认的迭代器。

我们所熟知的 数组 就天生具备这个特性。

1.1 访问默认迭代器

你可以使用 Symbol.iterator 访问对象的默认迭代器,像这样:

var arr = [1, 2, 3];
var iterator = arr[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 }"

上面的代码访问了 values 的默认迭代器,并且使用迭代器对象下的 next(),来迭代了数组的每一项。

因为 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

代码中的 isIterable() 函数用于检测对象是否存在默认迭代器,同时也判断了它是否为 函数。

1.2 for…of

除了上面的 next() 方法,我们还可以对具有 Symbol.iterator 属性的数据集合使用 for...of,它可以遍历数组、Set、Map、字符串以及节点列表等类似数组的对象。

1.2.1 遍历 数组、Set、Map

var arr = ['a', 'b'];
var set = new Set([1, 2]);
var map = new Map();

map.set('name', 'yix');
map.set('job', 'web');

for (var str of arr) {
console.log(str);
}

// a
// b

for (var num of set) {
console.log(num);
}

// 1
// 2

for (var attr of map) {
console.log(attr);
}

// ["name", "yix"]
// ["job", "web"]

1.2.2 遍历 字符串

有的时候,字符串中可能包含 双字节 字符,如果用传统的 for 循环,可能无法精确获取对应位置的字符,如:

var str = "A 𠮷 B";

for (var i = 0; i < str.length; i++) {
console.log(str[i]);
}

// A
// 空格
// �
// �
// 空格
// B

上面的代码,预期输出的值,应该为 A 空格 𠮷 空格 B,而实际结果确实 𠮷 字符被两个乱码所替代。

但如果使用 for...of 就不会出现这样的问题了:

var str = "A 𠮷 B";

for (var i of str) {
console.log(i);
}

// A
// 空格
// 𠮷
// 空格
// B

1.2.3 遍历 节点列表 NodeList

要操作 DOM 元素,我们通常会使用 document.querySelectorAll() 或者 document.getElementsByTagName("div") 这些原生方法来获取 DOM对象,而它们返回的 DOM对象 又是一个类似数组的 NodeList 对象,这个对象也具有 Symbol.iterator 属性,因此,我们可以直接对该对象使用 for...of 操作:

var divs = document.getElementsByTagName('div');

for (var div of divs) {
console.log(div.id);
}

以上代码中,调用 getElementsByTagName() 去获取文档中所有 div 的 NodeList 集合,然后使用 for-of 循环输出其对应的 id,这样我们就无需先把转换为数组,再做类似的处理了。

1.2.4 迭代对象的一些方法

对于可迭代的数据集合,ES6提供了三个方法:

  • entries() 返回一个包含键值对的迭代器
  • values() 返回一个包含值的迭代器
  • keys() 返回一个包含键的迭代器

你可以通过这其中的任何一种方法,来迭代检索集合中的每一项。下面对其逐一介绍:

entries() 方法

不同的数据类型,该方法返回的值不同,它的值是一个包含两项的数组。对于数组,第一项是数字索引。对于sets,第一项是值(因为它的键值是相同的)。对于 maps,它的第一项是键。

var arr = ['a', 'b'];
var set = new Set([1, 2]);
var map = new Map();

map.set('name', 'yix');
map.set('job', 'web');

for (var str of arr.entries()) {
console.log(str);
}

// [0, "a"]
// [1, "b"]

for (var num of set.entries()) {
console.log(num);
}

// [1, 1]
// [2, 2]

for (var attr of map.entries()) {
console.log(attr);
}

// ["name", "yix"]
// ["job", "web"]

values() 方法

这个方法只是返回集合中存储的值,但是 数组 不具备该方法:

var arr = ['a', 'b'];
var set = new Set([1, 2]);
var map = new Map();

map.set('name', 'yix');
map.set('job', 'web');

for (var num of set.values()) {
console.log(num);
}

// 1
// 2

for (var attr of map.values()) {
console.log(attr);
}

// yix
// web

keys() 方法

该方法返回集合中的每一个键,对于数组,它只是返回 数字索引。对于 sets,因为它的 键 和 值是相同的,所以keys() 和 values() 将返回相同的内容。对于 maps,keys() 将返回独一无二的键,用代码来证实下:

var arr = ['a', 'b'];
var set = new Set([1, 2]);
var map = new Map();

map.set('name', 'yix');
map.set('job', 'web');

for (var str of arr.keys()) {
console.log(str);
}

// 0
// 1

for (var num of set.keys()) {
console.log(num);
}

// 1
// 2

for (var attr of map.keys()) {
console.log(attr);
}

// name
// job

二、Generators

2.1 Generator函数

Generator函数是 ES6 中新引入的一种特殊函数,但它与普通的函数有些不同。在函数定义的写法上,它在关键词 function 后面增加了一个 * 符号,并且在函数体内,可以使用新的关键词 yield。

该函数返回一个迭代器对象。来看一个最基本的 Generator函数 :

function *hello() {
var greet = 'hello';

console.log('aaa');

yield greet;

console.log('bbb');

yield 'generator';
}

* 符号前后是否有空格不会影响 Generator函数 的定义。

通常情况下,要使用定义后的函数,可以通过函数调用的形式,但是对于 Generator函数,因为函数本身只是返回一个迭代器对象,所以你必须先调用它,然后再用这个迭代器对象的 next() 方法才能执行 Generator函数 中的代码:

function *hello() {
var greet = 'hello';

console.log('aaa');

yield greet;

console.log('bbb');

yield 'generator';
}

var hi = hello(); // 'aaa'

console.log(hi.next()); // {value: "hello", done: false}
console.log(hi.next()); // 'bbb' {value: "generator", done: false}
console.log(hi.next()); // {value: undefined, done: true}

让我们来分析下上面的代码,首先利用变量 hi 来保存 hello函数返回的迭代器对象,首次输出 aaa,到第一个 yield 时结束。

当第一次调用 hi.next() 时,则执行第一个 yield,接着输出一个对象,该对象包含了两个属性,value 和 done。value 的值表示 yield 后面跟着的内容,而 done 则表示迭代还没结束。

当第二次调用 hi.next() 时,则执行第一个 yield 后面的语句,并且执行第二个 yield 的内容,直到遇到下一个 yield 后,便结束运行。此时,是先输出 bbb,然后输出的对象中的 value 是第二个 yield 后面的值,因为迭代还没结束,所以 done 的值仍然为 false 。

当第三次调用 hi.next() 时,因为函数中没有剩余的 yield 和 其他可执行的代码,所以返回的对象中 value 的值为 undefined, 并且 done 的值变为 true 了。

要注意的是,如果关键词 yield 后面没有任何内容,则 value 的值为 undefined :

function *hello() {

yield 'hello';

yield;
}

var hi = hello();

console.log(hi.next()); // {value: "hello", done: false}
console.log(hi.next()); // {value: undefined, done: false}
console.log(hi.next()); // {value: undefined, done: true}

2.2 yield

作为 ES6 中一个新的关键词,yield 语句只能在 Generator函数 中使用。倘若你把它用在普通函数中,则会抛出一个错误:

function hello() {

yield 'hello';
}

var hi = hello(); // 报错:Unexpected string

通过前面讲到的 Generator函数 以及它的运行原理,我们可以知道,yield 语句可以分段执行 Generator函数 中的代码。只有当执行 next() 方法时,才能逐个运行 yield 语句。然后逐个返回对应的值,这一点和 return 的用法相同,但是它们之间也有差异。

yield 与 return 的异同比较

  • 一个函数中可以有多个 yield 语句,但只能有一个 return 语句,即可执行多次和一次的区别
  • yield 具有暂停执行函数内代码的功能,并且能够记录上一次暂停的位置,直到再次执行 next() 方法,而 return 没有这些
  • yield 只能在 Generator函数 中使用,而 return 既可以使用在普通函数,也能使用在 Generator函数
  • yield 和 return 都能返回其后面的值

2.3 Generator函数 返回对象的方法 - next()

其实在上面的文章,我们已经有了很多关于 next() 的描述。 主要是通过调用 next(),来执行一个接一个的执行 yield 语句。

这里要特别增加说明的是,带参数的 next(),我们来看下的例子:

function *sum() {
var a = 1;
var b = yield 2;
yield a + 3;
yield b + 3;
yield 1;
}

var total = sum();

console.log(total.next(5)); // {value: 2, done: false}
console.log(total.next(5)); // {value: 4, done: false}
console.log(total.next(5)); // {value: 8, done: false}
console.log(total.next(5)); // {value: 1, done: false}

如上面代码,与之前的 next() 调用不同的是,在这段代码中,我们每次调用 next() 时,都给它传递一个数字 5 作为参数。

第一次执行 next(5),返回的 value 的值为 2。即 b = yield 2 运行的结果。

第二次执行 next(5),返回的 value 的值为 4。即 yield a + 3 运行的结果。

第三次执行 next(5),返回的 value 的值为 8。这个结果有点出乎我们所料,此时执行的是 yield b + 3,而 b 的值是 yield 2,按理来说应该是 2 + 3 等于 5 才对。但是,由于 next() 方法传递了参数 5,此时上一个 yield 的值被这个参数所覆盖,所以才出现 5 + 3 等于 8 这样的结果 。

第四次执行 next(5),返回的 value 的值为 1。即 yield 1 运行的结果。

综上所述,next() 方法传递参数只会影响那些有 yield 参与操作的 yield 语句。

2.4 Generator函数 返回对象的方法 - return()

除了上面说的 next(),Generator函数 返回的对象中还有 return() 方法,该方法返回一个特定的值,并且结束迭代当前 Generator函数 中的 yield 语句。

来看个相关的例子:

function *hello() {
var greet = 'hello';

yield greet;

yield 'generator';
}

var hi = hello();

console.log(hi.next()); // {value: "hello", done: false}
console.log(hi.return()); // {value: undefined, done: true}
console.log(hi.next()); // {value: undefined, done: true}

第一次调用 next(),正常返回对应的对象。

接着调用 return(),此时返回的对象中 value 属性值为 undefined,done 属性值则为 true,即表示停止迭代了。

当后面再次调用 next(),后面返回的对象都是 {value: undefined, done: true} 了。

和 next() 类似,return() 函数也可以接收一个参数,此时,调用该方法后,返回的对象中 value 的属性值就是该参数,而其他的基本不变:

function *hello() {
var greet = 'hello';

yield greet;

yield 'generator';
}

var hi = hello();

console.log(hi.next()); // {value: "hello", done: false}
console.log(hi.return('abc')); // {value: "abc", done: true}
console.log(hi.next()); // {value: undefined, done: true}

2.5 Generator函数 返回对象的方法 - throw()

最后,Generator函数 返回的对象中还有一个 throw() 方法。

在 JavaScript 中,throw() 方法通常与 try…catch 结合使用,主要用于当程序运行出错时,我们可以自定义一个错误对象,然后向控制台抛出一个错误:

var number = prompt('输入一个 0 到 10 之间的数', '');

try {
if (number > 10)
throw 'errBig';
else if (number < 0)
throw "errSmall";
} catch(e) {
if(e === 'errBig')
alert('错误! 这个值太大');
if(e ==='errSmall')
alert('错误! 这个值太小');
}

上述代码检测用户输入的值,然后根据值的大小来定义不同的错误,最后反馈对应的错误。

以下代码演示了 Generator函数 返回对象中 throw() 方法的用法:

function *error() {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
}

var err = error();
err.next();

try {
err.throw('abc');
err.throw('def');
} catch (e) {
console.log('外部捕获', e);
}

// 外部捕获 abc
// 外部捕获 def

运行 err.throw('abc') 时,错误在 Generator函数 内部被捕获。当再次运行 err.throw('def'),错误被函数外部的 catch 所捕获。

2.6 Generator函数 作为 对象方法

要将 Generator函数 定义成 对象中的方法,可采用如下写法:

var person = {
name: 'yix',
showName: function *() {
console.log(this.name);
yield this.name;
}
};

var getName = person.showName();

getName.next(); // "yix"

通过前面的 对象 章节,我们知道,在对象字面量中定义方法可以采用缩写形式,于是,person 中的 showName 可以这样改写:

var person = {
name: 'yix',
* showName() {
console.log(this.name);
yield this.name;
}
};

以上两段定义 person 对象的代码是等价的。

2.7 Generator函数 与 异步操作

利用 Generator函数 中的 yield,我们可以很方便的处理一些异步操作,而无需使用一层或多层回调函数,如下面的例子:

function *paint() {
var resp = yield getData('data.json'),
data = formatData(resp);

insertHtml(data);
}

var render = paint();
// 获取数据
render.next();

// 格式化数据 并 插入数据
render.next();

上面的代码,定义了一个 paint 这个 Generator函数,在这个函数中,getData函数通过类似ajax的方式获取到数据。render = paint() 这行表示将返回的遍历器赋值给 render,当调用 render.next() 时,将会获取到数据,并将返回的数据赋值给了 resp。然后我们接着调用 render.next(),此时,便会执行 Generator函数 中其余的代码。

试想下,如果你需要的数据需要通过多个ajax才能拿到,那你肯定少不了层层嵌套的回调函数。但使用 yield 来编写,则显得非常的直观和简洁。