深拷贝与浅拷贝

对象的深拷贝与浅拷贝

什么是浅拷贝

只拷贝了数据对象的第一层,深层次的数据值与原始数据会互相影响(拷贝后的数据与原始数据还存有关联)

常见浅拷贝的方式:Object.assign()、扩展运算符

1
2
3
4
5
6
7
8
9
const obj1 = { name: 'dog', info: { age: 3 } }
const obj2 = Object.assign({}, obj1)
// 或者
const obj2 = { ...obj1 }

obj2.name = 'cat'
obj2.info.age = 4
console.log(obj1) // { name: 'dog', info: { age: 4 } }
console.log(obj2) // { name: 'cat', info: { age: 4 } }

什么是深拷贝

不管数据对象有多少层,改变拷贝后的值都不会影响原始数据的值。(拷贝后的数据与原始数据毫无关系)

常见深拷贝的方式:JSON.parse() 和 JSON.stringify() 配合使用

1
2
3
4
5
6
const obj1 = { name: 'dog', info: { age: 3 }, fn: function () {} }
const obj2 = JSON.parse(JSON.stringify(obj1))
obj2.name = 'cat'
obj2.info.age = 4
console.log(obj1) // { name: 'dog', info: { age: 3 }, fn: function(){} }
console.log(obj2) // { name: 'cat', info: { age: 4 } }

浅拷贝可以使用 Object.assign 或者遍历赋值的方式手动实现。

深拷贝可以通过JSON.stringify() 与 JSON.parse()实现,但对于对象有要求,因为在遇到函数,undefined,Sybmol,Date对象时会自动忽略,遇到正则时会返回空对象。也可以通过递归的方式手动实现深拷贝。

浅拷贝的实现

Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。

1
2
3
function clone(obj) {
return Object.assign({}, obj)
}

遍历赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function clone(obj) {
let cloneObj = Array.isArray(obj) ? [] : {};

// Object.keys不会遍历到原型链中的属性
for (let key of Object.keys(obj)) { // for of 遍历元素值
cloneObj[key] = obj[key]
}
return cloneObj
}


let c1 = {
a: 1,
b: 2,
c: {
d: 4,
e: 5
}
}

let d1 = clone(c1)
console.log(c1, d1)

c1.c.d = 12
console.log(c1, d1)

深拷贝的实现

JSON.stringify() 与 JSON.parse()

1
2
3
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}

问题:

  1. 对象中的时间类型会被变成字符串类型数据
  2. 对象中的 undefined 和 函数类型会直接丢失
  3. 对象中的 NaN、Infinity、-Infinity 会变成 null
  4. 对象循环引用时会报错

这些问题大部分源于 JSON.stringify(),毕竟这个函数的初衷不是用来做拷贝的。当然,json.stringify 提供了第二个参数(为一个函数),对象中的每个值会递归交给函数处理,可以在该函数中判断类型做相应处理。

递归

简单版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function deepClone(obj) {
// 处理 null date reg 原始类型
if (obj === null) return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (typeof obj !== 'object') return obj

// 处理对象和数组
const cloneObj = new obj.constructor() // 根据对象和数组自动生成
for (const key of Object.keys(obj)) {
cloneObj[key] = deepClone(obj[key])
}

return cloneObj
}
稍微复杂版
  1. 解决 symbol 无法作为键的问题

Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组 等于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

  1. 解决循环引用问题
1
2
3
4
5
6
const obj = {
a: 1
}
obj.b = obj

const newObj = deepClone(obj)

报错:栈内存溢出,死循环了。

因为在递归遍历obj的属性时,obj有属性指向自身,因此会无限循环。

开辟新内存记录出现过的 obj,如果已经出现过就不再遍历,直接返回。

  1. 解决垃圾回收问题

使用 WeakMap,WeakMap是弱引用,不影响垃圾回收(WeakMap键指向的对象的其它引用被清除后,该对象会被垃圾回收)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function deepClone(obj, hash = new WeakMap()) {
// 处理 null date reg 原始类型
if (obj === null) return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (typeof obj !== 'object') return obj

// 解决循环引用问题
if (hash.has(obj)) return hash.get(obj) // 说明 obj 在属性中出现过,再去遍历就会造成循环引用,因此直接返回hash中的记录

// 处理对象和数组
const cloneObj = new obj.constructor() // 根据对象和数组自动生成

hash.set(obj, cloneObj) // 记录出现过的 obj

// Reflect.ownKeys 解决 Symbol 无法作为键的问题
Reflect.ownKeys(obj).forEach(key => {
cloneObj[key] = deepClone(obj[key], hash)
})
return cloneObj
}

日常开发

日常开发中,如果要使用深拷贝,为了兼容各种边界情况,一般是使用三方库如 lodash

1
2
3
npm i --save lodash
import _ from 'lodash'
const cloneObj = _.cloneDeep(obj)

structuredClone()

一个新的深拷贝API,ES 的一部分,目前兼容性还不够好。chrome >= 98才支持。

1
structuredClone(value: any): any

这个函数有第二个参数 transferables,这个参数很少有用。详细信息,请参考MDN 页面structuredClone()

缺陷:

  1. 一些内置对象不能被复制,structuredClone()会抛出DOMException

Functions (ordinary functions, arrow functions, classes, methods)

DOM 节点DOM nodes

  1. structuredClone()不会复制对象的原型链

如果structuredClone()与类实例一起使用,将获得一个普通对象作为返回值,因为结构化克隆会丢弃对象的原型链。

  1. structuredClone()并不能复制DOM节点特性属性

访问器 get 变成了数据属性。

在副本中,特性属性始终具有默认值。

1
2
3
writable: true,
enumerable: true,
configurable: true,

深拷贝与浅拷贝
http://example.com/2022/06/14/深拷贝与浅拷贝/
Author
John Doe
Posted on
June 14, 2022
Licensed under