手撕深浅拷贝与优化

概念

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的
深拷贝
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象(新旧对象不共享同一块内存),且修改新对象不会影响原对象(深拷贝采用了在堆内存中申请新的空间来存储数据,这样每个可以避免指针悬挂)
浅拷贝
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址(新旧对象共享同一块内存),所以如果其中一个对象改变了这个地址,就会影响到另一个对象(只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃);
赋值和浅拷贝的区别
赋值
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝
会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

举例:

// 对象赋值
let obj1 = {
    name: 'Chen',
    age: 18,
    hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}

let obj2 = obj1;
obj2.name = 'Forever';
obj2.hobby[1] = 'swim';
obj2.hobby[2] = 'alpinism';
console.log('obj1===>', obj1);
console.log('obj2===>', obj2);

两者各属性都会进行改变

// 浅拷贝
let obj1 = {
    name: 'Chen',
    age: 18,
    hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}

let obj3 = {...obj1};
obj3.name = 'Forever';
obj3.hobby[1] = 'swim';
obj3.hobby[2] = 'alpinism';
console.log('obj1===>', obj1);
console.log('obj3===>', obj3);

在这里插入图片描述

结合下表会有更深刻的理解。在这里插入图片描述

实现

浅拷贝

注意:当拷贝对象只有一层的时候,是深拷贝

展开运算符…

let obj2 = {...obj1};

Object.assign()

let obj2 = Object.assign({}, obj1);

Array.prototype.concat()

let arr1 =  [
    {
        name: 'Chen'
    },
    'see a film', 
    'write the code', 
    'play basketball', 
    'tourism'
];
let arr2 = arr1.concat([]);

Array.prototype.slice()

let arr2 = arr1.slice();

深拷贝

JSON.parse(JSON.stringify())

对某些数据不支持:

如Date类型会被转为字符串类型,

Undefined和RegExp类型丢失等问题。

无法拷贝存在循环引用的对象。

拷贝自身可枚举字符串属性,原型链丢失。

属性特性丢失。 性能较差。
原理
用JSON.stringify将对像转成JSON字符串,再用JSON.parse()把字符串解析成对象,新的对像产生了,而且对象会开辟新的栈
对象:

let obj2 = JSON.parse(JSON.stringify(obj1));

数组:

let arr2 = JSON.parse(JSON.stringify(arr1));

jQuery.extend()

需要引入jq库

let obj1 = jQuery.extend(true, {}, obj);

Lodash

const _ = require('lodash');

const obj1 = {
  foo: 'bar',
  arr: [1, 2, 3],
  nestedObj: {
    baz: 'qux'
  }
};

const obj2 = _.cloneDeep(obj1); // 深拷贝对象

手写递归

原理:
遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/\[object (\w+)\]/, "$1").toLowerCase();
// 实现深拷贝(Object/Array)
const clone = (target) => {
    let result;
    let type = checkedType(target);
    if(type === 'object') result = {};
    else if(type === 'array') result = [];
    else  return target;
    for (let key in target) {
        if(checkedType(target[key]) === 'object' || checkedType(target[key]) === 'array') {
            result[key] = clone(target[key]);
        } else {
            result[key] = target[key]; 
        }
    }
    return result;
}
存在弊端——循环引用

什么是循环引用: 对象直接或间接地引用了自身
递归函数,看似已经解决了我们日常深拷贝的需要, 但是没有考虑到对象’循环引用’问题。
比如

obj.temp = obj; // obj中的属性temp的值指向了obj
const obj1 = clone(obj); // 无限循环下去 报错:栈内存溢出

obj中新增属性temp属性引用obj, obj中的temp中的temp属性引用了obj, 这就构成了循环引用;clone函数中, 循环调用clone,从而造成一个死循环导致爆栈
在这里插入图片描述

父级引用
就是上面的
obj.temp = obj; // obj中的属性temp的值指向了obj

同级引用
比父级往下一层

obj.detail['tempDetail'] = obj.detail; // obj.detail中的属性tempDetail的值指向了obj.detail

相互引用
你指我 我指你

obj.tempDetail= obj1;
obj1.tempDetail = obj;
优化——Map

针对深拷贝的循环引用问题,对clone函数进行优化:

开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
这个存储空间,需要可以存储key-value形式的数据,且key是一个引用类型。
我们可以选WeakMap这种数据结构:

检查WeakMap中有无克隆过的对象
有,直接返回
没有,将当前对象作为key,克隆对象作为value进行存储继续克隆

// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/\[object (\w+)\]/, "$1").toLowerCase();
// 实现深拷贝(Object/Array)
const clone = (target, hash = new WeakMap) => {
    let result;
    let type = checkedType(target);
    if(type === 'object') result = {};
    else if(type === 'array') result = [];
    else  return target;
    if(hash.get(target)) return target;

    let copyObj = new target.constructor();
    hash.set(target, copyObj)
    for (let key in target) {
        if(checkedType(target[key]) === 'object' || checkedType(target[key]) === 'array') {
            result[key] = clone(target[key], hash);
        } else {
            result[key] = target[key];
        }
    }
    return result;
}

以上。

参考资料:
前端面试 第三篇 js之路 深拷贝与浅拷贝

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值