js ES6 Iterator 遍历器与 for、for...of、for await...of、for...in 和 forEach 循环语句

目录

一、Iterator

1、遍历器(Iterator)

2、Iterator 的方法

(1)、next() 方法

(2)、return() 方法

(3)、throw() 方法

3、Iterator 接口的部署(★★★★★)

(1)、默认部署

(2)、原生具备 Iterator 接口的数据结构

4、调用 Iterator 接口的场合

(1)、Iterator 接口与解构赋值

(2)、Iterator 接口与扩展运算符

(3)、Iterator 接口与 yield*

(4)、Iterator 接口与 Generator 函数

二、for...of 循环

1、for...of 循环的概况

2、for...of循环的应用

(1)、遍历可迭代的数据结构

(2)、遍历不可迭代的数据结构

(3)、遍历类数组对象

三、for await...of 循环

1、for await...of 循环的使用

2、for await...of 循环的错误捕获

3、for await...of 循环与异步 Generator 函数

四、for、for...of、for...in 和 forEach 循环的区别

五、for 和 forEach 循环哪个更快?为什么?

六、遍历对象与数组

1、遍历对象

(1)、for … in

(2)、Object.keys()

(3)、Reflect.ownKeys()

(4)、Object.getOwnPropertyNames()

2、遍历数组

(1)、for

(2)、forEach

(3)、for ... in

(4)、for ... of

七、属性的可枚举性及其迭代方法


一、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 循环的区别”。

七、属性的可枚举性及其迭代方法

请戳这里:属性的可枚举性及其迭代方法_weixin79893765432...的博客-CSDN博客

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值