跳到主要内容

理解深浅拷贝

浅拷贝是拷贝一层,深层次的对象级别的就拷贝引用;深拷贝是拷贝多层,每一级别的数据都会拷贝出来;

浅拷贝的时候如果数据是基本数据类型,那么就如同直接赋值那种,会拷贝其本身,(分别存储在栈内存中不同地址,不会互相影响),如果除了基本数据类型之外还有一层对象,那么对于浅拷贝而言就只能拷贝对象的引用,原对象的改变会影响到拷贝对象上,因为原对象和新对象的引用类型同时指向的是堆内存;但是深拷贝就会拷贝多层【利用递归】,即就是创建一个一摸一样的对象,原对象和新对象不共享堆内存地址,完全独立。

什么叫对象的引用?

简单来说:就是把对象赋值给一个变量,这个变量就是对象的引用

  • 由于new Array 一个对象的时候保存在堆内存中,把new Array 这个对象的堆内存地址指向了arr这个变量
  • 即对象arr值是对象的引用,也就是一个地址
js
var arr = new Array()

实现浅拷贝的方法​

1、ES6中的Object.assign方法

2、解构赋值

1、Object.assign​

js
var orignObj ={
a:123,
b:456,
c:{
name:'qqqiu'
}
}
//当orignObj只有一层时,object.assign方法作为深拷贝
var copyObj =Object.assign({},orignObj)
//创建了一个新对象,原对象和新对象不共享堆内存地址,完全独立,实现深拷贝
// console.log(orignObj === copyObj) //false

//如果除了基本类型还有一层对象,则拷贝的时对象的引用,即原对象的改变会影响拷贝对象,因为对象的引用同时指向的是堆内存
orignObj.c.name = 'hahaha'
// 拷贝的是基本类型,放在了栈内存中,而且栈内存中地址不一样,不会互相影响,相互独立
orignObj.a = '789'
console.log(copyObj)

2、解构赋值​

js
var obj1 = {a:1,b:2}
var obj2 = {...obj1}
obj1.a = 3
console.log(obj2)

实现深拷贝的方法​

1、JSON.parse(JSON.stringify()),会有缺点,对象里面有函数或者undefined属性会造成属性的丢失

2、利用递归来实现一个方法进行拷贝

1、JSON.parse(JSON.stringify())​

序列化: JSON.stringify 将js对象序列化(JSON字符串)

简单来说就是利用JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来解析JSON字符转换为js对象

js
var arr1 = [
1,2,
{
username:'qqqiu'
}
]
//创建了一个一模一样的对象,放在了堆内存中,原对象和新对象不共享堆内存地址,完全独立,实现深拷贝。
var arr2 = JSON.parse(JSON.stringify(arr1))
arr1[2].username='qwk'
console.log(arr2,arr1)

原理: 用JSON.stringify将JavaScript值(对象或者数组)转换为JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会在存储在堆内存,创建一个一摸一样的对象,原对象和新对象不共享堆内存地址,完全独立,实现深拷贝

弊端:

  • JSON.parse(JSON.stringify())这种方法实现深拷贝很简单,但是不能处理函数,即不能拷贝函数
  • 对象里面有函数或者undefined属性会造成属性的丢失
    • 序列化会把函数和underfined属性丢失

2、递归 基础版本​

js
function deepClone(obj) {
const cloneObj = {}
for(const key in obj){
if(typeof obj[key] === 'object'){
//是对象就再次调用该函数递归
cloneObj[key] = deepClone(obj[key])
}else{
//基本类型的话直接复制值
cloneObj[key] = obj[key]
}
}
return cloneObj

}

const obj1 = {
a: 123,
b: 456
};
const obj2 = deepClone(obj1);

3 、改进版(改进后递归实现,解决循环引用)​

