一、for in(ES5)以任意顺序迭代对象的可枚举属性
1、什么叫可枚举属性
- 在属性对象中,
enumerable值为true时,这个属性就是可枚举的
const obj = { a: 'a', b: 'b' }
Object.prototype.c = 'c'
Object.defineProperty(obj, 'd', {
value: 'd'
})
通过Object.getOwnPropertyDescriptors(obj)可以获取到obj自身属性的描述对象
由此可见,a、b可以被for in遍历,而d不可以被fon in遍历
- 如果我们打印数组的属性描述对象:
Object.getOwnPropertyDescriptors(['a', 'b'])
由此可见,数组也是可以被for in遍历的,数组中的length属性不会被for in遍历到。同理,字符串也可以被for in遍历
另外,symbol键不会被for..in遍历到。
通过实例方法propertyIsEnumerable也可以知道该属性是否可枚举:
console.log(obj.propertyIsEnumerable('a')) // true
console.log(obj.propertyIsEnumerable('c')) // false
console.log(obj.propertyIsEnumerable('d')) // false
2、遍历的范围
for in会拿到原型上的数据
const obj = {
name: 'xx',
age: 10
}
Object.prototype.aa = 'aa'
for (const key in obj) {
console.log(key, obj[key])
}
若希望过滤掉原型上的属性:(为什么不用obj.hasOwnProperty(key)?)
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
console.log(key, obj[key])
}
}
其实,我最希望你尽量不要使用for...in,因为它会查找原型,效率肯定比不过for...of:
for (const key of Object.keys(obj)) {
console.log(key, obj[key])
}
3、得到的结果:for in遍历的是键名
const obj = {
name: 'xx',
age: 10
}
for (const key in obj) {
console.log(key)
}
/*
name
age
*/
二、for of(ES6)遍历可迭代数据
1、什么叫可迭代数据
- 在ES6中,具有
Symbol.iterator属性,它是一个函数,返回一个对象,调用对象中的next方法能得到目标的每一项
const arr = ['a', 'b']
const iterator = arr[Symbol.iterator]()
console.log(iterator.next()) // {value: 'a', done: false}
console.log(iterator.next()) // {value: 'b', done: false}
console.log(iterator.next()) // {value: undefined, done: true}
依次类推,String、Map、Set、TypedArray、arguments、NodeList都可以使用for of遍历
迭代器的实现: Iteartor迭代器规范
var arr = ['a', 'b', 'c']
arr[Symbol.iterator] = function () {
var _this = this
var index = 0
return {
next: function () {
if (index === _this.length ) {
return { value: undefined, done: true }
}
return { value: _this[index++], done: false }
}
}
}
for(const item of arr){
console.log(item)
}
遍历一个数组时,实际上内部走的是迭代器方法
2、遍历的范围
for of只能遍历自身的属性,不能遍历到原型上的属性
const arr = ['a', 'b']
Array.prototype.aa = 'aa'
for (const item of arr) {
console.log(item)
}
如果在迭代器中加上Object.keys(this.__proto__).forEach(item=>this.push(this.__proto__[item]))可以模拟能拿到原型上的数据
3、得到的结果:for of遍历的是键值
const arr = ['a', 'b', 'c']
for (const item of arr) {
console.log(item)
}
/*
a
b
c
*/
for of也可以拿到下标,和entries()结合使用:
const arr = ['a', 'b', 'c']
for (const [index, item] of arr.entries()) {
console.log(index, item)
}
/*
0 'a' -> 这里的0是数字
1 'b'
2 'c'
*/
三、for in和for of的使用场景
- for in一般情况下用来遍历对象,也可以遍历数组,但是在遍历数组时会有些问题
- for of一般情况下用来遍历数组,不可以遍历对象,因为对象没有迭代器
- 使用
[Symbol.iterator]检查某个数据类型有没有迭代器,数组和字符串可以使用for of遍历
- 使用
console.log({}[Symbol.iterator]) // undefined
console.log(new Number(123)[Symbol.iterator]) // undefined
console.log(new Boolean()[Symbol.iterator]) // undefined
console.log([][Symbol.iterator]) // ƒ values() { [native code] }
console.log(''[Symbol.iterator]) // ƒ [Symbol.iterator]() { [native code] }
四、为什么不推荐使用for in来遍历数组?
1、for in遍历数组时,得到的下标是字符串类型,容易疏忽掉
const arr = ['a', 'b', 'c']
for (const index in arr) {
console.log(index, typeof index)
}
/*
0 string
1 string
2 string
*/
2、for in会遍历到原型上的属性,在大多数场景下并不需要遍历原型上的属性,如果要过滤掉原型上属性还要做一层处理
const arr = ['a', 'b', 'c']
Array.prototype.aa = 'aa'
for (const index in arr) {
console.log(index, arr[index])
}
/*
0 a
1 b
2 c
aa aa
*/
从性能的角度说,for in的性能比较差
3、for in遍历的顺序
var obj = {
100: 100,
50: 50,
1: 1,
0: 0,
name: 'xx',
'-0': '-0',
'-100': '-100',
3.14: 3.14
}
obj[10] = 10
for (const key in obj) {
console.log(key, obj[key])
}
打印结果:
为什么?
因为for in遍历对象时,会首先找到对象中的非负整数属性,将这一部分的属性按照升序遍历,再找到其他的属性,按照创建时的顺序遍历出来
如果想按照创建时的顺序遍历出来,必须要避开属性名是非负整数
五、如果说非要使用for of来遍历对象呢?(仅做了解)
1、对象有length属性
因为obj内部没有实现迭代器,所以使用for of来遍历一个没有迭代器的对象会直接报错
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
}
// Uncaught TypeError: obj is not iterable
for (const key of obj) {
console.log(key)
}
如果给obj对象添加一个迭代器,或者给Object的原型上添加一个迭代器,那么就可以使用for of来遍历了
obj[Symbol.iterator] = Array.prototype[Symbol.iterator]
// Object.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]
仅仅是将数组的迭代器拿过来给对象使用,那么for...of遍历的对象需要有个length属性,此时的对象是一个类数组
2、对象没有length属性
而对一个对象实现迭代器,可以手写:
const obj = {
name: 'xx',
0: 0,
[Symbol()]: 'symbol属性值'
}
Object.prototype[Symbol.iterator] = function () {
var _this = this
var index = 0
var keys = Reflect.ownKeys(this) // ES6:Reflect.ownKeys可以获取到symbol类型的键,Object.keys只能拿到非symbol类型的键
return {
next: function () {
if (index > keys.length - 1) return { done: true, value: undefined }
return { done: false, value: _this[keys[index++]] }
}
}
}
for (const item of obj) {
console.log(item)
}
/*
0
xx
symbol属性值
*/
六、for await of异步迭代器
for...of是同步打印,如:
const asyncFn = (timeout) => {
return new Promise((res) => {
setTimeout(() => {
res(timeout)
}, timeout)
})
}
const test = () => {
const arr = [asyncFn(2000), asyncFn(1000), asyncFn(3000)]
for (const item of arr) {
item.then(console.log)
}
}
test()
/*
每隔1秒,顺序打印1000 2000 3000
*/
有时,我们希望循环可以等待每个promise对象的状态变为resloved才进入下一步,此时就需要用到for await of循环:
const test = async () => {
const arr = [asyncFn(2000), asyncFn(1000), asyncFn(3000)]
for await (const item of arr) {
console.log(Date.now(), item)
}
}
/*
1670999130324 2000
1670999130325 1000
1670999131317 3000
*/
打印顺序和数组写入顺序一致,和Promise.all有点像