Analice la diferencia entre copia profunda, copia superficial y asignación a través de la pila

prefacio

Los tipos de datos se dividen en:

Tipos de datos básicos Cadena, Número, Booleano, Nulo, Indefinido, Símbolo

Tipo de datos del objeto Objeto, Matriz

Las características del tipo de datos básico: los datos almacenados directamente en la pila (pila)
Las características del tipo de datos de referencia: el objeto almacenado se hace referencia en la pila y los datos reales se almacenan en la memoria del montón

El tipo de datos de referencia almacena un puntero en la pila, que apunta a la dirección inicial de la entidad en el montón. Cuando el intérprete busca un valor de referencia, primero recupera su dirección en la pila y luego obtiene la entidad del montón después de obtener la dirección.

copia profunda versus copia superficial

La copia profunda y la copia superficial son solo para tipos de datos de referencia como Objeto y Matriz.

  • La copia superficial solo copia el puntero a un objeto, no el objeto en sí, y los objetos nuevos y antiguos aún comparten la misma memoria.

  • Sin embargo, la copia profunda creará un objeto idéntico. El nuevo objeto no comparte memoria con el objeto original y la modificación del nuevo objeto no cambiará el objeto original.

¿La tarea es una copia profunda o una copia superficial?

Cuando asignamos un objeto a una nueva variable, lo que realmente se asigna es la dirección del objeto en la pila, no los datos en el montón. Es decir, los dos objetos apuntan al mismo espacio de almacenamiento. No importa qué objeto cambie, en realidad es el contenido del espacio de almacenamiento modificado, por lo que los dos objetos están vinculados.

Una copia superficial es una copia bit a bit de un objeto, que crea un nuevo objeto con una copia exacta de los valores de propiedad del objeto original. Si el atributo es un tipo básico, se copia el valor del tipo básico; si el atributo es una dirección de memoria (tipo de referencia), se copia la dirección de memoria, por lo que si uno de los objetos cambia esta dirección, afectará al otro. objeto. Es decir, el constructor de copia predeterminado solo realiza una copia superficial del objeto (copiando miembros uno por uno), es decir, solo se copia el espacio del objeto pero no el recurso.

Asignación directa:

var origin = {db:{id:'001'},num:33,read:[1,2,3,4]}
var data = origin;
origin.num= 10;
origin.read = [1,2];
origin.db.id = '002;
origin.db = {text:'aaa'}

Entonces, ¿es la copia superficial lo mismo que la tarea?

copia superficial:

Supongamos que tenemos un método de copia superficial llamado copia superficial. Para conocer el método de copia superficial específico, consulte el apéndice de este artículo: método de copia superficial

var origin = {db:{id:'001'},num:33,read:[1,2,3,4]}
var data = shallowCopy(origin);
origin.num = 10;
origin.read[0] = 10;
origin.db.id = '003'

Por lo tanto, podemos decir: en el caso de la asignación, el cambio del objeto antiguo definitivamente provocará el cambio del nuevo objeto (ya sea en una capa o en N capas)

¿Por qué necesitamos compartir la primera capa o la enésima capa? Veamos esta pera:

const data = {arr:[1,2,3],inner:{arr:[3,4,5]}}
const data2 = shallowCopy(data)
data.arr = [0,0,0];
data.inner.arr = [0,0,0]

可以看到浅拷贝后第一层引用对象不关联。具体见附录-浅拷贝面试题

深拷贝方法

可以看到,深拷贝后的结果与源数据是两个完全独立的数据。

深拷贝方法实现:

一、 通过JSON.stringify()

var a = {data:{name:'xxx'}}
var b = JSON.parse(JSON.stringify(a))
b; // {data:{name:'xxx'}}
a.data.name = 'abc';
b  // {data:{name:'xxx'}}

JSON.stringify()进行深拷贝有弊端:

var obj = {
        a:function(){}, 
        b: undefined, 
        c: null, 
        d: Symbol('s'), 
    }
var cloneObj = JSON.parse(JSON.stringify(obj ))
cloneObj // {c:null}

会忽略value为function, undefind, symbol, 并且在序列化BigInt时会抛出语法错误:TypeError: Do not know how to serialize a BigInt

更多弊端请看本文附录-JSON.parse(JSON.stringfy(对象))弊端

二、函数库lodash

该函数库也有提供_.cloneDeep用来做 Deep Copy

三、手写递归

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

附录-浅拷贝方法

一、手写浅拷贝

// 数组 对象都可以用
const shallowClone = (obj) => {
  const dst = {};
  for (let prop in obj) {
    if (arr.hasOwnProperty(prop)) {
        dst[prop] = obj[prop];
    }
  }
  return dst;
}
  1. for...in:遍历 Object 对象 obj,将可枚举值列举出来。

  1. hasOwnProperty():检查该枚举值是否属于该对象 obj,如果是继承过来的就去掉,如果是自身的则进行拷贝。

