ES6基础(四)Iterator和for...of

Iterator(遍历器) 和 for…of 循环

遍历器(Iterator)就是这样一种机制。它是一种接口,为各类不一样的数据结构提供统一的访问机制
任何数据结构只要部署 Iterator 接口,就能够完成遍历操做(即依次处理该数据结构的全部成员)
html

1、迭代器和 for…of 浅谈

1.1 传统 for 循环

先来看一段标准的 for 循环的代码:前端

var arr = [1,2,3];

for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}
// 1 2 3

注意,咱们拿到了里面的元素,但却多作了不少事:git

  • 咱们声明了 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 循环共同解决这个问题。es6

1.2 terator(迭代器)

迭代器的描述:github

  • 是为各类数据结构,提供一个统一的、简便的访问接口,是用于遍历数据结构元素的指针
  • 二是使得数据结构的成员可以按某种次序排列;
  • 三是 ES6创造的一种遍历命令 for…of 循环,Iterator 接口主要供 for…of 消费。

迭代的过程以下:web

  • 经过 Symbol.iterator 建立一个迭代器,指向当前数据结构的起始位置
  • 随后经过 next 方法进行向下迭代指向下一个位置:
    • next 方法会返回当前位置的对象,对象包含了 valuedone 两个属性;
    • value 是当前属性的值;
    • done 用于判断是否遍历结束,done 为 true 时则遍历结束;

迭代的内部逻辑应该是:数组

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) { 
  let index = 0;
  const iterator = { };
  iterator.next = function() { 
    if (index < array.length) return {  value: array[index++], done: false };
    return {  value: undefined, done: true };
  };
  return iterator;
}

1.3 什么是 for…of?

注意这里咱们仅说起了 forof 与迭代器的关系。数据结构

for…of 的描述:函数

  • for…of 语句在可迭代对象上建立一个迭代循环,调用自定义迭代钩子,并为每一个不一样属性的值执行语句——MDN
  • 一个数据结构只要部署了Symbol.iterator属性,就被视为具备 iterator 接口,就能够用for...of循环遍历它的成员。

看到这里你会发现for...of和迭代器老是在一块儿, for...of循环内部调用的是数据结构的Symbol.iterator方法。学习

举个例子:

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 属性。

JavaScript 原有的 for…in 循环,只能得到对象的键名,不能直接获取键值。ES6 提供 for…of 循环,容许遍历得到键值。

var arr = ["a", "b", "c", "d"];

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 循环读取键值

for…of 循环调用遍历器接口,数组的遍历器接口只返回具备数字索引的属性。这一点跟 for…in 循环也不同。

2、默认的 Iterator 接口

Iterator 接口的目的,就是为全部数据结构,提供了一种统一的访问机制。当使用 for…of 循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。

原生具有 Iterator 接口的数据结构以下。

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

拿数组举例:

const item = [1, 2, 3][Symbol.iterator]();
item.next();
item.next();
item.next();
// {value: 1, done: false}
// {value: 2, done: false}
// {value: 3, done: false}
// {value: undefined, done: true}

对于原生部署Iterator接口的数据结构,不用本身写遍历器生成函数,for...of 循环会自动遍历它们。除此以外,都须要本身在 Symbol.iterator 属性上面部署。

本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。

对象(Object)之因此没有默认部署 Iterator 接口,也是由于对象无法统一进行线性转换

一个对象若是要具有可被 for…of 循环调用的 Iterator 接口,就必须在 Symbol.iterator 的属性上部署遍历器生成方法(原型链上的对象具备该方法也可)。

class newiterator { 
  constructor(start, stop) { 
    this.value = start;
    this.stop = stop;
  }
  // Iterator接口 返回自己
  [Symbol.iterator]() { 
    return this;
  }

  next() { 
    if (this.value < this.stop) { 
      return {  value: this.value++, done: false };
    }
    return {  value: undefined, done: true };
  }
}

const iterator = new newiterator(0, 3);

for (let key of iterator) { 
  console.log(key);
}
// 0 1 2

上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator 属性对应一个函数,执行后返回当前对象的遍历器对象。

对于相似数组的对象(存在数值键名和 length 属性),部署 Iterator 接口,有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Iterator 接口。

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

[...document.querySelectorAll("div")]; // 能够执行了

NodeList 对象是相似数组的对象,原本就具备遍历接口,能够直接遍历。上面代码中,咱们将它的遍历接口改为数组的 Symbol.iterator 属性,能够看到没有任何影响。

注意,普通对象部署数组的 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
}

若是 Symbol.iterator 方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。

3、模拟实现的 for…of

其实模拟实现 for of 也比较简单,就是利用它与 Symbol.iterator 的关系。

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();
  }
}

4、使用 Iterator 接口的场景

有一些场合会默认调用 Iterator 接口(即 Symbol.iterator 方法),除了 for…of 循环,还有几个别的场合。

4.1 解构赋值

对数组和 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'];

4.2 扩展运算符

扩展运算符(…)也会调用默认的 Iterator 接口。

// 例一
var str = "hello";
[...str]; // ['h','e','l','l','o']

// 例二
let arr = ["b", "c"];
["a", ...arr, "d"];
// ['a', 'b', 'c', 'd']

上面代码的扩展运算符内部就调用 Iterator 接口。

实际上,这提供了一种简便机制,能够将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就能够对它使用扩展运算符,将其转为数组。

4.3 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.4 其余场合

因为数组的遍历会调用遍历器接口,因此任何接受数组做为参数的场合,其实都调用了遍历器接口。下面是一些例子。

  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(好比 new Map([[‘a’,1],[‘b’,2]]))
  • Promise.all()
  • Promise.race()

5、Iterator 接口与 Generator 函数

Symbol.iterator()方法的最简单实现,仍是使用 ES6 新提出的 Generator 函数。

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 命令给出每一步的返回值便可。

6、遍历器对象的 return(),throw()

遍历器对象除了具备 next()方法,还能够具备 return()方法和 throw()方法。若是你本身写遍历器对象生成函数,那么 next()方法是必须部署的,return()方法和 throw()方法是否部署是可选的。

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()方法关闭文件以后,再抛出错误。

参考

写在最后

JavaScript 系列:

  1. 《JavaScript 内功进阶系列》(已完结)
  2. 《JavaScript 专项系列》(持续更新)
  3. 《ES6 基础系列》(持续更新)

关于我

  • 花名:余光(沉迷 JS,虚心学习中)
  • WX:j565017805

其余沉淀

这是文章所在 GitHub 仓库的传送门,您点的 star,就是对我最大的鼓励 ~

相关文章
相关标签/搜索