ES6 Iterator 和 for...of 循环

Iterator

概念

遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

作用

1. 为各种数据结构,提供一个统一的、简便的访问接口;

2. 使得数据结构的成员能够按某种次序排列;

3.  ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费 

遍历过程

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的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};
    }
  };
}

可以看出next方法返回一个对象,表示当前数据成员的信息。这个对象具有valuedone两个属性,value属性返回当前位置的成员,done属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用next方法。

for of

上面说了 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费

我们来试下:

var iterator = makeIterator([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)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。

var obj = {
    value: 1,
    [Symbol.iterator]: function() {
        return makeIterator([1, 2, 3]);
    }
}

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

// 1
// 2
// 3

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

默认可遍历对象

let arr = ['a', 'b', 'c'];
for (let val of arr) {
    console.log(val);
}

// 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 }

对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

对象(Object)为什么没有默认部署 Iterator 接口?

因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。

一个对象如果要具备可被for...of循环调用的 Iterator 接口,就必须在Symbol.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 };
    }
    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
}

模拟实现 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();
    }
}

entries、keys、values

ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。

  • entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。
  • keys() 返回一个遍历器对象,用来遍历所有的键名。
  • values() 返回一个遍历器对象,用来遍历所有的键值。

数组:

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

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

// [0, 'a']
// [1, 'b']
// [2, 'c']


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

// 0
// 1
// 2

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

// a
// b
// c

 Map 类型与数组类似

let map = new Map()

map.set(0, 'a');
map.set(1, 'b');
map.set(2, 'c');


for (let pair of map.entries()) {
  console.log(pair);
}

// [0, 'a']
// [1, 'b']
// [2, 'c']


for (let key of map.keys()) {
  console.log(key);
}

// 0
// 1
// 2

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

// a
// b
// c

Set

let set = new Set(['a', 'b', 'c']);

for (let pair of set.entries()) {
  console.log(pair);
}

// ['a', 'a']
// ['b', 'b']
// ['c', 'c']


for (let key of set.keys()) {
  console.log(key);
}

// a
// b
// c

for (let value of set.values()) {
  console.log(value);
}
// a
// b
// c

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

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

Babel 是如何编译 for of 的

let set = new Set(['a', 'b', 'c']);

for (let s of set) {
  console.log(s);
}

// a
// b
// c

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

"use strict";
function _createForOfIteratorHelper(o, allowArrayLike) {
  // 判断是否有Symbol.iterator属性
  var it =
    (typeof Symbol !== "undefined" && o[Symbol.iterator]) || o["@@iterator"];
  if (!it) {
    if (
      Array.isArray(o) ||
      (it = _unsupportedIterableToArray(o)) ||
      (allowArrayLike && o && typeof o.length === "number")
    ) {
      if (it) o = it;
      var i = 0;
      var F = function F() {};
      return {
        s: F,
        n: function n() {
          if (i >= o.length)
            return {
              done: true,
            };
          return {
            done: false,
            value: o[i++],
          };
        },
        e: function e(_e) {
          throw _e;
        },
        f: F,
      };
    }
    throw new TypeError(
      "Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
    );
  }
  var normalCompletion = true,
    didErr = false,
    err;
  return {
    s: function s() {
      it = it.call(o);
    },
    n: function n() {
      var step = it.next();
      normalCompletion = step.done;
      return step;
    },
    e: function e(_e2) {
      didErr = true;
      err = _e2;
    },
    f: function f() {
      try {
        // 没有正常的遍历完成,并且遍历器有 return 方法时,就会执行该方法
        if (!normalCompletion && it["return"] != null) it["return"]();
      } finally {
        if (didErr) throw err;
      }
    },
  };
}

// 转成数组
function _unsupportedIterableToArray(o, minLen) {
  if (!o) return;
  if (typeof o === "string") return _arrayLikeToArray(o, minLen);
  var n = Object.prototype.toString.call(o).slice(8, -1);
  if (n === "Object" && o.constructor) n = o.constructor.name;
  if (n === "Map" || n === "Set") return Array.from(o);
  if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))
    return _arrayLikeToArray(o, minLen);
}

function _arrayLikeToArray(arr, len) {
  if (len == null || len > arr.length) len = arr.length;
  for (var i = 0, arr2 = new Array(len); i < len; i++) {
    arr2[i] = arr[i];
  }
  return arr2;
}

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

var _iterator = _createForOfIteratorHelper(colors),
  _step;

try {
  for (_iterator.s(); !(_step = _iterator.n()).done; ) {
    var color = _step.value;
    console.log(color);
  }
} catch (err) {
  _iterator.e(err);
} finally {
  _iterator.f();
}

可以看出使用 for of 循环的背后,还是会使用 Symbol.iterator 接口

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

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

return()方法必须返回一个对象,这是 Generator 语法决定的。

throw()方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。

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 1;
        }
    };
}

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

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

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

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

 

 可以看出return()方法必须返回一个对象,不然会报错

参考资料:

ES6 系列之迭代器与 for of · Issue #90 · mqyqingfeng/Blog · GitHub

阮一峰- ES6-Iterator 和 for...of 循环

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值