【JS】for in和for of的前世今生,从Symbol和Iterator讲起

952 阅读5分钟

本文主要通过Symbol和Iterator层层递进,对for of 和 for in进行了解释和说明,Symbol和Iterator基础好的同学,可以直接跳到第三节。

一、Symbol简述

1. 概要:

  • Symbol 值通过Symbol函数生成。
  • 凡是属性名属于 Symbol 类型,就都是独一无二的。
  • 对象的属性名可以有两种类型,一种是字符串,另一种就是 Symbol类型

2. Symbol 作为对象属性名

  • 举例:
	let mySymbol = Symbol();
	let a = {};
	a[mySymbol] = 'Hello!';
  • 注意:
    • 关于创建Symbol 对象,括号里面仅仅是对Symbol对象的备注,仅此而已。 使用Symbol 对象description属性,可以获得备注
    • Symbol 值作为对象属性名时,不能用点运算符。必须放在中括号里面。
	let mySymbol = Symbol('对Symbol对象的备注');
	对象A[Symbol对象] = 对象ASymbol属性的定义   // 可以是字符串,也可以是方法

3. 简单应用场景:计算图形的面积

  • 我们不需要关注shapeType.triangle和shapeType.square的具体值是什么,只需要关注它是独一无二的。在调用getArea的时候,通过第一个参数,确定要计算的是哪个类型图形的面积就可以了。
	const shapeType = {
	  triangle: Symbol(),   // 三角形
	  square: Symbol()   // 方形
	};
	
	function getArea(shape, options) {
	  let area = 0;
	  switch (shape) {
	    case shapeType.triangle:
	      area = 0.5 * options.width * options.height;
	      break;
	     case shapeType.square:
	      area = options.width * options.height;
	      break;
	  }
	  return area;
	}
	
	getArea(shapeType.triangle, { width: 100, height: 100 });

4. Symbol的遍历

  • for in遍历对象的Symbol属性:无效
  • 获得一个对象Symbol属性的键名
    • Object.getOwnPropertySymbols(obj)
    • Reflect.ownKeys(obj)

5. 重点来了:内置Symbol值(11个)

  • Symbol.hasInstance 对象的Symbol.hasInstance属性,指向一个内部方法,检测对象的继承信息。 其余的内置Symbol值,用法也类似,不在列举。
	class MyClass {
	  [Symbol.hasInstance](foo) {
	    return foo instanceof Array;
	  }
	}
	
	[1, 2, 3] instanceof new MyClass()   //true
  • Symbol.iterator 在下面Iterator中讲解

二、Iterator(迭代器)简述

