0


属性的遍历

一,几种遍历的形式

1,for - in 循环

for-in最常用的一种循环,可以用来遍历数组,字符串,Map,Set,甚至是对象:

const obj = {
  name: 'zhang san',
  age: 28,
}
for (const key in obj) {
  console.log(key)
}
//name
//age

for-in是一种另类的循环遍历,因为他与迭代协议无关,只是一种遍历键的行为。你可以在任意对象(甚至是Object这样的不可迭代对象)中使用。

2,for - of 循环

for-of也是一种常见的循环,他与for-in的不同点在于:for-of遍历的是‘值’,而for-in遍历的是‘键’;for-of会调用迭代接口,因此是一种可迭代对象的专属迭代语句,而for-in适合任何有键的对象。

class MyArray extends Array {
  *[Symbol.iterator]() {
    for (const item of this) {
      yield item
    }
  }
}

用上述代码去改写迭代接口会报错!因为for-of会在内部调用迭代接口,从而导致循环调用,造成爆栈的问题。但是用下述方法是可行的:

class MyArray extends Array {
  *[Symbol.iterator]() {
    for (const i in this) {
      yield this[i]
    }
  }
}

3,展开运算符和其他形式的遍历行为

JS原生语言中,其他会自动调用迭代接口的语句还有:展开运算符(...),Array.from(),new Map(),new Set(),Promise.all()等。因此,在迭代接口中使用这些语句,同样需要谨慎。

class MyArray extends Array {
  *[Symbol.iterator]() {
    new Set(this) //会报错!
  }
}

4,全局对象的内置遍历行为

诸如数组一类的可迭代对象,大多都存在着自身的遍历行为接口。如:

console.log(Set.prototype.forEach) // ƒ forEach() { [native code] }
console.log(Map.prototype.forEach) // ƒ forEach() { [native code] }

console.log(Array.prototype.forEach) // ƒ forEach() { [native code] }
console.log(Array.prototype.filter) // ƒ filter() { [native code] }
console.log(Array.prototype.map) // ƒ map() { [native code] }
console.log(Array.prototype.flatMap) // ƒ flatMap() { [native code] }
console.log(Array.prototype.some) // ƒ some() { [native code] }
console.log(Array.prototype.every) // ƒ every() { [native code] }
console.log(Array.prototype.find) // ƒ find() { [native code] }
console.log(Array.prototype.findIndex) // ƒ findIndex() { [native code] }
console.log(Array.prototype.reduce)//ƒ reduce() { [native code] }

每一个接口都有着不同的行为和不同的使用场景。灵活运用这些方法,才能使开发效率达到最高。

比如需要检测数组中的每一项是否都小于4,你可以这么写:

function isTrue() {
  let isAllTrue = true
  arr.forEach((item) => {
    //forEach不能return,不能break,所以就算发现了某一项不符合规定,也不能跳出循环
    if (!(item < 4)) {
      isAllTrue = false
    }
  })
  return isAllTrue
}

或者这样:

function isTrue(arr) {
  let isAllTrue = true
  //for循环可随时跳出
  for (const item of arr) {
    if (!(item < 4)) {
      isAllTrue = false
      break
    }
  }
  return isAllTrue
}

亦或者这样:

function isTrue(arr) {
  return arr.every((item) => item < 4)
}

every用在此处的优势,显而易见。

官网中对于这些方法的介绍面面俱到,就不在此赘述。附上地址:Array - JavaScript | MDNJavaScript的 Array 对象是用于构造数组的全局对象,数组是类似于列表的高阶对象。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array

作为一名合格的程序员,需要对这些常用的全局对象的每一种方法都了然于胸。至少要在心里明白,在什么场景,应该用什么样的手段,才能最低成本的实现。

二,遍历的不同行为

不知道你是否有思考过这样的问题:为什么Map的遍历,是返回键值对[key,value],而Set的遍历返回的确实存储的值?

const set = new Set([1, 2, 3, 4, 5])
const map = new Map()
map.set('name', 'zhang san').set('age', 28)

for (const item of map) {
  console.log(item) // ['name', 'zhang san'],['age', 28]
}
for (const item of set) {
  console.log(item) // 1,2,3,4,5
}

其本质的上,是因为Set的迭代接口指向的是Set.propoerty.value()方法,而Map的迭代接口,指向的是Map.property.entries(),这就直接导致了在for-of循环的时候,自动调用的迭代接口工厂函数,其实就是在调用value或者entires。

console.log(set[Symbol.iterator])//ƒ values() { [native code] }
console.log(map[Symbol.iterator])//ƒ entries() { [native code] }

因此,才会产生这种差异化的行为结果。但其实从设计的角度来考虑,因为本身Map就是一个键对应值的对象工具,类似于字典;而Set则属于一个“值对应值”(你可以这么去理解,但本质上是因为其采用的键与值相等),故而前者需要在遍历的过程中,将内部的键和值都暴露出来,而后者只需要暴露其值便能达到目的。

