要遍历处理一些数据,我们一般会用到 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,接着输出一个对象,该对象包含了两个属性,valuedonevalue 的值表示 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 来编写,则显得非常的直观和简洁。