1 for…in

1.1 概念

for...in语句任意顺序遍历一个对象的除Symbol以外的可枚举属性

1.2 语法

1
2
for (variable in object)
  statement
  • variable

    在每次迭代时,variable会被赋值为不同的属性名。

  • object

    非Symbol类型的可枚举属性被迭代的对象。

1.3 描述

for...in 循环只遍历可枚举属性。循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)。

提示:for...in不应该用于迭代一个 Array

  • 数组索引只是具有整数名称的枚举属性,并且与通用对象属性相同。不能保证for ... in将以任何特定的顺序返回索引。
  • for ... in循环语句将返回所有可枚举属性,不仅是数组索引,还包括非整数类型的名称继承的那些。

因为迭代的顺序是依赖于执行环境的,所以数组遍历不一定按次序访问元素。因此当迭代访问顺序很重要的数组时,最好用整数索引去进行for循环(或者使用Array.prototype.forEach()for...of循环)。

如果你只要考虑对象本身的属性,而不是它的原型,那么使用 getOwnPropertyNames()或执行hasOwnProperty()来确定某属性是否是对象本身的属性(也能使用propertyIsEnumerable)。

for-in循环的每次迭代都会产生更多开销,因此要比其他循环类型慢,一般速度为其他类型循环的 1/7。因此,除非明确需要迭代一个属性数量未知的对象,否则应避免使用for-in循环。如果需要遍历一个数量有限的已知属性列表,使用其他循环会更快,

1.4 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 基本使用
var obj = {a:1, b:2, c:3};    
for (var prop in obj) {
  console.log("obj." + prop + " = " + obj[prop]);
}
// Output:
// "obj.a = 1"
// "obj.b = 2"
// "obj.c = 3"

// 仅遍历自身属性
var triangle = {a: 1, b: 2, c: 3};
function ColoredTriangle() {
  this.color = 'red';
}
// triangle成为了ColorTriangle原型链上的属性
ColoredTriangle.prototype = triangle;
var obj = new ColoredTriangle();
for (var prop in obj) {
  // 利用hasOwnProperty筛选出自身属性
  if (obj.hasOwnProperty(prop)) {
    console.log(`obj.${prop} = ${obj[prop]}`);
  } 
}
// Output:
// "obj.color = red"

2 for…of

2.1 概念

for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

2.2 语法

1
2
3
for (variable of iterable) {
    //statements
}
  • variable

    在每次迭代中,将不同属性的分配给变量。

  • iterable

    被迭代枚举其属性的对象。

2.3 描述

  • 特点

    • forEach相比,可以正确响应break, continue, return

    • for-of 循环不仅支持数组,还支持大多数类数组对象,例如 DOM nodelist 对象。

    • for-of 循环也支持字符串遍历,它将字符串视为一系列 Unicode 字符来进行遍历。

    • for-of 也支持 MapSet (两者均为 ES6 中新增的类型)对象遍历。

    • 这是最简洁、最直接的遍历数组元素的语法。

    • 这个方法避开了for-in循环的所有缺陷。

  • 但需要注意的是,for-of循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用for-in循环(这也是它的本职工作)。

2.4 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 迭代Array
let iterable = [10, 20, 30];
for (let value of iterable) {
    value += 1;
    console.log(value);
}
// 11
// 21
// 31

// 迭代string
let iterable = "boo";
for (let value of iterable) {
  console.log(value);
}
// "b"
// "o"
// "o"

// 迭代Map
let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);

for (let entry of iterable) {
  console.log(entry);
}
// ["a", 1]
// ["b", 2]
// ["c", 3]

for (let [key, value] of iterable) {
  console.log(value);
}
// 1
// 2
// 3

// 迭代Set
let iterable = new Set([1, 1, 2, 2, 3, 3]);

for (let value of iterable) {
  console.log(value);
}
// 1
// 2
// 3

// 迭代arguments对象
(function() {
  for (let argument of arguments) {
    console.log(argument);
  }
})(1, 2, 3);

// 1
// 2
// 3

// 迭代DOM集合
// 迭代 DOM 元素集合,比如一个NodeList对象:下面的例子演示给每一个 article 标签内的 p 标签添加一个 "read" 类。
let articleParagraphs = document.querySelectorAll("article > p");

for (let paragraph of articleParagraphs) {
  paragraph.classList.add("read");
}

3 区别

无论是for...in还是for...of语句都是迭代一些东西。它们之间的主要区别在于它们的迭代方式。

  • for...in语句以任意顺序迭代对象的可枚举属性。(适合对象属性遍历)
  • for...of 语句遍历可迭代对象定义要迭代的数据。(适合数组遍历)

以下示例显示了与Array一起使用时,for...of循环和for...in循环之间的区别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Object.prototype.objCustom = function() {}; 
Array.prototype.arrCustom = function() {};
/* 每个对象将继承objCustom属性,并且作为Array的每个对象将继承arrCustom属性,由于继承和原型链,对象iterable继承属性objCustom和arrCustom。*/

let iterable = [3, 5, 7];
iterable.foo = 'hello';

for (let i in iterable) {
  console.log(i); // 0, 1, 2, "foo", "arrCustom", "objCustom"
}
/*此循环仅以原始插入顺序记录iterable对象的可枚举属性。它不记录数组元素3, 5, 7 或hello,因为这些不是枚举属性。但是它记录了数组索引以及arrCustom和objCustom。*/

for (let i in iterable) {
  if (iterable.hasOwnProperty(i)) {
    console.log(i); // 0, 1, 2, "foo"
  }
}
/*这个循环类似于第一个,但是它使用hasOwnProperty()来检查,如果找到的枚举属性是对象自己的(不是继承的)。如果是,该属性被记录。记录的属性是0, 1, 2和foo,因为它们是自身的属性(不是继承的)。属性arrCustom和objCustom不会被记录,因为它们是继承的。*/

