Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 系列之迭代器与 for of #90

Open
mqyqingfeng opened this issue Jul 10, 2018 · 15 comments
Open

ES6 系列之迭代器与 for of #90

mqyqingfeng opened this issue Jul 10, 2018 · 15 comments

Comments

@mqyqingfeng
Copy link
Owner

起源

一段标准的 for 循环代码:

var colors = ["red", "green", "blue"];

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

看着很简单,但是再回顾这段代码,实际上我们仅仅是需要数组中元素的值,但是却需要提前获取数组长度,声明索引变量等,尤其当多个循环嵌套的时候,更需要使用多个索引变量,代码的复杂度就会大大增加,比如我们使用双重循环进行去重:

function unique(array) {
    var res = [];
    for (var i = 0, arrayLen = array.length; i < arrayLen; i++) {
        for (var j = 0, resLen = res.length; j < resLen; j++) {
            if (array[i] === res[j]) {
                break;
            }
        }
        if (j === resLen) {
            res.push(array[i]);
        }
    }
    return res;
}

为了消除这种复杂度以及减少循环中的错误(比如错误使用其他循环中的变量),ES6 提供了迭代器和 for of 循环共同解决这个问题。

迭代器

所谓迭代器,其实就是一个具有 next() 方法的对象,每次调用 next() 都会返回一个结果对象,该结果对象有两个属性,value 表示当前的值,done 表示遍历是否结束。

我们直接用 ES5 的语法创建一个迭代器:

function createIterator(items) {
    var i = 0;
    return {
        next: function() {
            var done = i >= item.length;
            var value = !done ? items[i++] : undefined;

            return {
                done: done,
                value: value
            };
        }
    };
}

// iterator 就是一个迭代器对象
var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }

for of

除了迭代器之外,我们还需要一个可以遍历迭代器对象的方式,ES6 提供了 for of 语句,我们直接用 for of 遍历一下我们上节生成的遍历器对象试试:

var iterator = createIterator([1, 2, 3]);

for (let value of iterator) {
    console.log(value);
}

结果报错 TypeError: iterator is not iterable,表明我们生成的 iterator 对象并不是 iterable(可遍历的)。

那什么才是可遍历的呢?

其实一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是"可遍历的"(iterable)。

举个例子:

const obj = {
    value: 1
};

for (value of obj) {
    console.log(value);
}

// TypeError: iterator is not iterable

我们直接 for of 遍历一个对象,会报错,然而如果我们给该对象添加 Symbol.iterator 属性:

const obj = {
    value: 1
};

obj[Symbol.iterator] = function() {
    return createIterator([1, 2, 3]);
};

for (value of obj) {
    console.log(value);
}

// 1
// 2
// 3

由此,我们也可以发现 for of 遍历的其实是对象的 Symbol.iterator 属性。

默认可遍历对象

然而如果我们直接遍历一个数组对象:

const colors = ["red", "green", "blue"];

for (let color of colors) {
    console.log(color);
}

// red
// green
// blue

尽管我们没有手动添加 Symbol.iterator 属性,还是可以遍历成功,这是因为 ES6 默认部署了 Symbol.iterator 属性,当然我们也可以手动修改这个属性:

var colors = ["red", "green", "blue"];

colors[Symbol.iterator] = function() {
    return createIterator([1, 2, 3]);
};

for (let color of colors) {
    console.log(color);
}

// 1
// 2
// 3

除了数组之外,还有一些数据结构默认部署了 Symbol.iterator 属性。

所以 for...of 循环可以使用的范围包括:

  1. 数组
  2. Set
  3. Map
  4. 类数组对象,如 arguments 对象、DOM NodeList 对象
  5. Generator 对象
  6. 字符串

模拟实现 for of

其实模拟实现 for of 也比较简单,基本就是通过 Symbol.iterator 属性获取迭代器对象,然后使用 while 遍历一下:

function forOf(obj, cb) {
    let iterable, result;

    if (typeof obj[Symbol.iterator] !== "function")
        throw new TypeError(result + " is not iterable");
    if (typeof cb !== "function") throw new TypeError("cb must be callable");

    iterable = obj[Symbol.iterator]();

    result = iterable.next();
    while (!result.done) {
        cb(result.value);
        result = iterable.next();
    }
}

内建迭代器

为了更好的访问对象中的内容,比如有的时候我们仅需要数组中的值,但有的时候不仅需要使用值还需要使用索引,ES6 为数组、Map、Set 集合内建了以下三种迭代器:

  1. entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值。
  2. keys() 返回一个遍历器对象,用来遍历所有的键名。
  3. values() 返回一个遍历器对象,用来遍历所有的键值。

以数组为例:

var colors = ["red", "green", "blue"];

for (let index of colors.keys()) {
    console.log(index);
}

// 0
// 1
// 2

for (let color of colors.values()) {
    console.log(color);
}

// red
// green
// blue

for (let item of colors.entries()) {
    console.log(item);
}

// [ 0, "red" ]
// [ 1, "green" ]
// [ 2, "blue" ]

Map 类型与数组类似,但是对于 Set 类型需要注意以下:

