JavaScript 的数据类型分为 基本类型 + 引用类型。我们所说的的深浅拷贝都是针对 引用类型 而言的。
浅拷贝
只会赋值目标对象的第一层属性。
如果目标对象第一层是引用数据类型,就会直接赋值存在于栈内存中的堆内存地址,即传址,而不会开辟新的栈。
浅拷贝的实现
简单的引用复制
function shallowClone(copyObj) {
var obj = {};
for(let i in copyObj) {
obj[i] = copyObj[i]
}
return obj
}
Object.assign()
把任意多个的源对象自身的可枚举属性拷贝给目标对方,然后返回目标对象
const obj1 = {x: 1, y: {z: 3}}
const obj2 = Object.assign({}, obj1)
obj2.x = 3
console.log(obj1) // {x: 1, y: {z: 3}}
obj2.y.z = 8
console.log(obj1) // {x: 1, y: {z: 8}}
Array.concat()
const arr = [1, 2, [3, 4]];
const copy = arr.concat();
// 改变基本类型的值,不会改变原数组
copy[0] = 2
console.log(arr) // [1,2,[3, 4]]
// 改变引用类型,原数组也会跟着变化
copy[2][1] = 9
console.log(arr) // [1, 2, [3, 9]]
展开语法(Spread syntax)
const a = {
name: 'hhh',
book: {
title: 'js is hard',
price: '999'
}
}
let b = {...a}
b.name = 'new name'
console.log(a) //a的 name 没变
b.book.price = '123'
console.log(a) //a的 price 变了
Array.slice()
slice() 不改变原数组,slice() 方法返回一个新的数组对象,这个对象是由begin和end(不包括end)决定的原数组的浅拷贝
let a = [0, '1', [2, 3]]
let b = a.slice(1);
console.log(b) // ['1', [2, 3]]
a[1] = '99';
a[2][0] = 4
console.log(b) // ['1', [4, 3]]
深拷贝
就是对目标的完全拷贝,不像浅拷贝只复制了一层引用,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。拷贝前后两个对象互不影响。
深拷贝的实现
JSON.parse(JSON.stringify(obj))
JSON.parse(JSON.stringify(obj)) 可实现深拷贝,但存在以下问题:
undefined、任意的函数以及symbol值,在序列化过程中会被忽略- 它会抛弃对象的 constructor ,也就是深拷贝之后,不管这个对象原来的构造函数是什么,都会变成 Object
- 如果对象中存在循环引用的情况会报错
new Date情况下,转换结果不正确,转成字符串或者时间戳可以解决这个问题- 正则表达式类型,在序列化过程中会被转换成{}
递归
思想就是对每一层的数据都实现一次 创建对象 -> 对象赋值 的操作
function deepClone(source) {
const targetObj = source.constructor === Array ? [] : {};
for(let keys in source) {
if(source.hasOwnProperty(keys)) {
if(source[keys] && typeof source[keys] === 'object') {
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
} else {
targetObj[keys] = source[keys]
}
}
}
return targetObj;
}
简单的深拷贝完成了,但它还存在以下问题:
- 没有对传入的参数进行校验,传入 null 时,会返回 {}
- 对于对象的判断逻辑不严谨,因为
typeof null === 'object'
// 在一开始就进行判断参数校验,如果参数不是对象,返回null
function isObject(obj) {
return typeof obj === 'object' && obj !== null;
}
解决循环引用问题
上面的递归已经基本满足深拷贝的需求,但还有一种情况我们没考虑到,那就是 循环引用
使用哈希表
我们设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表时,取出该值并返回即可。
function deepClone(source, hash = new WeakMap()) {
if(!isObject(source)) return source;
if(hash.has(source)) return hash.get(source);
var target = Array.isArray(source) ? [] : {};
for(var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = deepClone(source, hash)
} else {
target[key] = source[key]
}
}
hash.set(source, target);
return target
}
ES5中用数组的方式
function deepClone(source, uniqueList) {
if(!isObject) return source;
if(!uniqueList) uniqueList = [];
var target = Array.isArray(source) ? [] : {};
var uniqueData = find(uniqueList, source)
if(uniqueData) {
return uniqueData.target
}
for(var i in source) {
if(Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = deepClone(source[key])
} else {
target[key] = source[key]
}
}
uniqueList.push({
source: source,
target: target
})
return target
}
function find(arr, item) {
for(var i = 0; i < arr.length; i++) {
if (arr[i] === item) {
return arr[i]
}
}
return null;
}
上面的函数也可以解决 引用丢失的问题,因为只要存储已拷贝过的对象就可以了。
破解递归爆栈
使用递归方法,就可能会爆栈,所以干脆不用递归,改用循环就可以解决。
function cloneLoop(x) {
const root = {};
// 栈
const loopList = [
{
parent: root,
key: undefined,
data: x
}
];
while(loopList.length) {
//深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {}
}
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
loopList.push({
parent:res,
key: k,
data: data[k]
})
} else {
res[k] = data[k]
}
}
}
}
return root;
}