1. 定义和作用

  • 迭代器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。
  • 遍历机制:
    • (1)创建一个指针对象,指向当前数据结构的起始位置。迭代器对象本质上,就指针对象

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

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

    • (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

  • 作用有三个:
    • 一是为各种数据结构,提供一个统一的、简便的访问接口;(方便数据输出)
    • 二是使得数据结构的成员能够按某种次序排列;(按照某种顺序输出)
    • 三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。(for of 封装了Iterator 的一些操作,遍历起来更便捷)

2. Iterator接口简介

广义可遍历:

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

狭义可遍历:

  • ES6 中,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。
  • Symbol.iterator属性本身是一个函数,用于遍历当前数据结构。
	手写一个简单的迭代器:
	const obj = {
	  [Symbol.iterator] : function () {
	    return {
	      next: function () {
	        return {value: 1,done: true};
	      }
	    };
	  }
	};
	let iter = obj[Symbol.iterator]()   // 返回了迭代器对象
	iter.next()  // {value: 1,done: true}

3. 原生具备 Iterator 接口的数据结构:

  • Array 、Map 、Set 、String 、TypedArray (ES2017新增)、函数的 arguments 对象 、NodeList 对象
  • 什么是原生具备 Iterator 接口的数据结构? 就是不用重写其Symbol.iterator属性,直接通过Symbol.iterator属性就能获得迭代器对象,进而通过next()对其内部的值进行遍历。
	这里以Array为例,讲述一下原生具备Iterator接口的数据结构遍历:
	
	let arr = ['a', 'b', 'c'];
	
	 // 通过arr原生Symbol.iterator属性,获取到了包含next()方法的迭代器对象
	let iter = arr[Symbol.iterator]();
	
	 // 通过iter对象的next()方法对arr进行遍历
	iter.next() // { value: 'a', done: false }
	iter.next() // { value: 'b', done: false }
	iter.next() // { value: 'c', done: false }
	iter.next() // { value: undefined, done: true }
  • 用ES5描述数组的Iterator(其实JS Array的迭代器就是这样实现的,只不过 ES6 的 API 给我们封装好了,变成原生的了,拿来即用)
	let arr  = [1,3];
	function myIterator(arr){
	    var index=0;
	    return {
	        next:function () {
	            return index<arr.length ? {value:arr[index++],done:false} : {value:undefined,done:true}
	        }
	    }
	}
	let iter = myIterator(arr);
	console.log(iter.next());  //{ value: 1, done: false }
	console.log(iter.next());  //{ value: 3, done: false }
	console.log(iter.next());  //{ value: undefined, done: true }
  • 那么,原生具备 Iterator 接口的数据结构有什么特点呢,如何辨别原生具备 Iterator 接口的数据结构呢? 很简单,看他们的原型对象上,是否具有[Symbol.iterator]属性,这里以Array为例,Array默认带有[Symbol.iterator]属性,所以它是原生支持迭代的。 在这里插入图片描述 同样,Map原型默认也是有[Symbol.iterator]属性的。 在这里插入图片描述 再来验证默认不支持迭代器进行遍历的Object和Number Object原型上没有[Symbol.iterator]属性,所以其默认不可遍历 在这里插入图片描述 Number原型上也没有[Symbol.iterator]属性,所以其默认也不可遍历 在这里插入图片描述

4. 给一个对象设置多个next(迭代器)

	const obj = {
	    [Symbol.iterator] : function () {
	        return this
	    },
	    next: function () {
	        return {value: 1,done: true};
	    },
	    next2: function () {
	        return {value: 2,done: true};
	    },
	    next3: function () {
	        return {value: 3,done: true};
	    }
	};
	let iter = obj[Symbol.iterator]()   
	console.log(iter)
  • 输出的迭代器对象是这样的。 在这里插入图片描述
  • 给一个对象设置多个迭代器有什么用处呢? 可以通过设置不同的遍历函数,把一个对象按照不同的需求进行遍历。

三、言归正传:说回for of

1.可是 for of 和 Iterator 有什么关系呢?

	let iterable = {
	    0: 'a',
	    1: 'b',
	    2: 'c',
	    length: 3,
	    [Symbol.iterator]: Array.prototype[Symbol.iterator]   //用遍历数组的Symbol.iterator去遍历该伪数组对象
	};
  • 如果我们老老实实按照最原始的next()方式遍历:
	let iter = iterable[Symbol.iterator]();
	let temp = null;
	while (iter){
	    temp = iter.next();
	    if (temp.done){
	        break
	    }else {
	        console.log(temp.value)
	    }
	}

原始的next()方式控制台输出 在这里插入图片描述

  • 如果用for of遍历:
	for (let temp of iterable){
	    console.log(temp)
	}

for of控制台输出 在这里插入图片描述

  • 咦,一样的!!!其实说白了,for of 就是迭代器的语法糖

2. 再来,对比如下两段代码:

  • 第一段代码(类数组):
	let iterable = {
	  0: 'a',
	  1: 'b',
	  2: 'c',
	  length: 3,
	  [Symbol.iterator]: Array.prototype[Symbol.iterator]  //用遍历数组的Symbol.iterator去遍历该对象
	};
	for (let item of iterable) {
	  console.log(item); // 'a', 'b', 'c'
	}
  • 第二段代码(索引不是数字的数组)
	let iterable = {
	  a: 'a',
	  b: 'b',
	  c: 'c',
	  length: 3,
	  [Symbol.iterator]: Array.prototype[Symbol.iterator]  //用遍历数组的Symbol.iterator去遍历该对象
	};
	for (let item of iterable) {
	  console.log(item); // undefined, undefined, undefined
	}
  • 同样都是用for of遍历,为什么第二段代码全是undefined呢?
    • 还记得我们最开始用ES5描述数组的Iterator的例子吗(见下)?
    • 这是因为数组默认是根据数字索引来进行遍历的,第二段代码很显然不是通过数字索引遍历,所以当然是undefined了。
	function myIterator(arr){
	    var index=0;
	    return {
	        next:function () {
	            return index<arr.length ? {value:arr[index++],done:false} : {value:undefined,done:true}
	        }
	    }
	}

3. 重点来了:为什么 for of 更适合遍历数组,而不是遍历对象?

祭出代码:

	[][Symbol.iterator] // [Function: values]
	{}[Symbol.iterator] // undefined
  • for..of 进行遍历,会向被访问对象请求一个迭代器对象,通过迭代器的 next() 方法遍历所有返回值。
  • 综上,for of遍历数组时,默认按照数组的 index索引 进行遍历。而当遇到对象的时候,对象是没有默认的迭代器的,需要重写迭代器才能遍历;再就是对象的属性名称往往是字符串,并不符合for of按照索引遍历的要求

如果想用for of遍历对象呢?

  • 法一:重写对象的[Symbol.iterator],不推荐,太费事
  • 法二:Object.keys()

Object.keys() 参数:要返回其枚举自身属性的对象 返回值:一个表示给定对象的所有可枚举属性的字符串数组

	let iterable = {
	    a: '111',
	    b: '222',
	    c: '333',
	};
	for (let temp of Object.keys(iterable)){
	    console.log(iterable[temp])  // 111,222,333
	}

4. 再来说一说:for in 为什么更适合遍历对象?

  1. for in 遍历的是对象属性,要手动获取属性值。如果用for in遍历数组,数组中的每个元素的索引被视为属性名称,拿到的是每个元素索引。
  2. for-in循环会枚举对象原型链上的可枚举属性:
	Object.prototype.objCustom = function () {}; 
	Array.prototype.arrCustom = function () {};
	
	let iterable = [3, 5, 7];
	iterable.foo = "hello";
	
	for (let i in iterable) {
	  console.log(i); //  0, 1, 2, "foo", "arrCustom", "objCustom"
	}

5. 最后,一道经典面试题:for of和for in的区别?

for infor of
特性ES5ES6
遍历方式按照对象的key,不仅遍历数字键名,还会遍历手动添加的其它键,甚至包括原型链上的键按照对象的value,对于普通对象,没有部署原生的 iterator 接口,直接使用 for...of 会报错
能否遍历出 Symbol不能不能
主要用途遍历对象遍历数组