var colors = new Set(["red", "green", "blue"]);

for (let index of colors.keys()) {
    console.log(index);
}

// red
// green
// blue

for (let color of colors.values()) {
    console.log(color);
}

// red
// green
// blue

for (let item of colors.entries()) {
    console.log(item);
}

// [ "red", "red" ]
// [ "green", "green" ]
// [ "blue", "blue" ]

Set 类型的 keys() 和 values() 返回的是相同的迭代器,这也意味着在 Set 这种数据结构中键名与键值相同。

而且每个集合类型都有一个默认的迭代器,在 for-of 循环中,如果没有显式指定则使用默认的迭代器。数组和 Set 集合的默认迭代器是 values() 方法,Map 集合的默认迭代器是 entries() 方法。

这也就是为什么直接 for of 遍历 Set 和 Map 数据结构,会有不同的数据结构返回:

const values = new Set([1, 2, 3]);

for (let value of values) {
    console.log(value);
}

// 1
// 2
// 3
const values = new Map([["key1", "value1"], ["key2", "value2"]]);
for (let value of values) {
    console.log(value);
}

// ["key1", "value1"]
// ["key2", "value2"]

遍历 Map 数据结构的时候可以顺便结合解构赋值:

const valuess = new Map([["key1", "value1"], ["key2", "value2"]]);

for (let [key, value] of valuess) {
    console.log(key + ":" + value);
}

// key1:value1
// key2:value2

Babel 是如何编译 for of 的

我们可以在 Babel 的 Try it out 中查看编译的结果:

const colors = new Set(["red", "green", "blue"]);

for (let color of colors) {
    console.log(color);
}

对于这样一段代码,编译的结果如下:

"use strict";

var colors = new Set(["red", "green", "blue"]);

var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;

try {
    for (
        var _iterator = colors[Symbol.iterator](), _step;
        !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
        _iteratorNormalCompletion = true
    ) {
        var color = _step.value;

        console.log(color);
    }
} catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
} finally {
    try {
        if (!_iteratorNormalCompletion && _iterator.return) {
            _iterator.return();
        }
    } finally {
        if (_didIteratorError) {
            throw _iteratorError;
        }
    }
}

至少由编译的结果可以看出,使用 for of 循环的背后,还是会使用 Symbol.iterator 接口。

而这段编译的代码稍微复杂的地方有两段,一段是 for 循环这里:

for (
    var _iterator = colors[Symbol.iterator](), _step;
    !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
    _iteratorNormalCompletion = true
) {
    var color = _step.value;
    console.log(color);
}

跟标准的 for 循环写法有些差别,我们看下 for 语句的语法:

for (initialize; test; increment) statement;

initialize、test 和 increment 三个表达式之间用分号分割,它们分别负责初始化操作循环条件判断计数器变量的更新

for 语句其实就相当于:

initialize;
while (test) {
    statement;
    increment;
}

代码的逻辑为:先进行初始化,然后每次循环执行之前会执行 test 表达式,并判断表达式的结果来决定是否执行循环体,如果 test 计算结果为真值,则执行循环体中的 statement。最后,执行 increment 表达式。

而且值得注意的是,其实 for 循环中的三个表达式中任意一个都可以被忽略,不过分号还是要写的。

比如 for(;;),不过这就是一个死循环……

比如:

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

又比如:

var i = 0,
    len = colors.length;
for (; i < len; ) {
    i++;
}

然后我们再来看 Babel 编译的这个 for 循环表达式:

for (
    var _iterator = colors[Symbol.iterator](), _step;
    !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
    _iteratorNormalCompletion = true
) {
    var color = _step.value;
    console.log(color);
}

用 while 的写法相当于:

var _iterator = colors[Symbol.iterator](),
    _step;
while (!(_iteratorNormalCompletion = (_step = _iterator.next()).done)) {
    var color = _step.value;
    console.log(color);
    _iteratorNormalCompletion = true;
}

是不是就好懂了很多呢,然后你就会发现,其实 _iteratorNormalCompletion = true 这句是完全没有必要的……

另外一段稍微复杂的代码是:

try {
  ...
} catch (err) {
  ...
} finally {
  try {
    if (!_iteratorNormalCompletion && _iterator.return) {
      _iterator.return();
    }
  } finally {
    ...
  }
}

因为 _iteratorNormalCompletion = (_step = _iterator.next()).done,所以 _iteratorNormalCompletion 表示的就是是否完成了一次完整的迭代过程,如果没有正常的迭代完成,并且迭代器有 return 方法时,就会执行该方法。

而之所以这么做,就要提到迭代器的 return 方法。

引用阮一峰老师的 ECMAScript 6 入门:

遍历器对象除了具有 next 方法,还可以具有 return 方法和 throw 方法。如果你自己写遍历器对象生成函数,那么 next 方法是必须部署的,return 方法和 throw 方法是否部署是可选的。

return 方法的使用场合是,如果 for...of 循环提前退出(通常是因为出错,或者有 break 语句或 continue 语句),就会调用 return 方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return 方法。

