深拷贝 + 浅拷贝【JS深入知识汇点7】

516 阅读4分钟

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() 方法返回一个新的数组对象,这个对象是由beginend(不包括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;
}