Skip to content

对象的深拷贝与浅拷贝

前言

浅拷贝有很多种方法,扩展运算符、Object.assign方法,直接for in 遍历拷贝。

深拷贝需要使用递归,需要额外考虑一些特殊情况,比如扩展对象Date|RegExp的拷贝,还有可能会遇到循环引用,该如何解决。

浅拷贝

使用for in来遍历对象,需要注意下面几点:

  1. for in 会遍历到继承过来的属性,所以使用hasOwnProperty判断一下,只拷贝自己本身的属性
  2. 本例中只考虑到了数组和普通对象,使用了instanceof判断传递过来的值
js
// 只考虑对象类型
function shallowCopy(obj) {
    if (typeof obj !== 'object') return

    let newObject = obj instanceof Array ? [] : {}

    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObject[key] = obj[key]
        }
    }

    return newObject
}

深拷贝

简单版

只考虑普通对象,不考虑内置对象和函数。

使用递归,赋值时在判断一下类型。

js
function deepClone(obj) {
    if (typeof obj !== 'object') return;
    let newObj = obj instanceof Array ? [] : {};

    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
        }
    }

    return newObj
}

复杂版

基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。

js
function isObject(target) {
	return target !== null && (typeof target === 'object' || typeof target === 'function')
}

function getType(target) {
	return Object.prototype.toString.call(target)
}

function deepClone(target, map = new WeakMap()) {
	// 基本类型
	if (!isObject(target)) {
		return target
	}

	if (map.get(target)) {
		return target
	}

	map.set(target, true)

	const type = getType(target)

	// 日期
	if (type === '[object Date]') {
		// 或者 return new target.constructor(target)
		return new Date(target)
	}

	// 正则
	if (type === '[object RegExp]') {
		return new RegExp(target.source, /\w*$/.exec(target))
	}

	// function
	if (type === '[object Function]') {
		const fnStr = target.toString()
		if (target.prototype) {
			// function声明的函数
			const index1 = fnStr.indexOf('(')
			const index2 = fnStr.indexOf(')')
			const index3 = fnStr.indexOf('{', index2)

			const params = fnStr.slice(index1 + 1, index2)
			const body = fnStr.slice(index3 + 1, -1)

			if (params) {
				return new Function(params, body)
			} else {
				return new Function(body)
			}
		} else {
			// 箭头函数
			return eval(fnStr)
		}
	}

	let cloneTarget

	// map
	if (type === '[object Map]') {
		cloneTarget = new Map()
		target.forEach((value, key) => {
			cloneTarget.set([key], deepClone(value, map))
		})
	}

	// set
	if (type === '[object Set]') {
		cloneTarget = new Set()
		target.forEach((value) => {
			cloneTarget.add(deepClone(value, map))
		})
	}

	// 数组
	if (type === '[object Array]') {
		cloneTarget = []
		for (const value of target) {
			cloneTarget.push(deepClone(value, map))
		}
	}

	// 对象
	if (type === '[object Object]') {
		cloneTarget = {}
		for (const key in target) {
			if (Object.prototype.hasOwnProperty.call(target, key)) {
				cloneTarget[key] = deepClone(target[key], map)
			}
		}
	}

	return cloneTarget
}

总结

  1. 浅拷贝时:

    1. 判断传递过来的是数组还是对象,可以使用下面几种方法

      1. Array.isArray() 判断是否是数组
      2. target instanceof Array 判断原型
      3. Object.prototype.toString.call(target) ==='[object Array]' 打印输出
    2. 使用for in遍历对象时,继承属性也会被遍历出来,所以在赋值时,可以使用Object.prototype.hasOwnProperty判断一下是否是自己的属性

    3. 深拷贝时,有可能会出现循环引用的情况,可以在递归的时候传递一个weakMap,记录下已经遍历过的对象,当再次遇到时,直接返回结果即可。

    4. 判断传递的对象是否是扩展对象Date|RegExp,可以使用它们的constructor属性,比如:

      /^(RegExp|Date)$/i.test(constructor.name)

参考

https://juejin.cn/post/6946022649768181774#heading-9https://juejin.cn/post/6844903929705136141#heading-5