分享

关于遍历的那些事儿

 小仙女本仙人 2022-11-29 发布于北京

一、传统遍历

所谓传统遍历,是区别于 ES6 遍历器的遍历

1. 遍历数组

1.1 for, while

遍历一个数组,可以使用 for、while 循环。

1.2 实例的迭代方法

又或者调用 ES5 给 Array 原型添加的 5 个迭代方法。这 5 个迭代方法接受两个参数:

  1. 在全部元素运行的迭代函数,这个函数可以接受三个参数:
    • 元素的值
    • 元素的下标
    • 数组本身
  2. 运行在指定函数的 this(可选)

这 5 个迭代方法具体功能不太一致:

  1. every:如果迭代函数都返回 true,则返回 true
  2. some:如果迭代函数有一个返回 true,则返回 true
  3. filter:返回迭代函数返回 true 的元素组成的子数组
  4. forEach:单纯执行迭代函数,没有返回值
  5. map:返回迭代函数返回值组成的数组

注意,上述 5 个迭代方法都不会改变原数组本身

1.3 实例的归并方法

ES5 还给给 Array 原型新增两个归并方法:reduce 与 reduceRight。这两个函数会在数组全部元素上迭代执行一个函数,并构建一个最终返回的值。这两个方法接受两个参数:

  1. 迭代函数,它有四个参数:
    • 前一个元素的迭代函数返回值
    • 元素值
    • 元素下标
    • 数组对象
  2. 作为归并基础的初始值

reduce 是从数组首迭代到数组尾,reduceRight 相反。它们的一个典型应用场景是用来求数组和

let arr1 = [1, 2, 3];
let res = arr1.reduce((pre, cur) => pre+cur, 0);
console.log(res); // 6
1.4 缺点:
  1. for、while 循环:写法麻烦
  2. 迭代、归并方法:无法使用 break、continue 以及外部函数的 return 去中止循环

2. 遍历对象

2.1 枚举性

遍历对象的思路比较统一,就是遍历键名,然后再用键名去访问键值。而关于键名的遍历,需要提到一个概念:键名是否可以被枚举

我们都知道,一个对象上的每一个属性都有一个描述对象,用来描述这个属性的一下行为,比如读、写、是否可删除、是否可枚举等。

属性描述对象有一个属性 enumerable,它是一个布尔值,表示属性是否可枚举

// 定义一个函数,访问 obj 上 key 属性的可枚举性 enumerable
let getEnmerable = (obj, key)=>{
	return Object.getOwnPropertyDescriptor(obj, key).enumerable;
}
// 一个对象的自定义属性一般都是可枚举的
getEnmerable({a: 1}, 'a'); // true

// 数组本质也是一个对象
let arr = [1, 2, 3];
getEnmerable(arr, 0); // true,说明数组的'0’属性是枚举的
getEnmerable(arr, 'length'); // false,说明数组的'length’属性是不可枚举的
console.log(Object.keys(arr)) // ["0", "1", "2"]
2.2 遍历对象键名的方法

遍历对象键名的方法,常用的有 5 种,它们遍历的属性范围有所不同:

  1. for...in:自身及其原型上的可枚举的、非 Symbol 属性
  2. Object.keys (obj):自身的可枚举的、非 Symbol 属性
  3. Object.getOwnPropertyNames (obj):自身的非 Symbol 属性,不管是否可以枚举
  4. Object.getOwnPropertySymbols (obj):自身的 Symbol 属性,不管是否可以枚举
  5. Reflect.ownKeys (obj):自身的全部属性,不管是否可以枚举已经是否是 Symbol

只有 for...in 会去遍历实例原型,其他的遍历方法都不会去遍历实例原型。推荐使用 Object.keys,它能适用我们正常开发的大部分场景。

2.3 遍历顺序
  1. 首先,遍历属性名为数值的属性,按数字大小升序
  2. 再次,遍历属性名为字符串的属性,按生成时间先后升序
  3. 最后,遍历属性名为 Symbol 属性,按生成时间先后升序

二、遍历器

1. 什么是遍历器

介绍完上面的遍历后,有没有觉得,api 一大堆又不通用,记起来很麻烦。是的,这就是为什么 ES6 要提出遍历器的原因——提供一种统一的接口机制来处理所有不同的数据结构

对应着遍历器,ES6 提供了一种新的遍历机制:for...of,专门用来遍历遍历器和有遍历器接口的数据结构。

那,怎么得到遍历器呢?ES6 给一些数据结构引入了 Symbol.iterator 这个唯一属性,这个属性指向一个方法,这个方法可以用来生成对应数据结构的遍历器对象。

