深拷贝和浅拷贝
最开始接触浅拷贝,可能是通过一些数组或者是js对象合并的实现,可能并没有对其中的原理和实现做一些比较深入的整理和学习,今天终于有时间可以系统的去整理一下这部分的知识点
让我们进入正题,可能我们对于浅拷贝会有一个初步的理解:
需要我们自己创建一个对象, 用来接受你要重新复制或引用的对象值。如果对象属性是基本
的数据类型, 复制的就是基本类型的值给新对象;但是如果属性是 引用数据类型,复制的就
是内存中的地址, 如果是其中的一种对象改变了这个内存中的地址的话,那么肯定是会影响
另一个对象。
实际开发中我们可能会用以下的几种方式
-
object.assign(target, ...sources)
-
但是使用中需要注意几点
- 它不会拷贝对象的继承属性
- 它不会拷贝对象的不可枚举的属性
- 可以拷贝 Symbol 类型的属性
-
-
拓展运算符 let cloneObj = {...obj}
-
concat 拷贝数组
-
slice 拷贝数组
如何实现一个浅拷贝
基于上面一些关于浅拷贝的理解, 如果让你去实现一个浅拷贝,大致的思路会分为两部分:
1. 对于基本类型做一个最基本的一个拷贝
2. 对于引用类型开辟一个新的存储, 并且拷贝一层对象属性
那么我们围绕这两个思路,尝试去实现一下吧:
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let item in target) {
if (target.hasOwnProperty(item)) {
cloneTarget[item] = target[item];
}
}
return cloneTarget;
} else {
return target;
}
}
这段代码的实现思路就是, 通过利用类型判断, 针对于引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性.
小tip
Object.prototype.hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是是否会有指定的键)
深拷贝的原理和实现
从上面可以看出浅拷贝只是创建了一个新对象, 复制了原有对象的基本类型的值, 而引用类型只拷贝了一层属性, 在深层是无法拷贝的
那么 对于深拷贝而言, 它对于复杂引用数据类型, 其在内存中完全开辟了一块内存地址, 并将原有的对象完全复制过来存在。
那么我们来看一下深拷贝的原理:
将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟出一个全新的空间存在新对象, 且新对象的修改并不会改变原对象, 二者实现真正的分离
既然我们知道了深拷贝的实现原理, 那我们是不是可以去想一想它的实现的方式呢?
方式1 (JSON.stringify)
这种方式的话,其实就是把一个对象序列化成为一个JSON的字符串,并将对象里面的内容转换成字符串, 最后再用 JSON.parse() 方法将JSON 字符串生成一个新的对象。
let obj1 = {a:1, b: [1,2,3]}
let str = JSON.stringify(obj1)
let obj2 = JSON.parse(str)
console.log(obj2); // {a:1, b : [1,2,3]}
其实基于 JSON.stringify 实现深拷贝需要注意一些地方,这里我做了一些总结:
1. 拷贝的对象的值中如果有函数, undefined、symbol这几种类型, 经过JSON.stringify序列化之后的字符串中这个键值对会消失
2. 拷贝Date 引用类型会变成字符串
3. 无法拷贝不可枚举的属性
4. 无法拷贝对象的原型链
5. 拷贝 RegExp 引用类型会变成空对象
6. 对象中含有 NaN、Infinity、 -Infinity, JSON 序列化的结果会变成 null
7. 无法拷贝对象的循环引用 既对象成环 (obj[key] = obj)
下面我会给出一段代码供大家去测试学习,希望可以交流一下
function testObj() {
this.func = function () { console.log('test-func')};
this.obj = { test: 1};
this.arr = [11,22,33];
this.unde = undefined;
this.reg = /123456/;
this.date = new Date(0);
this.NaN = NaN;
this.infinity = Infinity;
this.sym = Symbol(11);
}
let obj1 = new testObj();
Object.defineProperty(obj1, "innumerable", {
enumrable: false,
value: 'innumerable',
})
console.log('obj1', obj1)
let testStr = JSON.stringify(obj1);
let testObj2 = JSON.parse(testStr);
console.log('testObj2', testObj2)
那么我们先来尝试实现一下吧
如何实现一个深拷贝
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 来解决
if (hash.has(obj)) return hash.get(obj)
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
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
}
其中我们可以上面提到的几个问题做几个具体的解析
1. 针对于能够遍历对象的不可枚举属性以及 Symbol 类型, 我们可以使使用Reflect.ownKeys方法
2. 当参数为 Date、RegExp 类型则直接生成一个新的实例返回
3. 利用 Object的 getOwnPropertyDescriptors方法可以获取对象的所有属性,以及对应的特性,顺便结合 object.create() 创建一个新的对象,并继承传入原对象的原型链
4. 利用WeakMap 类型作为hash, 因为 WeakMap 是弱引用类型, 可以有效防止内存泄漏,作为检测循环引用很有帮助, 如果存在循环,则引用直接返回 WeakMap存储的值
test 代码:
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)
好了,今天的深拷贝的探索先到这里即将结束, 我们可以一起交流探索 一些相关的实现方式。
告辞