forEach中你没注意的坑

270 阅读5分钟

背景

正值“木兰”台风来袭且六点下班之际,窗外风雨飘摇,甚是嚣张;无奈只能等雨势稍小,再行回家之计。这时,我的同事给我发来了这样一段代码,问这怎么不行呢?

batchRepalce() {
    const checkRcords = this.$refs.XTable.getCheckboxRecords()
    checkRcords.forEach(item => {
        item = { ...item, ...this.editFrom }
    })
}

我打开一看,这不挺好的么?怎么不行呢?然后我回了个信息,让他换一句代码

item = { ...item, ...this.editFrom }
// 替换成
Object.assign(item,this.editFrom)

他换了代码之后,回道:咦,怎么又可以了?这不是一个意思么?

坐好,请听我细细道来(让我查查资料告诉你)!

forEach

定义

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。

注意: forEach() 对于空数组是不会执行回调函数的。

参数

function(currentValue, index, array)

  • currentValue 必需。当前元素
  • index 可选。当前元素的索引值
  • array 可选。当前元素所属的数组对象

循环简单数组

下面三个操作,哪一个不会影响原数组?

const arr = [1, 2, 3, 4]
arr.forEach((item, index, array) => {
  item += 1 // ...①
  arr[index] += 1 // ...②
  array[index] += 1 // ...③
})
console.log(arr)

答案是①,如果数组中是基本数据类型:string、number、boolean等,只使用回调函数的第一个参数修改数组中的值是不会影响原数组的

循环数组对象

下面代码输出什么?

const arr = [{ num: 1 }, { num: 2 }, { num: 3 }]
arr.forEach((item, index) => {
  item = { num: 1000 + index }
})
console.log(arr)

[{ num: 1000 }, { num: 1001 }, { num: 1002 }]么?

并不是哦,输出的结果还是[{ num: 1 }, { num: 2 }, { num: 3 }]

为啥呢?看一下这段代码你就懂了?

const arr = [{ num: 1 }, { num: 2 }, { num: 3 }]
let item = arr[0]
item = { num: 1000 }
console.log(arr)

输出的结果还是[{ num: 1 }, { num: 2 }, { num: 3 }]

数组对象在遍历时,实际上是将数组项的引用地址赋值给item,如果将另一个对象的引用地址重新赋值给item,并不会改变原引用地址的数据,也就不会影响原数组。

此时我们只修改数组项的某一个属性,这个时候是会影响原数组的,因为item指向的是引用地址,修改属性相当于是修改了引用地址中对象的属性,也就会修改原数组

const arr = [{ num: 1 }, { num: 2 }, { num: 3 }]
arr.forEach((item, index) => {
  item.num = 1000 + index
})
console.log(arr) // [{ num: 1000 }, { num: 1001 }, { num: 1002 }]

空值会直接跳过

const arr = [1,,,,2, 3]
console.log(arr.length); // 6
arr.forEach(item => {
  console.log(item) // 依次输出1、2、3
})

另:这个会输出什么呢?

const arr = [1, '', undefined, null, 2, 3]
console.log(arr.length)
arr.forEach(item => {
  console.log(item)
})

forEach()第一次调用callback时就会确定遍历的范围,一般来说会按照索引升序为数组中的有效元素执行一次callback函数

在遍历中增or删原数组数据

  • 添加
const arr = [1, 2, 3]
let forEachCount = 0
arr.forEach(item => {
  console.log(item) // 依次输出 1、2、3
  forEachCount++
  arr.push(4)
})
console.log(forEachCount) // 3
console.log(arr) // [1, 2, 3, 4, 4, 4]

在调用forEach()后添加到数组中的项都不会被访问到

  • 删除
const arr = [1, 2, 3, 4, 5]
let forEachCount = 0
arr.forEach(item => {
  console.log(item) // 依次输出 1、3 、5其中2、4被跳过了 ...①
  forEachCount++
  arr.shift() // 删除第一个 ...②
})
console.log(forEachCount) // 3
console.log(arr) // [4、5]

上面说到forEach()第一次调用callback时就会确定遍历的范围,那为什么不是遍历5次呢?

  • ①,传递的item是遍历到该元素的值,即时在回调中删除了该元素,但值已经传递过来了
  • ②,每次执行arr.shift()的时候都会改变数组的长度,在下一轮循环的时候,forEach会通过当前的索引去取得当前传递item的值,所以就会出现跳过的现象;一直到索引找不到对应值,循环结束

通过forEach删除数组会出现乱序的选项,建议使用filter过滤

(...)和Object.assign

对象中的扩展运算符(...)用于取出源对象中的所有可遍历属性,拷贝到目标对象之中;

Object.assign() 方法将所有可枚举(Object.propertyIsEnumerable() 返回 true)和自有(Object.hasOwnProperty() 返回 true)属性从一个或多个源对象复制到目标对象,返回修改后的对象。

const arr = { num: 1 }
const clone = { ...arr }
clone.num = 2
console.log(arr) // {num: 1}// 等价于 Object.assignconst arr = { num: 1 }
const clone = Object.assign({}, arr)
clone.num = 2
console.log(arr)

那这是深拷贝还是浅拷贝呢?只能深拷贝第一层。第二层的拷贝还是浅拷贝

const arr = { num: 1, obj: { num: 1 }}
const clone = { ...arr }
clone.num = 2
clone.obj.num = 2
console.log(arr) // { num: 1, obj: { num: 2 }}// 等价于 Object.assignconst arr = { num: 1, obj: { num: 1 }}
const clone = Object.assign({}, arr)
clone.num = 2
clone.obj.num = 2
console.log(arr) // { num: 1, obj: { num: 2 }}

对象合并

 const a = { name: 'George', age: 20 }
 const b = { name: 'Peppa', sex: 'women' }
 const c = { ...a, ...b }
 console.log(c) // {name: 'Peppa', age: 20, sex: 'women'}
     
 // 等价于 Object.assignconst a = { name: 'George', age: 20 }
 const b = { name: 'Peppa', sex: 'women' }
 const c = Object.assign({}, a, b)
 console.log(c) // {name: 'Peppa', age: 20, sex: 'women'}

两者相同点

  1. 都可以实现对象合并
  2. 都可以将源对象的所有可枚举属性,复制到目标对象
  3. 在复制的过程中出现同名的属性(方法),后边的属性(方法)会覆盖之前的同名属性(方法)
  4. 都是浅拷贝(浅拷贝理解为拷贝指向数值的地址或者指向数值的指针,深拷贝理解为拷贝到内存中的具体的数值)

两者不同点

  • object.assign(target,source1,source2,source3)可以接收多个参数,第一个参数是目标对象;会直接改变目标对象
const a = { name: 'George', age: 20 }
const b = { name: 'Peppa', sex: 'women' }
Object.assign(a, b)
console.log(a) // {name: 'Peppa', age: 20, sex: 'women'}
  • object.assign如果只有一个参数,Object.assign会直接返回该参数
const obj = {a: 1};
console.log(Object.assign(obj)); //{a:1}
Object.assign(obj) === obj // true
  • Object.assign 会触发的setter方法 这使得mutation的值发生改变,对象展开语法 不会触发setter
const a = { name: 'George', age: 20 }
const b = { name: 'Peppa', sex: 'women' }
const c = Object.assign(a, b)
// 
const a = { name: 'George', age: 20 }
const b = { name: 'Peppa', sex: 'women' }
const c = {...a,..b}

最后

这是本人在掘金的第一篇文章,还请看到的童鞋们多多指教!

我想大多数人一开始都是很菜的,没有关系,保持求知欲,当你踩了足够的坑,也会收获更多的知识;共勉!!