深拷贝和浅拷贝的区别,如何实现一个深拷贝,什么是循环引用

JavaScript存在两大基本数据类型

基本数据类型:String,Number,Boolean,undefined和null

引用数据类型:object

基本数据类型保存在栈内存中,占据空间小,大小固定,且频繁被使用所以放入栈存储,值无法修改,基本数据类型的比较是值的比较只要两个变量的值像等我们认为两个变量是相等的,如果一个变量的值是一个基本数据类型,引用数据类型的值是保存在堆内存的对象,占据空间大,大小也不固定存储在栈中影响程序性能所以放入栈中存储,当一个变量的值是一个引用数据类型时,这个变量是保存在栈内存中的,值是一个指向这个变量在堆内存中的实际内存地址。

那么栈和堆有什么区别呢?

在数据结构中栈和堆的区别

  1. 栈是一种线性数据结构,它遵循后进先出(Last In First Out,LIFO)的原则。
  2. 栈的操作主要是通过压栈(Push)和出栈(Pop),即在栈的一端进行插入和删除操作。
  3. 栈的内存分配通常是在编译时确定的,大小固定,效率较高。
  4. 栈中的元素通常有确定的生命周期,由编译器自动管理
  5. 堆是一种树形数据结构,特别是二叉堆,它可以是最大堆(Max Heap)或最小堆(Min Heap)。
  6. 在堆中,最大的元素(在最大堆中)或最小的元素(在最小堆中)总是在根节点。
  7. 堆的操作主要是插入(Insert)和删除(Delete),这些操作会重新调整堆中的元素以满足堆的性质。
  8. 堆的内存分配是在运行时动态进行的,大小不固定,效率相对较低,但更加灵活。
  9. 堆中的元素通常由程序员手动管理,需要手动分配和释放内存。

 在操作系统中栈和堆的区别

操作系统中内存被分为栈区和堆区,栈区中的内存是由编译器自动分配释放,而堆区中的内存由程序员手动分配释放或是由垃圾回收机制回收操作不当就可能引起内存泄露

什么是深拷贝

深拷贝是开辟一个新的栈,两个对象的属性完全相同,但是指向不同的内存地址,修改一个对象的属性另外一个对象不会发生变化

举例

我们可以发现我们使用深拷贝复制了a变量的属性和属性值 给b变量,之后修改了b变量的age属性,a变量没有发生变化

常见的深拷贝方式

1. JSON.stringify()和JSON.pase()

上面以介绍也是最常用的方式,这里不做过多赘述,但是这种方式存在弊端,会忽略undefinedsymbol函数

2. lodash

const _ = require('lodash');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

3.递归

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

4.jQuery库

const $ = require('jquery');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false

 5.手写深拷贝

function deepCopy(obj) {
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    let copy = Array.isArray(obj) ? [] : {};

    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            copy[key] = deepCopy(obj[key]);
        }
    }

    return copy;
}

什么是浅拷贝

浅拷贝值得是创建一个新的数据这个数据有着是对原始数据的值的一份精确拷贝

如果原始数据是基本数据类型则是把基本数据类型的值拷贝一份,如果是引用数据类型则是把变量引用的内存地址拷贝一份,那么大家想这会怎么样呢?

我们仍以上面这个例子举例说明

可以发现我们改变b的值 让a的值也发生了变化,这就是说如果一个变量a的值数据类型是引用类型,我们拷贝这个变量给另外一个变量b,实际上我们拷贝的是引用类型的内存地址,这个时候a,和b会共享这个内存地址,在实际开发中这种情况是并不安全的

常见的浅拷贝实现方法

1.Objext.assign()

var obj = {
    age: 18,
    nature: ['smart', 'good'],
    names: {
        name1: 'fx',
        name2: 'xka'
    },
    love: function () {
        console.log('fx is a great girl')
    }
}
var newObj = Object.assign({}, fxObj);

2.slice()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

 3.concat()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

4. 扩展运算符

const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

深拷贝和浅拷贝的区别

通过下面这张图我们探索他们俩的区别

通过这张图我们发现虽然深拷贝和浅拷贝都是创建了一个新的对象,但是复制对象属性值的时候行为不一样,浅拷贝只是复制它的指针,不是创建一个新的内存保存这个新对象的值,新旧对象共享这个内存,而深拷贝则是完全创建一个新的内存地址将拷贝的值放入这个地址中。

深浅拷贝的应用场景

对于一些基本数据类型我们直接使用浅拷贝更加方便不会有什么影响,对于引用数据类型,为了保证开发过程中的数据安全我们建议使用深拷贝

循环引用

循环引用是指对象的属性直接或间接引用了对象本身。这种情况在JavaScript中是允许的,但如果不小心处理,可能会导致内存泄漏或者无限递归等问题。

let obj = {};
obj.self = obj;

在这个例子中,obj对象有一个属性self,这个属性的值就是obj对象本身,形成了一个循环引用。

这种情况在实际开发中可能会出现,例如在表示树形结构时,子节点可能需要引用父节点,而父节点又包含了子节点,就可能形成循环引用。

需要注意的是,如果使用JSON.stringify()方法将循环引用的对象转换为JSON字符串,会抛出错误,因为JSON不支持循环引用。同样,一些简单的深拷贝实现(如JSON.parse(JSON.stringify(obj)))也不能正确处理循环引用。

常见的解除循环引用的方法

1.使用 WeakMap 记录已经访问过的对象

在进行深拷贝的时候,我们可以使用一个 WeakMap 来记录已经被复制过的对象。当检测到一个对象已经被复制过,就直接返回对应的复制对象,这样就可以避免循环引用的问题。

function deepCopy(obj, hash = new WeakMap()) {
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    let copy = Array.isArray(obj) ? [] : {};
    hash.set(obj, copy);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            copy[key] = deepCopy(obj[key], hash);
        }
    }
    return copy;
}

 2.手动解除循环引用

如果你知道哪些地方可能会出现循环引用,也可以手动解除循环引用。例如,如果一个节点引用了它的父节点,你可以在复制节点的时候,不复制它的父节点属性,或者将父节点属性设置为 null。

let node = {
    parent: null,
    children: []
};
node.parent = node;
// 手动解除循环引用
node.parent = null;

 这两种方法都可以有效地解除循环引用,但是需要根据具体的应用场景来选择使用哪种方法。

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值