总结自若离的《JavaScript核心原理精讲》
在JavaScript编程中,经常会遇到拷贝对象的情况,在拷贝的场景中,存在着两种情况,所谓的浅拷贝与深拷贝。
浅拷贝
浅拷贝意味着将与一个对象的所有属性拷贝到一个新的对象上。浅的意思是只拷贝对象的属性而不考虑该属性值是否是引用类型(即嵌套情况)。新的对象虽然开辟了一个新的内存地址,但该地址引用的对象属性都是原来对象的属性。
浅拷贝的实现
Object.assign
Object.assign具有拷贝对象的能力。
本来该方法的用法是将一个对象与另一个对象进行合并const res = Object.assign(target, source)
。因此当target为空对象时,即实现了浅拷贝的功能。
- 该方法会将源对象上的可枚举属性与自有属性复制到目标对象上(包括Symbol类型的属性),不会拷贝继承的属性。
- 该方法会将对象中的访问器属性转换为数据属性复制到目标对象中,因为该方法使用源对象的
[[Get]]
来获取属性值,用目标对象上的[[Set]]
来设置属性值
扩展运算符
const res = { ...source }
与Object.assign的拷贝类似
数组的concat拷贝和slice拷贝
const res = [1, { a: 2 }, 3].concat()
。
const res = [1, 2, { a: 3 }, 4].slice(1, 3)
本质上讲,数组同样也是对象,而concat方法和slice方法是数组实例的方法,最终结果也是浅拷贝。
实现
function shallowCopy(source) {
const target = Array.isArray(source)? []: {}
for(let prop in source) {
if (souce.hasOwnProperty(prop)) {
target[prop] = source[prop]
}
}
return target
}
深拷贝
与浅拷贝相对的是深拷贝,深拷贝意味着新建一个目标对象(内存区域),将源对象从内存中完整拷贝出来给目标对象,这样实现了源对象与目标对象的彻底分离,双方互不影响。
JSON方法
先将源对象序列化const mid = JSON.stringify(source)
再将序列化的数据解析为新的对象即可const res = JSON.parse(mid)
。
- 因为JSON方法是用来做序列化的,因此无法拷贝属性值是函数、undefined和Symbol属性
- 无法拷贝循环引用
obj.key = obj
- 拷贝Date引用类型时属性值会变成字符串
- 无法拷贝不可枚举属性
- 无法拷贝原型链
- 拷贝正则类型会变成空对象
- 拷贝NaN、Infinity、-Infinity时,序列化后变为null
如果深拷贝只是为了数据的拷贝,那么使用JSON方法无疑是最快捷最实用的方式。
基本版递归实现
function deepClone(source) {
const target = Array.isArray(source)? []: {}
for(let prop in source) {
if (typeof source[prop] === 'object') {
target[prop] = deepClone(source[prop])
} else {
target[prop] = source[prop]
}
}
return target
}
- 无法拷贝方法、不可枚举属性和Symbol属性
- 只是普通的引用类型值的拷贝
- 无法解决循环引用(陷入死循环)
改进版递归实现
- 针对不可枚举属性和Symbol类型值,实用Reflect.ownKeys方法
- Reflect.ownKeys(obj)返回obj对象自身属性名组成的数组,无论是否可枚举,并且包括Symbol类型的属性名
- 当参数source是一个Date类型或者正则类型时,则直接生成一个新的实例返回
- 利用
Object.getOwnPropertyDescriptors(obj)
方法可以获得对象的所有自有属性及其属性描述对象,结合Object.create(proto)
方法创建新对象,并继承传入源对象的原型链 - 利用WeakMap类型作为Hash表,检测是否存在循环引用。如果存在循环引用,则引用直接返回WeakMap存储的值
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date)
return new Date(obj) // 日期对象直接返回一个新的日期对象
if (obj.constructor === RegExp)
return new RegExp(obj) //正则对象直接返回一个新的正则对象
//如果循环引用了就用 weakMap 来解决,我们只是用WeakMap来存储一下obj 与 cloneObj的映射关系,个人觉得hash在函数执行完后就释放了,没必要非用WeakMap来处理
if (hash.has(obj)) return hash.get(obj)
let allDesc = Object.getOwnPropertyDescriptors(obj)
//继承原型链并将obj的自有属性添加到cloneObj上
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
hash.set(obj, cloneObj)
//遍历传入参数所有键的特性
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}
// 下面是验证代码
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)