你可以利用这种思想去模拟遍历一个Object对象:

const object = {
  name: 'zhang san',
  age: 28,
}
for (const item of Object.values(object)) {
  console.log(item) //zhang san , 28
}
for (const item of Object.entries(object)) {
  console.log(item) //['name', 'zhang san'], ['age', 28]
}

但是注意,这也只是“模拟”,因为Object对象并不会维护其插入的顺序,因此该遍历的结果,很有可能是一个“无序遍历”。而Map和Set的遍历,则是按照其插入顺序的“顺序遍历”。

二,不可枚举属性的“遍历”

首先介绍一下for-in循环的特性:遍历所有对象可访问可枚举属性。

怎么理解这句话呢?如下述示例:

const myPrototype = { protoName: 'proto', protoMethods() {} }
Object.defineProperty(myPrototype, 'protoSecret', {
  enumerable: false,
  value: 'protoSecret',
})
const obj = Object.create(myPrototype, {
  name: {
    enumerable: true,
    value: 'zhang san',
  },
  age: {
    enumerable: true,
    value: 28,
  },
  secret: {
    enumerable: false,
    value: 'secret',
  },
})

用for-in循环会输出什么结果呢?

for (const item in obj) {
  console.log(item)//name, age, protoName, protoMethods
}

可以看到,其将自身属性和原型属性中,所有可枚举属性都给遍历出来了。不仅如此,对于整个原型链上的属性,都会有这种现象:

Object.prototype.ObjectName = 'Object zhang san'
for (const item in obj) {
  console.log(item) //name, age, protoName, protoMethods, Object zhang san
}

这是由于 in 操作符的特性导致的:

console.log('secret' in obj) // true
console.log('protoName' in obj) // true
console.log('protoSecret' in obj) //true

只要存在于本身或者原型链之上,那么用in检测一个对象是否存在这些属性,就一定会返回true。

因此,在很多源码中,对于这种行为都会进行一层过滤,比如在vue源码中会经常出现类似如下的代码:

for (const item in obj) {
  if (obj.hasOwnProperty(item)) {
    console.log(item) //name, age
  }
}

但其实你完全可以这么写:

for (const item of Object.keys(obj)) {
  console.log(item) //name, age
}

该方法会返回一个由给定对象的自身可枚举属性组成的数组。values方法和entries方法也与其类似,只是返回值上存在区别。

在此给大家推荐一个装13的写法(毕竟,不喜欢装13的程序员,不是好程序员):

for (const [key, item] of Object.entries(obj)) {
  console.log(`${key} : ${item}`) //name : zhang san , age : 28
}

不过,站在兼容性的角度上,IE暂未支持该方法。

不知道大家有没有发现过这样的问题:用Symbol设置属性名的时候,不管其是否是可枚举属性,也无法将其通过for-in遍历出来,甚至Object[keys | values | entries]三兄弟也无法做到。

const obj = Object.create(myPrototype, {
  name: {
    enumerable: true,
    value: 'zhang san',
  },
  age: {
    enumerable: true,
    value: 28,
  },
  secret: {
    enumerable: false,
    value: 'secret',
  },
  [Symbol.for('symbol')]: {
    enumerable: true,
    value: 'symbol',
  },
})
for (const item of Object.values(obj)) {
  console.log(`${item}`) //zhang san , 28
}
for (const item of Object.keys(obj)) {
  console.log(`${item}`) //name , age
}
for (const [key, item] of Object.entries(obj)) {
  console.log(`${key}: ${item}`) //name: zhang san, age: 28
}

在对象中,目前能获取到Smybol属性名的方法就只有两种:getOwnPropertySmybols以及Reflect反射的ownKeys。但是它们也有缺陷,就是无论其是否可枚举,都能直接获取。这一点上getOwnPropertyNames也类似:

for (const item of Object.getOwnPropertyNames(obj)) {
  console.log(item) // name, age, secret
}
for (const item of Object.getOwnPropertySymbols(obj)) {
  console.log(item) //Symbol(symbol)
}
for (const item of Reflect.ownKeys(obj)) {
  console.log(item) // name, age, secret, Symbol(symbol)
}

注意:调用上述方法返回的都是数组,因此在遍历的时候需要用for-of去取数组的值而不是for-in,否则你会获取到数组的下标。对于任何一个方法,你都需要仔细的了解其特性之后,才能将其的优点发挥到最大。

关于循环遍历和枚举的内容还有很多,在此只是做了一个粗浅的描述。大家若是感兴趣,可以多自己尝试不同方法不同循环的用发,毕竟在开发中,遍历是最常见的手段之一。

文中内容均带有个人理解,并不保证权威。若有错误,欢迎随时批评指正。

标签: javascript

本文转载自: https://blog.csdn.net/ccuucc/article/details/123946440
版权归原作者 依然范特西fantasy 所有, 如有侵权,请联系我们删除。

“属性的遍历”的评论:

还没有评论