js的浅拷贝和深拷贝(面试高频考点)

132 阅读8分钟

浅拷贝和深拷贝

//数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和对象数据类型。
//基本数据类型的特点:直接存储在栈(stack)中的数据
//引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
//引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。
//当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
image.png

1. 浅拷贝-------对象的浅拷贝是其属性与拷贝源对象的属性共享相同引用(指向相同的底层值)的副本。

//因此,当你更改源或副本时,也可能导致其他对象也发生更改
//也就是说,你可能会无意中对源或副本造成意料之外的更改。这种行为与深拷贝的行为形成对比,
//在深拷贝中,源和副本是完全独立的。
//对于浅拷贝,有选择地更改对象中现有元素的共享属性的值与给现有元素赋一个全新的值是不同的,理解这一点很重要。
//如果在一个数组对象名为copy的浅拷贝中,copy[0]元素的值是{"list":["butter","flour"]},然后执行copy[0].List = ["oil","flour"],那么源对象中的相应元素也将发生变化。
//因为你有选择地更改了源对象和浅拷贝共享的对象的属性。
//如果你做的是copy[0] = {"list":["oil","flour"]},那么源对象中的对应元素将不会改变.
//因为在这种情况下,你不仅仅是有选择地改变了浅拷贝与源对象共享的现有数组元素的属性;相反,你实际上是在浅拷贝中分配了一个全新的值给copy[0]数组元素。

//在JS中,所有标准的内置对象复制操作,创建的是浅拷贝而不是深拷贝(如:展开语法(...)、Array.prototype.concat()、Array.prototype.slice()、Array.from()、Object.assign()和Object.create())。


1.  示例
//考虑以下示例,其中创建了一个ingredients_list数组对象,然后通过复制ingredients_list对象创建了一个ingredients_list_copy对象。
let ingredients_list = ["noodles", { list: ["eggs", "flour", "water"] }];
let ingredients_list_copy = Array.from(ingredients_list);
console.log(JSON.stringify(ingredients_list_copy));// ["noodles",{"list":["eggs","flour","water"]}]

//改变ingredients_list_copy中list属性的值也将导致ingredients_list源对象中的list属性发生变化。
ingredients_list_copy[1].list = ["rice flour", "water"];
console.log(ingredients_list[1].list);// Array [ "rice flour", "water" ]
console.log(JSON.stringify(ingredients_list));// ["noodles",{"list":["rice flour","water"]}]

//为 ingredients_list_copy中的第一个元素赋值,不会导致ingredients_list源对象中的第一个元素发生任何变化。
ingredients_list_copy[0] = "rice noodles";
console.log(ingredients_list[0]);// noodles
console.log(JSON.stringify(ingredients_list_copy));// ["rice noodles",{"list":["rice flour","water"]}]
console.log(JSON.stringify(ingredients_list));// ["noodles",{"list":["rice flour","water"]}]



2. 赋值和浅拷贝的区别
//当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。
//也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
//浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
//如果属性是基本类型,拷贝的就是基本类型的值;
//如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
//即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。



3. 浅拷贝方法:
    1.Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
        //Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身
        //注意:当object只有一层的时候,是深拷贝
    2.Array.prototype.concat()修改新对象会改到原对象
    3.Array.prototype.slice()同样修改新对象会改到原对象
        //关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
    4.Object.create()
    5.Array.from()
    6.展开语法(...)
    7.赋值(=)拷贝
  
  
//原数组的元素会按照下述规则拷贝:
    //如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。
    //两个对象引用都引用了同一个对象。
    //如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
//对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。
    //在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

2. 深拷贝--------对象的深拷贝是指其属性与其拷贝的源对象的属性不共享相同的引用(指向相同的底层值)的副本。

//因此,当你更改源或副本时,可以确保不会导致其他对象也发生更改;
//也就是说,你不会无意中对源或副本造成意料之外的更改。
//这种行为与浅拷贝的行为形成对比,在浅拷贝中,对源或副本的更改可能也会导致其他对象的更改(因为两个对象共享相同的引用)。
//在JS中,标准的内置对象复制操作,不创建深拷贝(相反,它们创建浅拷贝)(如:展开语法、Array.prototype.concat()、Array.prototype.slice()、Array.from()、Object.assign() 和 Object.create())。
//如果一个JS对象可以被序列化,则存在一种创建深拷贝的方式:使用JSON.stringify()将该对象转换为 JSON 字符串,然后使用JSON.parse()将该字符串转换回(全新的)JavaScript 对象:

let ingredients_list = ["noodles", { list: ["eggs", "flour", "water"] }];
let ingredients_list_deepcopy = JSON.parse(JSON.stringify(ingredients_list));
// Change the value of the 'list' property in ingredients_list_deepcopy.
ingredients_list_deepcopy[1].list = ["rice flour", "water"];
// The 'list' property does not change in ingredients_list.
console.log(ingredients_list[1].list);// Array(3) [ "eggs", "flour", "water" ]
//上面的代码看出,因为深拷贝与其源对象不共享引用,所以对深拷贝所做的任何更改都不会影响源对象。

//然而,虽然上面代码中的对象足够简单,可以序列化,但许多JS对象根本不能序列化
//例如,函数(带有闭包)、Symbol、在 HTML DOM API 中表示 HTML 元素的对象、递归数据以及许多其他情况。
//在这种情况下,调用JSON.stringify()来序列化对象将会失败。所以没有办法对这些对象进行深拷贝。
//对于可序列化的对象,你也可以使用structuredClone()方法来创建深拷贝。
//structuredClone()的优点是允许源代码中的可转移对象被转移到新的副本,而不仅仅是克隆。
//但是请注意,structuredClone()不是JS语言本身的特性
//相反,它是浏览器和任何其他实现了window这样全局对象的JS运行时的一个特性。
//调用structuredClone()来克隆一个不可序列化的对象会失败,与调用JSON.stringify()来序列化它会失败相同。


1. 为什么要使用深拷贝?-------因为希望在改变新的数组(对象)的时候,不改变原数组(对象)
//深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
//浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存(分支)。

//浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
//如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
//深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象,是“值”而不是“引用”(不是分支)
//拷贝第一层级的对象属性或数组元素
//递归拷贝所有层级的对象属性和数组元素
//深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。



2. 深拷贝方法:
    1.使用JSON.stringify()将该对象转换为 JSON 字符串,然后使用JSON.parse()将该字符串转换回(全新的)JavaScript 对象,这种方法虽然可以实现数组或对象深拷贝,但不能处理函数
    2.structuredClone()方法来创建深拷贝,Function对象是不能被结构化克隆算法复制的;企图去克隆 DOM 节点同样会抛出 `DATA_CLONE_ERR` 异常;对象的某些特定参数也不会被保留
    3.手写递归方法
        递归方法实现深度克隆原理:遍历对象、数组直到里面都是基本数据类型,然后再去复制,就是深度拷贝
    4.函数库lodash
        该函数库也有提供_.cloneDeep用来做 Deep Copy

方法代码实现可参考:blog.csdn.net/qq_37430247…