二、Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

var obj = {data:{name:'xxx'}}
var newCloneObj = Object.assign({},obj)
obj.data.name ='ddd'
newCloneObj.data.name// ddd

三、ES6的拓展运算符

var obj = {data:{name:'xxx'}}
var newCloneObj = {...obj}
obj.data.name ='ddd'
newCloneObj.data.name  // ddd

四、Object.create()

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

Object.create(proto,[propertiesObject])接收两个参数一个是新创建对象的__proto__, 一个属性列表

let aa = {
    a: undefined, 
    func: function(){console.log(1)}, 
    b:2, 
    c: {x: 'xxx', xx: undefined},
}
let bb = Object.create(aa, Object.getOwnPropertyDescriptors(aa))

五、Array.prototype.concat()

concat() 是数组的一个内置方法,用户合并两个或者多个数组。

这个方法不会改变现有数组,而是返回一个新数组。

const arr1 = [
  1,
  {
    username: 'jsliang',
  },
];

let arr2 = arr1.concat();
arr2[0] = 2;
arr2[1].username = 'LiangJunrong';
console.log(arr1);
// [ 1, { username: 'LiangJunrong' } ]
console.log(arr2);
// [ 2, { username: 'LiangJunrong' } ]

六、Array.prototype.slice()

slice() 也是数组的一个内置方法,该方法会返回一个新的对象。

slice() 不会改变原数组。

const arr1 = [
  1,
  {
    username: 'jsliang',
  },
];

let arr2 = arr1.slice();
arr2[0] = 2;
arr2[1].username = 'LiangJunrong';
console.log(arr1);
// [ 1, { username: 'LiangJunrong' } ]
console.log(arr2);
// [ 2, { username: 'LiangJunrong' } ]

附录-浅拷贝面试题

我们先来看看

const data = {arr:[1,2,3],inner:{arr:[3,4,5]}}
const data2 = shallowCopy(data)
data.arr[0] = 3
data.arr = [0,0,0]

data.arr[0] = 3

继上面的内容,我们先解析一下:

不论变量是存在栈内,还是存在堆里(反正都是在内存里),其结构和存值方式是差不多的,都有如下的结构:

let foo = 1;
let bar = 2;

我们新建data2:

当我们第一次修改源数据data.arr[0]:

相对于我们去修改堆空间里对应内存地址的值。而不是开劈新的内存空间进行存值。

堆空间里基本数据类型的替换将沿用原来的内存地址,如果引用类型的替换则将开辟新的内存地址,等待回收老的内存地址【不一定会回收,因为可能被其他引用】

此时并不会改变data和data2里arr的引用地址。所以data和data2同步关联。

第二次修改源数据data.arr = [0,0,0]:

可以看到的是堆里新开辟了一个空间放引用对象arr的内容。同时解绑data.arr的引用地址,把新空间的地址绑定到data.arr上,而data2.arr由于还是指向0x0206的内存空间,所以旧的内存空间没有被收回。data2.arr未发生改变

再回归到问题本身:

为什么data.inner.arr = [0,0,0]会同时改变data和data2里面的值?

你可以看到,data和data2的inner,即第二层,都是指向同一个内存空间,

当我们去改变inner对象内容里面的赋值的时候,data和data2的inner指向地址不变,只不过里面的arr我们会重新读值。使得两者改动时发生关联。

所以我们说,无论浅拷贝通过什么方法实现,拷贝结果的第一层会是深拷贝(两个内存空间)

附录-JSON.parse(JSON.stringfy(对象))弊端

  • 如果obj里面存在时间对象,JSON.parse(JSON.stringify(obj))之后,时间对象变成了字符串。

  • 如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象。

  • 如果obj里有函数,undefined,则序列化的结果会把函数, undefined丢失。

  • 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null。

  • JSON.stringify()只能序列化对象的可枚举的自有属性。如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor。

  • 如果对象中存在循环引用的情况也无法正确实现深拷贝。

function Person (name) {
    this.name = 20
}

const lili = new Person('lili')

let a = {
    data0: '1',
    date1: [new Date('2020-03-01'), new Date('2020-03-05')],
    data2: new RegExp('\\w+'),
    data3: new Error('1'),
    data4: undefined,
    data5: function () {
        console.log(1)
    },
    data6: NaN,
    data7: lili
}

let b = JSON.parse(JSON.stringify(a))

Supongo que te gusta

Origin blog.csdn.net/weixin_42274805/article/details/129375370
Recomendado
Clasificación