Object 拷贝

什么是对象的浅拷贝

例子
当变量相等时,实际上指向的是同一个对象,所以改变 b 的属性值后。a 会被影响到

1
2
3
4
let a = {age: 16}
let b = a
b.age = 20
// a.age 也会变成 20

浅拷贝,可以使用以下方法

1
2
3
let c = { ...a }
// vue
let c = { ...this.a }

当修改了 age,不会互相影响
浅拷贝时里面的嵌套对象是共享的,还是会被影响,深拷贝时里面的嵌套对象就不会被影响

1
2
3
4
5
6
7
let b = {
name: 'John',
info: {
age: 30
}
}
let c = { ...b }

修改了 name,不会互相影响,如果修改了 info.age,则会互相影响

浅拷贝的方式

  1. Object.assign():
    1
    const c = Object.assign({}, b)

这也会创建一个新的对象 c,并把 b 的所有属性拷贝到 c。
Object.assign() 是 ES6 提供的专门用于对象合并的方法,性能较好。

  1. 扩展运算符 spread 语法:
    1
    2
    const c = { ...b } // 拷贝对象
    const c = [ ... b] // 拷贝数组

我们已经讲过,这会创建一个新的对象 c,并拷贝 b 的属性到 c。
性能中等,略优于手动循环赋值,但不及 Object.assign()。

  1. 手动循环拷贝:
    1
    2
    3
    4
    const c = {}
    for (const key in b) {
    c[key] = b[key]
    }

这手动循环 b 的属性,并拷贝到 c 中。
循环赋值的性能较差,不推荐使用。

  1. 使用 _.clone():
    1
    const clone = _.clone(obj)

如前所说,_.clone() 使用 Object.assign() 进行浅拷贝,性能较好。

深拷贝

  1. JSON.parse(JSON.stringify(obj)):
    1
    const c = JSON.parse(JSON.stringify(b))

这会把对象 b 转换为 JSON 字符串,然后解析该字符串得到一个新对象 c。在这个过程中,嵌套的对象也会被拷贝。

  1. 手动递归拷贝:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const c = {}
    const deepCopy = obj => {
    if (typeof obj !== 'object' || obj == null) return obj
    for (let key in obj) {
    c[key] = deepCopy(obj[key])
    }
    return c
    }
    const c = deepCopy(b)

这定义了一个递归函数 deepCopy(),它会不断调用自己来拷贝嵌套的对象属性。

  1. 使用 lodash 的 _.cloneDeep():
    1
    const c = _.cloneDeep(b)

lodash 中的 _.cloneDeep() 可以完美拷贝一个对象及其嵌套对象。

_.cloneDeep() > 手动递归拷贝 > JSON.parse(JSON.stringify(obj))
一些具体的性能对比数据:

  • 浅拷贝只需要 0.5ms
  • lodash _.cloneDeep 需要 5-10ms
  • JSON.parse(JSON.stringify(obj)) 需要 200-500ms
  1. 首先考虑是否真的需要深拷贝 - 浅拷贝的性能是最好的,如果满足需求应首选浅拷贝
  2. 如果需要深拷贝,使用 lodash 的 _.cloneDeep() 方法
  3. 最后才考虑手动递归拷贝或者 JSON.parse(JSON.stringify(obj))

在 vue 中

this.buttons 我需要做一个循环处理判断修改值,1. 直接用 this.buttons for 循环然后变化值,2. 先用变量进行深拷贝后 for 循环处理完值,再赋值回 this.buttons,这两种方式,哪种性能更高

这两种方式的性能差异主要体现在:

  1. 深拷贝的性能开销
  2. 数组的变异(mutation)次数
    如果直接循环 this.buttons 进行修改,那么数组的变异次数比较高,这会触发 Vue 的依赖追踪和更新机制,降低性能。
    而先深拷贝一份,循环修改副本,最后再将副本赋值给 this.buttons,数组的变异只发生一次,所以性能会好一些。
    所以,如果 this.buttons 数组较大,修改的次数也较多,那么推荐:
    1
    let temp = _.cloneDeep(this.buttons)

// 循环修改 temp
this.buttons = temp
如果 this.buttons 较小,只需要做少量修改,那么直接循环修改也问题不大,性能差异不会很明显。
综上,建议是:

  1. 如果只是少量修改,直接循环 this.buttons 也可以
  2. 如果涉及大量修改,尤其是大数组,那么深拷贝到一个临时变量,循环修改临时变量,最后再赋值回 this.buttons 是一个更优的方案。
  3. 也可以结合 computed property 进行优化,例如:
    1
    2
    3
    4
    5
    6
    7
    computed: {
    editedButtons() {
    let temp = _.cloneDeep(this.buttons)
    // 修改 temp
    return temp
    }
    }

在一般环境下,当数组长度达到100-500左右,如果进行大量修改(10-50次以上),或在 1000 个数组更改其中一个项时,使用拷贝的方法性能会更好,那么使用深拷贝的方式会有比较明显的性能优势。
因此我们建议,基本上使用浅拷贝或深拷贝的方式来处理数组数据会比较好。