本文主要通过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对象] = 对象A的Symbol属性的定义 // 可以是字符串,也可以是方法
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 为什么更适合遍历对象?
- for in 遍历的是对象属性,要手动获取属性值。如果用for in遍历数组,数组中的每个元素的索引被视为属性名称,拿到的是每个元素索引。
- 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 in | for of | |
---|---|---|
特性 | ES5 | ES6 |
遍历方式 | 按照对象的key,不仅遍历数字键名,还会遍历手动添加的其它键,甚至包括原型链上的键 | 按照对象的value,对于普通对象,没有部署原生的 iterator 接口,直接使用 for...of 会报错 |
能否遍历出 Symbol | 不能 | 不能 |
主要用途 | 遍历对象 | 遍历数组 |