for (let i of iterable) {
  console.log(i); // 3, 5, 7
}
/*该循环迭代并记录iterable作为可迭代对象定义的迭代值,这些是数组元素3, 5, 7,而不是任何对象的属性。*/

4 迭代协议

迭代协议 - JavaScript | MDN (mozilla.org)

迭代协议作为 ECMAScript 2015 的一组补充规范,分为可迭代协议迭代器协议

4.1 可迭代协议

  • 可迭代协议指出,要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator访问该属性:
1
2
let someString = "hi";
typeof someString[Symbol.iterator];          // "function"
  • 人话总结:一个对象或者原型链上有@@iterator(Symbol.iterable)属性,那么就称其为可迭代对象

4.2 可迭代对象示例

4.2.1 内置可迭代对象:

目前所有的内置可迭代对象如下:StringArrayTypedArrayMapSet,它们的原型对象都实现了 @@iterator 方法。

4.2.2 自定义可迭代对象:

1
2
3
4
5
6
7
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};
[...myIterable]; // [1, 2, 3]

4.2.3 接受可迭代对象的内置API

  • new Map([iterable])new WeakMap([iterable])
  • new Set([iterable])new WeakSet([iterable])
  • Promise.all([iterable])Promise.race([iterable])
  • Array.from([iterable])

4.2.4 需要可迭代对象的语法

一些语句和表达式:

  • for..of
1
2
3
for(let value of ["a", "b", "c"]){
    console.log(value);
}
  • 展开语法
1
[..."abc"]; // ["a", "b", "c"]
  • yield*
1
2
3
4
function* gen() {
  yield* ["a", "b", "c"];
}
gen().next(); // { value: "a", done: false }
  • 解构赋值
1
2
[a, b, c] = new Set(["a", "b", "c"]);
a // "a"

4.3 迭代器协议

  • 只有实现了一个拥有以下语义的 next() 方法,一个对象才能成为迭代器
属性
next 一个无参数的或者可以接受一个参数的函数,返回一个应当拥有以下两个属性的对象:
done(boolean)
如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。

value
迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

next() 方法必须返回一个对象,该对象应当有两个属性: donevalue,如果返回了一个非对象值(比如 falseundefined),则会抛出一个 TypeError 异常("iterator.next() returned a non-object value")。
  • 人话翻译:迭代器指的是具有next()方法的类或函数next()是一个无参数或者可以接受一个参数的函数,返回一个拥有done和value属性对象

4.4 迭代器示例

4.4.1 自定义迭代器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function makeIterator(array) {
    let nextIndex = 0;
    return {
       next: function () {
           return nextIndex < array.length ? {
               value: array[nextIndex++],
               done: false
           } : {
               done: true
           };
       }
    };
}

let it = makeIterator(['gsq', 'zs']);

console.log(it.next().value); // 'gsq'
console.log(it.next().value); // 'zs'
console.log(it.next().done);  // true

4.4.2 使用class改写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyIterator {
  constructor(data) {
    this.data = data
  }
  [Symbol.iterator]() {
    // 注意这里index没有定义为原型属性
    // 每个迭代之间的index是相互独立的
    // 使得多次迭代更安全?
    let index = 0;

    return {
      next: () => {
        if (index < this.data.length) {
          return {value: this.data[index++], done: false}
        } else {
          return {done: true}
        }
      }
    }
  }
}
let mi = new MyIterator(['gsq', 'zs']);

console.log(mi.next().value); // 'gsq'
console.log(mi.next().value); // 'zs'
console.log(mi.next().done);  // true

4.4.3 使用Generator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function* myGeneratorIterator(array) {
    let nextIndex = 0;

    while(nextIndex < array.length) {
        yield array[nextIndex++];
    }
}

let gen = makeSimpleGenerator(['gsq', 'zs']);

console.log(gen.next().value); // 'gsq'
console.log(gen.next().value); // 'zs'
console.log(gen.next().done);  // true

生成器对象既是可迭代对象也是迭代器

5 生成器

  • 生成器与普通函数相差无几
1
2
3
function* gen(){
	yield 1
}

5.1 特点

  1. 生成器函数在执行时能暂停,后面又能从暂停处继续执行
  2. 调用一个生成器函数并不会马上执行内部的语句,而是返回一个这个生成器的迭代器对象
1
2
const a = gen() 
typeof a[Symbol.iterator] // 'function'
  1. 当返回的迭代器的next()方法被首次调用时,其内的语句会执行到第一个出现yield的位置为止
  2. yield后紧跟迭代器要返回的值
  3. 如果使用了yield*,则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)
  4. 调用next()方法时,如果传入了参数,那么这个参数会传给上一条执行的yield语句左边的变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function *gen(){
    yield 10;
    x = yield 'foo';
    yield x;
}

var gen_obj=gen();
console.log(gen_obj.next());// 执行 yield 10,返回 10
console.log(gen_obj.next());// 执行 yield 'foo',返回 'foo'
console.log(gen_obj.next(100));// 将 100 赋给上一条 yield 'foo' 的左值,即执行 x=100,返回 100
console.log(gen_obj.next());// 执行完毕,value 为 undefined,done 为 true
  1. 当在生成器函数中显式 return 时,会导致生成器立即变为完成状态,即调用 next() 方法返回的对象的 donetrue。如果 return 后面跟了一个值,那么这个值会作为当前调用 next() 方法返回的 value 值。可以理解为,return就是生成器最后一个yield