我们可以举个例子:

function createIterator(items) {
    var i = 0;
    return {
        next: function() {
            var done = i >= items.length;
            var value = !done ? items[i++] : undefined;

            return {
                done: done,
                value: value
            };
        },
        return: function() {
            console.log("执行了 return 方法");
            return {
                value: 23333,
                done: true
            };
        }
    };
}

var colors = ["red", "green", "blue"];

var iterator = createIterator([1, 2, 3]);

colors[Symbol.iterator] = function() {
    return iterator;
};

for (let color of colors) {
    if (color == 1) break;
    console.log(color);
}
// 执行了 return 方法

不过正如你在编译后的代码中看到,仅仅是在有 return 函数的时候执行了 return 函数而已,return 函数中返回的值其实并不生效……

但是你不返回值或者返回一个基本类型的值的话,结果又会报错……

TypeError: Iterator result undefined is not an object

这是因为 return 方法必须返回一个对象,而这又是 Generator 规范决定的……

总之如果是在浏览器中使用的话,return 函数的返回值其实并不生效 T^T

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

@liaorende
Copy link

liaorende commented Sep 5, 2018

var done = i >= item.length;
笔误了吧 items.length

@zjp6049
Copy link

zjp6049 commented Nov 9, 2018

在日常代码中 有什么情况是使用for of会比较方便吗

@cobish
Copy link

cobish commented Jan 29, 2019

@zjp6049 只想要遍历值,而不需要用到 index 或 key 等的时候

const arr = ['a', 'b', 'c'];

for (let val of arr) {
  console.log(val);
}

// "a"
// "b"
// "c"

@zhangzs000
Copy link

var done = i >= item.length;

大佬,上面这个是items,小bug

@ystarlongzi
Copy link

@zjp6049 作者在开头已经提到了,for 的问题在于

..... 需要提前获取数组长度,声明索引变量等,尤其当多个循环嵌套的时候,更需要使用多个索引变量,代码的复杂度就会大大增加 .....

@fantasydai
Copy link

完整版createIterator

function createIterator(items){
  function addIterator(items){
    let i = 0
    let done = false
    return {  
      next() {
        done = i >= items.length
        return {
          value: items[i++],
          done
        }
      }
    }
  }
  let iterator = addIterator(items)
  iterator[Symbol.iterator] = ()=> addIterator(items)
  return iterator
}

@CommanderXL
Copy link

@ystarlongzi @cobish @zjp6049 我觉得 for...of 的主要运用的场景并不是作者文中开头提到的那些吧。比如你要遍历一个数组,不想使用 for 循环,数组提供的 map/forEach 等也都能完成对应的功能。for...of 只能搭配部署了 iterator 接口的值来进行使用。而不同值的遍历也有对应的遍历方法,比如要遍历对象,使用 for ... in 循环同样能完成对应的工作。

@wisetc
Copy link

wisetc commented Sep 18, 2019

博主好文。这个跟 python 语言里面的 iterator 有什么区别

@xpf888xpf
Copy link

没有区别

@l-iiii-llll-y
Copy link

babel编译中 _iteratorNormalCompletion = true是有必要的,因为每次在进行条件判断
!(_iteratorNormalCompletion = (_step = _iterator.next()).done) 的时候会对_iteratorNormalCompletion重新赋值。

例如当前_step.done = false时,_iteratorNormalCompletion也会变成false,如果没有将_iteratorNormalCompletion设为true,那么下一次循环,当_step.done = false时,判断条件会返回false,就会退出循环。

@ALEXLAY
Copy link

ALEXLAY commented Aug 11, 2020

babel编译中 _iteratorNormalCompletion = true是有必要的,因为每次在进行条件判断
!(_iteratorNormalCompletion = (_step = _iterator.next()).done) 的时候会对_iteratorNormalCompletion重新赋值。

例如当前_step.done = false时,_iteratorNormalCompletion也会变成false,如果没有将_iteratorNormalCompletion设为true,那么下一次循环,当_step.done = false时,判断条件会返回false,就会退出循环。

你认真的吗。。。。每次判断对_iteratorNormalCompletion的赋值 都会覆盖 “_iteratorNormalCompletion = true”,所以去掉是无所谓的。

@bingyuea
Copy link

深入JS讨论很多,到ES6这慢慢好像就少了。。。

@MoJiaBings
Copy link

大佬,迭代器那段代码 var done = i >= item.length; 应该是 items。

@MailKiller
Copy link

babel编译中 _iteratorNormalCompletion = true是有必要的,因为每次在进行条件判断
!(_iteratorNormalCompletion = (_step = _iterator.next()).done) 的时候会对_iteratorNormalCompletion重新赋值。

例如当前_step.done = false时,_iteratorNormalCompletion也会变成false,如果没有将_iteratorNormalCompletion设为true,那么下一次循环,当_step.done = false时,判断条件会返回false,就会退出循环。

条件语句中是赋值,并不是判断相等与否~

@Sharang-heng
Copy link

所以forEach中return无效而for可以的原因是forEach迭代器没有return方法这个原因?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests