什么是深浅拷贝
想当初,我一直以为赋值也算是浅拷贝……
赋值
对基本类型来说,赋值后就是两份栈内存中的独立数据,赋值后互不影响;对引用类型来说,赋值后仅是两份栈内存中的地址,地址指向堆内存中的同一份数据,赋值后相互影响。
实际上,浅拷贝和深拷贝都是对于引用类型来说的,比如拷贝一个对象或数组。
浅拷贝
新建一个对象,这个对象有原始对象的属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的是其内存地址,会互相影响。
简单说,浅拷贝只解决了第一层的问题。
深拷贝
深拷贝会拷贝原始对象的所有属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起被拷贝时即为深拷贝。
深拷贝相较于浅拷贝速度慢并且花销更大。深拷贝的两个对象互不影响。
如何实现深浅拷贝
浅拷贝的实现
Object.assign() 实现
Object.assign()
把所有可枚举属性从一个或多个对象复制到目标对象,返回目标对象。
const origin = {
name: "Leon",
friend: {
name: "Jack",
age: 18
}
}
const shallowCopy = Object.assign({}, origin)
shallowCopy.name = "Mike"
shallowCpoy.friend.age = 23
console.log(origin)
// { name: "Leon", friend: { name: "Jack", age: 23 } }
console.log(shallowCopy)
// { name: "Mike", friend: { name: "Jack", age: 23 } }
拓展运算符实现
ES6 的 ...
拓展运算符。
let origin = ['Leon', 'Jack', ['Apple', 'Banana']]
let shallowCopy = [...origin]
shallowCopy[1] = 'Allen'
shallowCopy[2][1] = 'Peach'
console.log(origin)
// ['Leon', 'Jack', ['Apple', 'Peach']]
console.log(shallowCopy)
// ['Leon', 'Allen', ['Apple', 'Peach']]
Array.prototype.slice() 实现
slice()
方法返回的是原数组的浅拷贝,原数组不会被改变。
let origin = [0, 1, [2, 3]]
let shallowCopy = origin.slice(0) // 从第0个开始到数组结束,即整个数组
shallowCopy[1] = 8
shallowCopy[2][1] = 9
console.log(origin)
// [0, 1, [2, 9]]
console.log(shallowCopy)
// [0, 8, [2, 9]]
当然,Array.prototype.concat()
也一样可以实现。
自己手写实现
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return
// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {}
// 遍历 object,并且判断是 object 的自身属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key]
}
}
return newObject
}
这里注意,之所以还要用 hasOwnProperty()
多判断一步,是因为遍历原始对象时,其原型链上的属性也会被遍历到,而我们只需要拷贝其自身的属性。
深拷贝的实现
Lodash 的 _.cloneDeep() 方法实现
const _ = require('lodash')
let origin = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
}
let deepCopy = _.cloneDeep(origin)
console.log(origin.b.f === deepCopy.b.f) // false
// TODO 源码分析
jQuery 的 $.extend() 方法实现
const $ = require('jquery')
let origin = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
}
let deepCopy = $.extend(true, {}, origin)
console.log(origin.b.f === deepCopy.b.f) // false
JSON.stringfy() 方法不完美实现
利用 JSON.stringfy()
将对象序列化成 JSON 字符串,再使用 JSON.parse()
来反序列化成对象。
let origin = {
name: "Leon",
friend: {
name: "Jack",
age: 18
}
}
let deepCopy = JSON.parse(JSON.stringfy(origin))
deepCopy.name = "Mike"
deepCopy.friend.age = 23
console.log(origin)
// { name: "Leon", friend: { name: "Jack", age: 18 } }
console.log(deepCopy)
// { name: "Mike", friend: { name: "Jack", age: 23 } }
这种实现方式比较常用,但是存在缺陷:
- 拷贝的对象中如果有函数、undefined、symbol,当使用过
JSON.stringify()
处理后都会消失 - 拷贝 Date 引用类型会变成字符串,拷贝 RegExp 引用类型会变成空对象
- 拷贝 NaN、Infinity、-Infinity,会变成 null
- 无法拷贝对象的原型链
- 无法拷贝不可枚举的属性
- 存在循环引用的问题,即存在
prop[key] = prop
,会报错
如果原始对象中明确不存在以上的情况,才可以使用。
自己手写循环递归实现
function deepCopy(obj) {
// 如果是基本数据类型或者是函数,直接返回
if (typeof obj !== 'object' || obj === null || obj instanceof Function) {
return obj;
}
// 对日期类型和正则表达式类型单独处理
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 创建一个新的对象
const copy = Array.isArray(obj) ? [] : {};
// 遍历对象的属性并复制到新对象中
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}