拿到这个遍历器对象之后,我们可以不断地调用遍历器对象的 next 方法来对数据结构进行遍历。for...of 遍历的本质也是不断调用数据结构遍历器对象的 next 方法

看一个例子:

// 定义一个简单的数组
let arr = [1, 2, 3];

// 获取遍历器对象 
let iter = arr[Symbol.iterator] (); 

// 调用遍历器对象上的next方法,开始遍历
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: 3, done: false}
console.log(iter.next()); // 遍历完毕 {value: undefined, done: true}

// for...of 本质就是调用数据集合的遍历器对象上的 next 方法去进行遍历
for(let index of arr) {
	console.log(index) // 1 2 3
}

原生具备遍历器接口的数据结构有:

  1. Array
  2. Map
  3. Set
  4. String
  5. 函数的 arguments 对象
  6. NodeList 对象
  7. TypedArray

ps:如上文,纯对象是没有遍历器接口的

拥有了遍历器接口,我们就可以统一使用 for...of 来遍历上诉的这些数据结构,而不用死脑筋地去背每种数据结构自己固定的 api 了。

2. 数组的遍历器

for...of 已经可以遍历数组得到数组每一个元素的值了,但是如果你有特殊需求,除了数组元素值以外还需要数组的下标,又或者你只需要数组的下标……ES6 给数组原型上新添加了几个方法,可以满足你的需求:

  1. arr.entries():生成遍历结果为 [键名, 键值] 的遍历器
  2. arr.keys():生成遍历结果为 键名 的遍历器
  3. arr.values():生成遍历结果为 键值 的遍历器

调用上述 api 生成特定的遍历器后,我们可以再用 for...of 遍历这个遍历器,就可以得到我们特定的遍历需求。

值得一提的是,调用这些 api 后,我们用 for...of 遍历的是一个遍历器,而如果我们直接 for...of 去遍历数组 arr 本身,其实数组还是会调用 arr.values() 来生成一个遍历器给 for...of 去遍历。

所以用 for...of 遍历 arr.values() 和遍历数组 arr 本身是一样的,最终 for...of 遍历的都是遍历器

arr.entries() 和解构赋值一起使用,食用口味更佳。

let arr = [23,4,45,4];
for (let [i, num] of arr.entries()) {
	console.log(i, num);
}

3. 对象的遍历器

一开始是不是很好奇,为什么对象没有遍历器接口?因为对象存在的意义不是让你来遍历的,如果你需要一个可以遍历的键值对的话,可以用 Map 结构,它是拥有遍历器接口的

当然,如果你硬要遍历对象的话,也可以。

和数组类似,也有几个特殊的方法去给对象生成特定的遍历器,不同的是,这些方法不在对象原型上,而是在 Object 构造函数上:

  1. Object.entries(obj):生成遍历结果为 [键名, 键值] 的遍历器
  2. Object.keys(obj):生成遍历结果为 键名 的遍历器
  3. Object.values(obj):生成遍历结果为 键值 的遍历器

老样子,配合解构赋值才是遍历器遍历最好的使用方法:

let obj = {
	b: 1,
	0: 2,
	a: 3,
}
for(let [key, value] of Object.entries(obj)) {
	console.log(`${key}:${value}`);
	// 0: 2
	// b: 1
	// a: 3
}

4. Set、Map 的遍历器

ES6 新增了两类有趣的数据结构:Set、Map,它们也拥有遍历器接口。要遍历它们可以通过生成遍历器(for...of 遍历的本质也是生成遍历器),所以它们的原型上也拥有这三宝:

  1. entries():生成遍历结果为 [键名, 键值] 的遍历器
  2. keys():生成遍历结果为 键名 的遍历器
  3. values():生成遍历结果为 键值 的遍历器
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
for (let [key, num] of map) {
	console.log(key, num);
}

除了通过生成遍历器的去遍历以外,其实 Set、Map 原型上也有一个不借用遍历器遍历的方法,就是 forEach

另外,WeakSet 和 WeakMap 是不支持遍历的,因为它们对于对象的引用都是弱引用。

三、总结

Set 是一个类 Array 的结构,但是我们可以看到,ES6 对于 Array 原型上的很多 api 都没有再赋给 Set (比如 reduce、map 等),这其实也反映出 ES6 提出的遍历器的目的——提出一种统一的接口机制来遍历全部的数据结构,淘汰各种数据结构自身原本过度泛滥的遍历 api。包括各种数据结构上生成遍历器的方法也都是同名的 entries、keys、values ,也体现出这一点。

所以,以后我们编程的时候,尽量保持一个习惯,能用遍历器机制去遍历的,都尽量使用遍历器机制遍历,保持一个统一良好的遍历写法。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多