js
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)
//获取一个对象的所有自身属性的描述符
const allDesc = Object.getOwnPropertyDescriptors(obj)
//1、Object.getPrototypeOf返回指定对象的原型
//2、Object.create 创建一个对象 ,第二个参数是一个对象,为新对象添加指定的属性值和属性描述符
const cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
//继承原型链
hash.set(obj, cloneObj)
//遍历传入参数所有键的特性
//Reflect.ownKeys: 返回一个由目标对象自身的属性键组成的数组
for (const 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)

4、利用递归来实现一个方法进行拷贝​

  • 递归每一层数据,然后遍历拷贝到新对象中
  • 递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
js
//定义检测数据类型的功能函数
function checkedType(target) {
//为什么要从第八位开始呢
// [object type]
//object加一个空格正好是7位,第八位就是就是类型的开头
return Object.prototype.toString.call(target).slice(8, -1)
}
//实现深度克隆---对象/数组
function clone(target) {
//判断拷贝的数据类型
//初始化变量result 成为最终克隆的数据
let result, targetType = checkedType(target)
if (targetType === 'Object') {
result = {}
} else if (targetType === 'Array') {
result = []
} else {
return target
}
//遍历目标数据
for (let i in target) {
//获取遍历数据结构的每一项值。
let value = target[i]
//判断目标结构里的每一值是否存在对象/数组
if (checkedType(value) === 'Object' ||
checkedType(value) === 'Array') { //对象/数组里嵌套了对象/数组
//继续遍历获取到value值
result[i] = clone(value)
} else { //获取到value值是基本的数据类型或者是函数。
result[i] = value;
}
}
return result
}

5、structuredClone​

  • structuredClone() 是原生方法,可以创建对象的深拷贝
  • 兼容性问题,可以使用polyfill代替
    • 通过polyfill来实现多平台的兼容,抹平差异性

**解决循环引用问题 ** 【理解】​

  • 循环引用: 两个对象被创建,并互相引用,形成了一个循环
  • 深拷贝,如果出现循环引用会报错,因为深拷贝存在递归,会进行无限递归造成栈溢出, WeakMap 来解决循环引用问题

为什么使用 WeakMap来解决循环引用?

  1. WeakMap 对象中的键只能是对象,不接受其他类型的值作为键名;
  2. weakMap 作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值
  3. WeakMap 不能包含无引用的对象,否则会被自动清除出集合(垃圾回收机制);
  4. WeakMap可以解决内存泄漏,当对象被WeakMap引用的时候,因为是弱引用,可能在任何时刻被回收,即垃圾回收机制不将该引用考虑在内
js
function deepClone(obj, wm = new WeakMap()) {
// constructor属性返回创建该实例对象的构造函数
if (obj.constructor === Date) return new Date(obj); // 日期对象直接返回一个新的日期对象
if (obj.constructor === RegExp) return new RegExp(obj); //正则对象直接返回一个新的正则对象
//使用WeakMap来解决循环引用,这样就不会导致无限递归的问题
if (wm.has(obj)) return wm.get(obj);
let cloneObj = Object.create(obj);
wm.set(obj, cloneObj);
//返回对象所有属性,包括可枚举和不可枚举的属性,组成数组
Reflect.ownKeys(obj).forEach((key) => {
if (obj[key] instanceof Object) {
cloneObj[key] = deepClone(obj[key], wm);
} else {
cloneObj[key] = obj[key];
}
});
return cloneObj;
}

const obj = {
name: "qqqiu",
age: "20",
info: [1, 2, 3],
};

obj.loop = obj; //设置循环引用

//如何解决深拷贝时候栈溢出问题?使用weakMap解决

const cloneObj = deepClone(obj);

console.log(cloneObj, "clone");

总结​

  • 需要循环+递归去实现

  • 使用weakMap解决循环引用

  • 如果是引用类型需要额外判断

    • 正则对象直接返回一个新的正则对象
    • 日期对象直接返回一个新的日期对象
      • 如果是函数,就返回函数的内存地址给新创建的对象