目录
3、for await...of 循环与异步 Generator 函数
四、for、for...of、for...in 和 forEach 循环的区别
(4)、Object.getOwnPropertyNames()
一、Iterator
1、遍历器(Iterator)
JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。
Iterator 是 Symbol 基本数据类型的一个属性,是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的作用有三个:
- 一是,为各种数据结构,提供一个统一的、简便的访问接口;
- 二是,使得数据结构的成员能够按某种次序排列;
- 三是, ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of使用。
Iterator 的遍历过程是这样的:
- 通过 Symbol.iterator 创建一个迭代器,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 随后通过 next 方法进行向下迭代指向下一个位置, next 方法会返回当前位置的对象,对象包含了 value 和 done 两个属性, value 是当前属性的值, done 用于判断是否遍历结束。
- 当 done 为 true 时则遍历结束。
只要有遍历器接口,数据结构就可以用for...of 或 while 循环遍历。
2、Iterator 的方法
(1)、next() 方法
每一次调用next方法,都会返回一个包含value和done两个属性对象的当前成员的信息:
- value属性是当前成员的值。
- done属性是一个布尔值,表示遍历是否结束。
如果你自己写遍历器对象生成函数,那么next方法是必须部署的。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
对于遍历器对象来说,done: false和value: undefined属性都是可以省略的,因此上面的makeIterator函数可以简写成下面的形式。
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++]} :
{done: true};
}
};
}
(2)、return() 方法
return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。
function readLinesSync(file) {
return {
[Symbol.iterator]() {
return {
next() {
return { done: false };
},
return() {
file.close();
return { done: true };
}
};
},
};
}
上面代码中,函数readLinesSync接受一个文件对象作为参数,返回一个遍历器对象,其中除了next方法,还部署了return方法。下面的两种情况,都会触发执行return方法。
// 情况一
for (let line of readLinesSync(fileName)) {
console.log(line);
break;
}
// 情况二
for (let line of readLinesSync(fileName)) {
console.log(line);
throw new Error();
}
上面代码中,情况一输出文件的第一行以后,就会执行return方法,关闭这个文件;情况二会在执行return方法关闭文件之后,再抛出错误。
注意,return方法必须返回一个对象,这是 Generator 规格决定的。
(3)、throw() 方法
throw方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。请参阅 Generator 函数一章。
3、Iterator 接口的部署(★★★★★)
(1)、默认部署
ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator 属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名 Symbol.iterator,它是一个表达式,返回 Symbol 对象的 iterator 属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};
上面代码中,对象 obj 是可遍历的(iterable),因为具有 Symbol.iterator 属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有 next 方法。每次调用 next 方法,都会返回一个代表当前成员的信息对象,具有 value 和 done 两个属性。
(2)、原生具备 Iterator 接口的数据结构
原生具备 Iterator 接口的数据结构有以下 7 个:
- Array
- Map
- Set
- String
- TypedArray
- 函数中的 arguments 对象
- NodeList 对象
①、数组的Symbol.iterator属性
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。
②、对象默认没部署 Iterator 接口,为其部署
let obj = {
0: 'a',
1: 'b',
2: 'c',
}
console.log([...obj]);// Uncaught TypeError: obj is not iterable
console.log({...obj});// {0: "a", 1: "b", 2: "c"}
for(let p of obj){
console.log(p);//TypeError: obj is not iterable
}
// Uncaught TypeError: obj is not iterable
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。
一个对象如果要具备可被 for...of 循环调用的 Iterator 接口,就必须在 Symbol.iterator 的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
--> 在对象的属性上部署 Iterator 接口
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value); // 0, 1, 2
}
上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator 属性对应一个函数,执行后返回当前对象的遍历器对象。
也可以这样为对象添加 Iterator 接口的例子。
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
--> 在原型链上为对象部署 Iterator 接口
function Obj(value) {
this.value = value;
this.next = null;
}
Obj.prototype[Symbol.iterator] = function() {
var iterator = { next: next };
var current = this;
function next() {
if (current) {
var value = current.value;
current = current.next;
return { done: false, value: value };
} else {
return { done: true };
}
}
return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i); // 1, 2, 3
}
上面代码首先在构造函数的原型链上部署 Symbol.iterator 方法,调用该方法会返回遍历器对象 iterator,调用该对象的 next 方法,在返回一个值的同时,自动将内部指针移到下一个实例。
③、类数组对象添加 Iterator 接口的简便方法
对于类数组对象(存在数值键名和length属性),部署 Iterator 接口,有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Iterator 接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll('div')] // 可以执行了
下面是另一个类数组对象调用数组的 Symbol.iterator 方法的例子。
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
注意,普通对象部署数组的 Symbol.iterator 方法,并无效果。
let iterable = {
a: 'a',
b: 'b',
c: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // undefined, undefined, undefined
}
4、调用 Iterator 接口的场合
(1)、Iterator 接口与解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用 Symbol.iterator 方法。
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
(2)、Iterator 接口与扩展运算符
在 数组 或 函数参数 中使用 展开语法(...)时,该语法只能用于 可迭代对象。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
(3)、Iterator 接口与 yield*
yield* 后面跟的是一个可遍历的结构,它会默认调用该结构的遍历器接口。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
(4)、Iterator 接口与 Generator 函数
Symbol.iterator 方法的最简单实现,是使用 Generator 函数。
Generator 函数默认调用 Symbol.iterator 接口。
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
}
[...myIterable] // [1, 2, 3]
// 或者采用下面的简洁写法
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// "hello"
// "world"
上面代码中,Symbol.iterator方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。
二、for...of 循环
1、for...of 循环的概况
for...of 循环是 ES6 新引入的循环,作为遍历所有数据结构的统一的方法。
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
2、for...of循环的应用
for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。
(1)、遍历可迭代的数据结构
①、遍历 Array
for...of 只能遍历部署了 iterator 接口,数组原生具备 iterator 接口(即默认部署了Symbol.iterator属性),可以直接使用for...of循环。
for...of循环读取数组的键值。下面是与for...in与forEach的对比。
var arr = ['a', 'b', 'c', 'd'];
arr.forEach(function (element, index) {
console.log(element); // a b c d
console.log(index); // 0 1 2 3
});
for (let a in arr) {
console.log(a); // 0 1 2 3
}
for (let a of arr) {
console.log(a); // a b c d
}
for...in循环,只能获得对象的键名,不能直接获取键值,而 for...of 可以。
②、遍历 Map 和 Set
Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用 for...of 循环。
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
console.log(e);
}
// Gecko
// Trident
// Webkit
var es6 = new Map();
es6.set("edition", 6).set("committee", "TC39").set("standard", "ECMA-262");
for (var [name, value] of es6) {
console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262
(2)、遍历不可迭代的数据结构
for...of 只能遍历部署了 iterator 接口的数据结构,对于没有部署 iterator 接口的数据结构,会报错:
let obj = {name:"Mary", age:18};
for(var item of obj){
console.log(item);
}
// TypeError: obj is not iterable
可以使用以下方法代替:
let obj = {name:"Mary", age:18};
Object.keys(obj).forEach(key=>{
console.log(key, obj[key]);
})
// name Mary
// age 18
// 或者这样写
for(var key in obj){
console.log(key, obj[key]);
}
// name Mary
// age 18
(3)、遍历类数组对象
类数组对象:有length属性的对象。有的对象默认自带 length 对象,比如一个字符串,有的对象是自定义的 length 属性。
①、遍历包含 Iterator 接口的类数组对象
// 字符串
let str = "hello";
for (let s of str) {
console.log(s);
}
// h
// e
// l
// l
// o
// DOM NodeList对象
let paras = document.querySelectorAll("p");
for (let p of paras) {
console.log(p);
}
// <p></p>
// <p></p>
// <p></p>
// arguments对象
function printArgs() {
for (let x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// 'a'
// 'b'
②、遍历不包含 Iterator 接口的类数组对象
并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 报错
for (let x of arrayLike) {
console.log(x);
}
// 正确
for (let x of Array.from(arrayLike)) {
console.log(x);
}
三、for await...of 循环
for...of 循环,用于遍历同步的 Iterator 接口。
for await...of 循环,虽然可以用于同步遍历器,但是主要用于遍历异步的 Iterator 接口。
1、for await...of 循环的使用
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
上面代码中,createAsyncIterable()返回一个拥有异步遍历器接口的对象,for...of循环自动调用这个对象的异步遍历器的next方法,会得到一个 Promise 对象。await用来处理这个 Promise 对象,一旦resolve,就把得到的值(x)传入for...of的循环体。
2、for await...of 循环的错误捕获
如果 next 方法返回的 Promise 对象被 reject,for await...of 就会报错。
【反例】此时用 try...catch 是捕获不到的:
async function fn () {
try {
for await (const v of createRejectingIterable()) {
console.log(v);
}
} catch (e) {
console.error('111', e);// 不会执行的
}
}
【正例】像下面这样写就可以捕获到错误啦:
async function fn () {
for await (let v of createRejectingIterable()) {
console.log(v);
}
}
fn()
.catch(e => console.log('111', e));
3、for await...of 循环与异步 Generator 函数
for await...of 循环可以与异步 Generator 函数结合起来使用。
async function* prefixLines(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。
// 同步 Generator 函数
function* map(iterable, func) {
const iter = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iter.next();
if (done) break;
yield func(value);
}
}
// 异步 Generator 函数
async function* map(iterable, func) {
const iter = iterable[Symbol.asyncIterator]();
while (true) {
const {value, done} = await iter.next();
if (done) break;
yield func(value);
}
}
上面代码中,map是一个 Generator 函数,第一个参数是可遍历对象iterable,第二个参数是一个回调函数func。map的作用是将iterable每一步返回的值,使用func进行处理。上面有两个版本的map,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。
异步 Generator 函数内部,能够同时使用await和yield命令。可以这样理解,await命令用于将外部操作产生的值输入函数内部,yield命令用于将函数内部的值输出。例如:
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
(async function () {
for await (const line of readLines(filePath)) {
console.log(line);
}
})()
四、for、for...of、for...in 和 forEach 循环的区别
一般的,使用 for...in 遍历对象,使用 for...of 遍历数组。
for:循环代码块一定的次数。
forEach:专门针对 数组 的循环遍历。
for...in:遍历 对象(非 Symbol 类型)的“可枚举”属性(键)。不建议与数组一起使用,数组可以用 Array.prototype.forEach() 和 for ... of 遍历。
for...of:遍历 可迭代对象(带有 Iterator 遍历器 的 对象)的属性值(值)。
// 遍历字符串
let str = "qwertyui";
for (let item in str) {
console.log(item);// 0~7——键
}
for (let key of str) {
console.log(key);// q~i——值
}
// 遍历数组
let arr = [1,2,3];
for (let item in arr) {
console.log(item);// 0~2——键
}
for (let key of arr) {
console.log(key);// 1~3——值
}
// 遍历对象
let obj = {
name: "Mary",
age: 18
}
for (let item in obj) {
console.log(item);// name/age——键
}
for (let key of obj) {
console.log(key);// Uncaught TypeError: obj is not iterable
}
注:普通对象不具有 Iterator。
下面示例展示了 for...of 和 for...in 循环遍历 Array 时的区别:
Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};
let iterable = [3, 5, 7];
iterable.foo = 'hello';
for (let i in iterable) {
console.log(i); // logs 0, 1, 2, "foo", "arrCustom", "objCustom"
}
此循环仅以原始插入顺序记录iterable 对象的可枚举属性。它不记录数组元素3, 5, 7 或hello,因为这些不是枚举属性。但是它记录了数组索引以及arrCustom和objCustom。如果你不知道为什么这些属性被迭代,array iteration and for...in中有更多解释。
for (let i in iterable) {
if (iterable.hasOwnProperty(i)) {
console.log(i); // logs 0, 1, 2, "foo"
}
}
这个循环类似于第一个,但是它使用hasOwnProperty() 来检查,如果找到的枚举属性是对象自己的(不是继承的)。如果是,该属性被记录。记录的属性是0, 1, 2和foo,因为它们是自身的属性(不是继承的)。属性arrCustom和objCustom不会被记录,因为它们是继承的。
for (let i of iterable) {
console.log(i); // logs 3, 5, 7
}
该循环迭代并记录iterable作为可迭代对象定义的迭代值,这些是数组元素 3, 5, 7,而不是任何对象的属性。
五、for 和 forEach 循环哪个更快?为什么?
在循环大量数据时,for 循环语句更快。
因为:forEach 本身是一个函数,它的调用需要判断上下文,这就稍微比 for 循环语句慢了那么一点点,当处理大量数据时,这个微小的差距就会变大,凸显出来。所以,在循环大量数据时,for 循环语句更快。
六、遍历对象与数组
1、遍历对象
(1)、for … in
遍历 对象(非 Symbol 类型)的“可枚举”属性(键)。不建议用来遍历数组,遍历数组可以用 Array.prototype.forEach() 和 for ... of 遍历。
(2)、Object.keys()
Object.keys() 方法会返回一个由给定对象的所有自身 可枚举属性(不包括 Symbol 值作为名称的属性)组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
(3)、Reflect.ownKeys()
静态 Reflect.ownKeys() 方法返回一个由给定对象的所有自身 属性名(包括 不可枚举属性 和 Symbol 值作为名称的属性) 的数组。
它的返回值等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。
(4)、Object.getOwnPropertyNames()
Object.getOwnPropertyNames() 方法返回一个由指定对象的所有自身属性的 属性名(包括 不可枚举属性 但不包括 Symbol 值作为名称的属性)组成的数组。
(5)、Object.values()
Object.values() 方法返回一个给定对象自身的所有可枚举 属性值 的数组,值的顺序与使用 for...in 循环的顺序相同(区别在于 for-in 循环枚举原型链中的属性)。
需要注意的是,Object.values() 方法输出的值的顺序默认有时候会改变:
var an_obj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.values(an_obj)); // ['b', 'c', 'a']
2、遍历数组
(1)、for
(2)、forEach
(3)、for ... in
(4)、for ... of
(1)~(4)详见:本文的 “四、for、for...of、for...in 和 forEach 循环的区别”。