Para comprender realmente la copia profunda y superficial, debe dominar los conceptos básicos de asignación, almacenamiento de objetos en la memoria y tipos de datos.
Para comprender mejor la copia profunda y superficial, primero veamos el tipo de datos y la forma de almacenamiento en la memoria.
1. Tipos de datos de JavaScript
tipo primitivo
Null
: Contiene un solo valor:null
Undefined
: Contiene un solo valor:undefined
Boolean
: Contiene dos valores:true
yfalse
Number
: Entero o float, y algunos valores especiales (-Infinity
,+Infinity
,NaN
)String
: una secuencia de caracteres que representan un valor de textoSymbol
: un tipo de datos cuya instancia es única e inmutable
tipo de objeto
Object
: No es demasiado para clasificar por sí mismo, a excepción de los objetos de uso común, matriz, función, etc., todos son objetos especiales.
2. La diferencia entre tipos primitivos y tipos de objetos
1. Tipos primitivos
En JavaScript, cada variable requiere un espacio de memoria para almacenar.
El espacio de memoria se divide en dos tipos: memoria de pila y memoria de pila.
El valor del tipo primitivo en JavaScript se almacena directamente en la pila, y cuando se define la variable, la pila le asigna espacio de memoria.
Características de la memoria de pila:
- Los valores almacenados son de tamaño fijo
- menos espacio
- Puede operar directamente las variables guardadas, y la eficiencia de operación es alta
- El espacio de almacenamiento es asignado automáticamente por el sistema
La característica de los tipos primitivos es la inmutabilidad, es decir, el valor en sí no se puede cambiar.
var str = 'JingYu';
str.slice(1);
str.substr(1);
str.trim(1);
str.toLowerCase(1);
str[0] = 1;
console.log(str); // JingYu
Hay un caso especial:
str += '6'
console.log(str); // JingYu6
¿Esta situación no satisface la inmutabilidad? No, en el código anterior, hemos realizado la operación de str += '6', de hecho, se ha abierto otro espacio de memoria en la pila para almacenar 'JingYu6', y luego la variable str apunta a este espacio, entonces Esto no viola las características de inmutabilidad.
2. Tipo de referencia
El valor del tipo de referencia (tipo de objeto) en JavaScript en realidad se almacena directamente en la memoria del montón, y solo se almacena una dirección de longitud fija en la memoria de la pila, que apunta al valor en la memoria del montón.
Características de la memoria del montón:
- El valor almacenado es de tamaño variable y se puede ajustar dinámicamente
- Gran espacio, baja eficiencia operativa
- No se puede manipular directamente su almacenamiento interno, use la dirección de referencia para leer
- Asignar espacio a través del código
El tipo de referencia no tiene las características de inmutabilidad, podemos cambiarlo fácilmente, especialmente la matriz tiene muchas funciones para cambiar:
pop()
Elimine el último elemento de la matriz, si la matriz está vacía, no cambie la matriz, devuelva indefinido, cambie la matriz original, devuelva el elemento eliminadopush()
Agregue uno o más elementos al final de la matriz, cambie la matriz original y devuelva la longitud de la nueva matrizshift()
Elimine el primer elemento de la matriz, si la matriz está vacía, no realice ninguna operación, devuelva indefinido, cambie la matriz original y devuelva el valor del primer elementounshift()
Agrega uno o más elementos al comienzo de una matriz, modifica la matriz original y devuelve la longitud de la nueva matrizreverse()
Invierta el orden de los elementos en la matriz, cambie la matriz original y devuelva la matrizsort()
Ordene los elementos de la matriz, cambie la matriz original y devuelva la matrizsplice()
Agregar/eliminar elementos de una matriz, mutar la matriz original y devolver el elemento eliminado
3. copiar
Cuando copiamos una variable a otra variable, los tipos primitivos y los tipos de referencia se comportan de manera diferente.
tipo primitivo:
var name = 'JingYu';
var name2 = name;
name2 = 'JINGYU';
console.log(name); // JingYu;
Se puede ver a partir de los resultados de salida que cuando modificamos el resultado de name2, no tiene efecto en name. Eso es porque cuando copiamos name
la variable a name2
, creamos un nuevo espacio de memoria en el espacio de memoria de la pila. Este espacio de memoria almacena el name2
valor de la variable. El valor name
es el mismo que la variable pero la dirección del espacio de memoria es completamente diferente. Entonces el name2
valor modificado name
permanece sin cambios.
Tipo de referencia:
var obj = {
name:'JingYu'};
var obj2 = obj;
obj2.name = 'JINGYU';
console.log(obj.name); // JINGYU
Se sorprenderá al descubrir que el resultado que se muestra es diferente del tipo original. ¿Esta es la razón por?
Esto se debe a que cuando copiamos una variable de tipo de referencia, en realidad copiamos la dirección almacenada en la pila, por lo que el obj2 copiado es en realidad el mismo objeto en el montón al que apunta obj. Por tanto, si cambiamos el valor de alguna de las variables, la otra variable se verá afectada, por lo que existen copias profundas y superficiales.
4. Compara
Mire el código primero, adivine el resultado de la ejecución...
var name = 'JingYu';
var name2 = 'JingYu';
console.log(name === name2); // true
var obj = {
name:'JingYu'};
var obj2 = {
name:'JingYu'};
console.log(obj === obj2); // false
Para los tipos primitivos, sus valores se comparan directamente al comparar, y se devuelve verdadero si los valores son iguales.
Para los tipos de referencia, sus direcciones de referencia se compararán durante la comparación. Aunque los atributos de los objetos almacenados en el montón por las dos variables son iguales, se almacenan en diferentes espacios de almacenamiento, por lo que el valor de comparación es falso.
5. Pasar por valor
Primero dé una conclusión: los parámetros de todas las funciones en ECMAScript se pasan por valor.
No hay paso por referencia en ECMAScript.
let name = 'JingYu';
function changeValue(name){
name = 'JINGYU';
}
changeValue(name);
console.log(name);
Obviamente, el resultado de la ejecución anterior es 'JingYu', es decir, el parámetro de la función es solo una variable local copiada por la variable entrante, y cambiar esta variable local no afectará la variable externa.
let obj = {
name:'JingYu'};
function changeValue(obj){
obj.name = 'JINGYU';
}
changeValue(obj);
console.log(obj.name); //JINGYU
Cuando el parámetro de función es un tipo de referencia, también copiamos una copia del parámetro a la variable local, pero la copia copiada apunta a la dirección en la memoria del montón. Operamos en las propiedades del objeto dentro de la función, que en realidad es diferentes de las variables externas apuntan al mismo valor en la memoria del montón, pero esto no implica pasar por referencia.
let obj = {
};
function changeValue(obj){
obj.name = 'JingYu';
obj = {
name:'JINGYU'};
}
changeValue(obj);
console.log(obj.name); // JingYu
obj = {name:'JINGYU'};
Este es solo un objeto local dentro de la función.
3. Copia superficial
Hemos introducido los conocimientos básicos anteriormente. Ahora a nuestro tema.
concepto
La copia superficial se refiere a la creación de nuevos datos que tienen una copia exacta de los valores de atributo de los datos originales.
Si el atributo es un tipo primitivo, se copia el valor del tipo primitivo. Si el atributo es un tipo de referencia, se copia la dirección de memoria,
es decir, una copia superficial es una copia de capa y un tipo de referencia profunda comparte la dirección de memoria.
Implementación
copia superficial simple
function shallowClone(obj) {
const newObj = {
};
for(let prop in obj) {
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}
En JavaScript, hay otros tres fenómenos de copia superficial:
Object.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);
Array.prototype.slice()
,Array.prototype.concat()
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"]
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
Copia usando el operador de extensión
const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
Cuatro, copia profunda
concepto
La copia profunda crea una nueva pila. Las propiedades de los dos objetos son las mismas, pero corresponden a dos direcciones diferentes. La modificación de las propiedades de un objeto no cambiará las propiedades del otro objeto. Métodos comunes de copia profunda
:
_.cloneDeep()
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
jQuery.extend()
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
JSON.stringify()
const obj2=JSON.parse(JSON.stringify(obj1));
Pero este método tiene desventajas, ignorará undefined, símbolo y función
const obj = {
name: 'A',
name1: undefined,
name3: function() {
},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
recursividad de bucle escrito a mano
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;
}
Cinco, la diferencia entre copia superficial, copia profunda y asignación
En primer lugar, con la ayuda de dos imágenes, puede ver más claramente la diferencia entre la copia superficial y la copia profunda.
De la figura anterior, se encuentra que tanto la copia superficial como la copia profunda crean un nuevo objeto, pero cuando se copian las propiedades del objeto, el comportamiento es diferente. La
copia superficial solo copia el puntero de la propiedad a un objeto, no el objeto en sí. compartiendo la misma memoria, la modificación de las propiedades del objeto afectará al objeto original
// 浅拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj3=shallowClone(obj1) // 一个浅拷贝方法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
Pero la copia profunda creará otro exactamente el mismo objeto, el nuevo objeto no comparte memoria con el objeto original, y la modificación del nuevo objeto no cambiará el objeto original.
// 深拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
copia superficial y asignación
asignación
var obj1={
name:'张三',
age:18,
class:['一班']
}
var obj2=obj1//进行了赋值的操作
obj2.name='李四'
obj2.class[0]='二班'
console.log(obj1)
console.log(obj2)
Se puede ver en el ejemplo que el objeto obj2 asignado cambia, y el valor del objeto original obj1 también cambia.Esto se debe a que al objeto obj2 asignado se le asigna la dirección de memoria de pila del objeto obj1 original, y apuntan a la misma datos de la memoria del montón Por lo tanto, operar en los datos del objeto obj2 asignado cambiará los datos en la memoria del montón pública, por lo que el valor del objeto original también cambiará.
copia superficial
var obj1={
name:'张三',
age:18,
class:['一班']
}
function qianCopy(obj){
var obj2={
}
for(var attr in obj){
//循环对象的所有属性值
if(obj.hasOwnProperty(attr)){
obj2[attr]=obj1[attr]
}
}
return obj2
}
var obj3=qianCopy(obj1)
obj3.name='李四'
obj3.age = 20
obj3.class[0]='二·班'
console.log(obj1)
console.log(obj3)
Se puede ver a partir de los resultados que obj3 cambió el nombre del valor del tipo básico, pero no cambió el nombre del objeto original obj1, y obj3 cambió el valor del tipo de referencia, lo que provocó que el valor del objeto original cambiara
6. Resumen
La asignación es una copia completa. Al asignar un objeto a otro objeto, simplemente copia la dirección de memoria de un objeto en la pila a otro objeto. Si se cambia el valor de la propiedad de un objeto, el valor de la propiedad del otro objeto también cambiará. .
La copia superficial es copiar una capa. Cuando el atributo es un objeto, la copia superficial es copiar. Lo que se copia es la dirección de memoria del objeto. Dos objetos apuntan a la misma dirección. La copia profunda es una capa profunda de copia recursiva. Cuando el atributo es un objeto, la copia profunda es
una pila recién abierta, dos objetos apuntan a direcciones diferentes