Copia profunda en ES6 usando la sintaxis de propagación

99

Estoy tratando de crear un método de mapa de copia profunda para mi proyecto Redux que funcionará con objetos en lugar de matrices. Leí que en Redux cada estado no debería cambiar nada en los estados anteriores.

export const mapCopy = (object, callback) => {
    return Object.keys(object).reduce(function (output, key) {

    output[key] = callback.call(this, {...object[key]});

    return output;
    }, {});
}

Funciona:

    return mapCopy(state, e => {

            if (e.id === action.id) {
                 e.title = 'new item';
            }

            return e;
        })

Sin embargo, no copia en profundidad los elementos internos, por lo que necesito modificarlo para:

export const mapCopy = (object, callback) => {
    return Object.keys(object).reduce(function (output, key) {

    let newObject = {...object[key]};
    newObject.style = {...newObject.style};
    newObject.data = {...newObject.data};

    output[key] = callback.call(this, newObject);

    return output;
    }, {});
}

Esto es menos elegante ya que requiere saber qué objetos se pasan. ¿Hay alguna forma en ES6 de usar la sintaxis de propagación para copiar en profundidad un objeto?

Chico
fuente
8
Este es un problema XY. No debería tener que trabajar mucho en propiedades profundas en redux. en su lugar, debe crear otro reductor que funcione en el segmento secundario de la forma del estado y luego usarlo combineReducerspara componer los dos (o más) juntos. Si utiliza técnicas idiomáticas de reducción, su problema de clonación profunda de objetos desaparece.
Gracias

Respuestas:

72

Esta funcionalidad no está integrada en ES6. Creo que tienes un par de opciones dependiendo de lo que quieras hacer.

Si realmente desea realizar una copia profunda:

  1. Usa una biblioteca. Por ejemplo, lodash tiene un cloneDeepmétodo.
  2. Implemente su propia función de clonación.

Solución alternativa a su problema específico (sin texto completo)

Sin embargo, creo que si estás dispuesto a cambiar un par de cosas, puedes ahorrarte algo de trabajo. Supongo que controlas todos los sitios de llamadas a tu función.

  1. Especifique que todas las devoluciones de llamada pasadas a mapCopydeben devolver nuevos objetos en lugar de mutar el objeto existente. Por ejemplo:

    mapCopy(state, e => {
      if (e.id === action.id) {
        return Object.assign({}, e, {
          title: 'new item'
        });
      } else {  
        return e;
      }
    });

    Esto hace uso de Object.assignpara crear un nuevo objeto, establece las propiedades de eese nuevo objeto y luego establece un nuevo título en ese nuevo objeto. Esto significa que nunca mutará objetos existentes y solo creará nuevos cuando sea necesario.

  2. mapCopy puede ser realmente simple ahora:

    export const mapCopy = (object, callback) => {
      return Object.keys(object).reduce(function (output, key) {
        output[key] = callback.call(this, object[key]);
        return output;
      }, {});
    }

Esencialmente, mapCopyconfía en que quienes llaman hagan lo correcto. Es por eso que dije que esto asume que usted controla todos los sitios de llamadas.

Frank Tan
fuente
3
Object.assign no realiza copias profundas de objetos. consulte developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… - Object.assign () copia los valores de propiedad. "Si el valor de origen es una referencia a un objeto, solo copia ese valor de referencia".
Greg Somers
Correcto. Esta es una solución alternativa que no implica una copia profunda. Actualizaré mi respuesta para ser más explícito al respecto.
Frank Tan
104

En su lugar, use esto para una copia profunda

var newObject = JSON.parse(JSON.stringify(oldObject))

var oldObject = {
  name: 'A',
  address: {
    street: 'Station Road',
    city: 'Pune'
  }
}
var newObject = JSON.parse(JSON.stringify(oldObject));

newObject.address.city = 'Delhi';
console.log('newObject');
console.log(newObject);
console.log('oldObject');
console.log(oldObject);

Nikhil Mahirrao
fuente
64
Esto solo funciona si no necesita clonar funciones. JSON ignorará todas las funciones para que no las tenga en el clon.
Noland
7
Aparte de las funciones, tendrá problemas con undefined y null usando este método
James Heazlewood
2
También tendrá problemas con las clases definidas por el usuario, ya que las cadenas de prototipos no se serializan.
Patrick Roberts
8
Su solución que utiliza la serialización JSON tiene algunos problemas. Al hacer esto, perderá cualquier propiedad de Javascript que no tenga un tipo equivalente en JSON, como Function o Infinity. JSON.stringify ignorará cualquier propiedad asignada a undefined, lo que hará que se pierdan en el objeto clonado. Además, algunos objetos se convierten en cadenas, como Fecha, Conjunto, Mapa y muchos otros.
Jonathan Brizio
2
Estaba teniendo una pesadilla horrible al tratar de crear una copia fiel de una matriz de objetos, objetos que eran esencialmente valores de datos, sin funciones. Si eso es todo de lo que tiene que preocuparse, entonces este enfoque funciona a la perfección.
Charlie
29

De MDN

Nota: La sintaxis de propagación llega efectivamente a un nivel de profundidad al copiar una matriz. Por lo tanto, puede que no sea adecuado para copiar matrices multidimensionales como muestra el siguiente ejemplo (es lo mismo con Object.assign () y sintaxis de propagación).

Personalmente, sugiero usar la función cloneDeep de Lodash para la clonación de objetos / matrices de varios niveles.

Aquí hay un ejemplo práctico:

const arr1 = [{ 'a': 1 }];

const arr2 = [...arr1];

const arr3 = _.clone(arr1);

const arr4 = arr1.slice();

const arr5 = _.cloneDeep(arr1);

const arr6 = [...{...arr1}]; // a bit ugly syntax but it is working!


// first level
console.log(arr1 === arr2); // false
console.log(arr1 === arr3); // false
console.log(arr1 === arr4); // false
console.log(arr1 === arr5); // false
console.log(arr1 === arr6); // false

// second level
console.log(arr1[0] === arr2[0]); // true
console.log(arr1[0] === arr3[0]); // true
console.log(arr1[0] === arr4[0]); // true
console.log(arr1[0] === arr5[0]); // false
console.log(arr1[0] === arr6[0]); // false
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>

Mina Luke
fuente
4
arr6 no me funciona. En el navegador (Chrome 59.0 que admite ES6 obtengo SyntaxError no detectado: token inesperado ... y en el nodo 8.9.3 que admite ES7 obtengo TypeError: undefined no es una función en la
Achi Even-dar
@ AchiEven-dar no sire por qué tiene un error. Puede ejecutar este código directamente en stackoverflow presionando el botón azul Run code snippety debería ejecutarse correctamente.
Mina Luke
3
arr6 no funciona para mí también. En el navegador - chrome 65
yehonatan yehezkel
18

A menudo uso esto:

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

    if(obj instanceof Date) {
        return new Date(obj.getTime());
    }

    if(obj instanceof Array) {
        return obj.reduce((arr, item, i) => {
            arr[i] = deepCopy(item);
            return arr;
        }, []);
    }

    if(obj instanceof Object) {
        return Object.keys(obj).reduce((newObj, key) => {
            newObj[key] = deepCopy(obj[key]);
            return newObj;
        }, {})
    }
}
HéctorGuo
fuente
3
const a = {
  foods: {
    dinner: 'Pasta'
  }
}
let b = JSON.parse(JSON.stringify(a))
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

Usar JSON.stringifyy JSON.parsees la mejor manera. Porque al usar el operador de propagación no obtendremos la respuesta eficiente cuando el objeto json contiene otro objeto dentro de él. necesitamos especificar eso manualmente.

Shashidhar Reddy
fuente
1
function deepclone(obj) {
    let newObj = {};

    if (typeof obj === 'object') {
        for (let key in obj) {
            let property = obj[key],
                type = typeof property;
            switch (type) {
                case 'object':
                    if( Object.prototype.toString.call( property ) === '[object Array]' ) {
                        newObj[key] = [];
                        for (let item of property) {
                            newObj[key].push(this.deepclone(item))
                        }
                    } else {
                        newObj[key] = deepclone(property);
                    }
                    break;
                default:
                    newObj[key] = property;
                    break;

            }
        }
        return newObj
    } else {
        return obj;
    }
}
Jeroen Breen
fuente
1
// use: clone( <thing to copy> ) returns <new copy>
// untested use at own risk
function clone(o, m){
  // return non object values
  if('object' !==typeof o) return o
  // m: a map of old refs to new object refs to stop recursion
  if('object' !==typeof m || null ===m) m =new WeakMap()
  var n =m.get(o)
  if('undefined' !==typeof n) return n
  // shallow/leaf clone object
  var c =Object.getPrototypeOf(o).constructor
  // TODO: specialize copies for expected built in types i.e. Date etc
  switch(c) {
    // shouldn't be copied, keep reference
    case Boolean:
    case Error:
    case Function:
    case Number:
    case Promise:
    case String:
    case Symbol:
    case WeakMap:
    case WeakSet:
      n =o
      break;
    // array like/collection objects
    case Array:
      m.set(o, n =o.slice(0))
      // recursive copy for child objects
      n.forEach(function(v,i){
        if('object' ===typeof v) n[i] =clone(v, m)
      });
      break;
    case ArrayBuffer:
      m.set(o, n =o.slice(0))
      break;
    case DataView:
      m.set(o, n =new (c)(clone(o.buffer, m), o.byteOffset, o.byteLength))
      break;
    case Map:
    case Set:
      m.set(o, n =new (c)(clone(Array.from(o.entries()), m)))
      break;
    case Int8Array:
    case Uint8Array:
    case Uint8ClampedArray:
    case Int16Array:
    case Uint16Array:
    case Int32Array:
    case Uint32Array:
    case Float32Array:
    case Float64Array:
      m.set(o, n =new (c)(clone(o.buffer, m), o.byteOffset, o.length))
      break;
    // use built in copy constructor
    case Date:
    case RegExp:
      m.set(o, n =new (c)(o))
      break;
    // fallback generic object copy
    default:
      m.set(o, n =Object.assign(new (c)(), o))
      // recursive copy for child objects
      for(c in n) if('object' ===typeof n[c]) n[c] =clone(n[c], m)
  }
  return n
}
usuario10919042
fuente
Los comentarios están en el código para aquellos que buscan una explicación.
Wookies-Will-Code
1
const cloneData = (dataArray) => {
    newData= []
    dataArray.forEach((value) => {
        newData.push({...value})
    })
    return newData
}
  • a = [{nombre: "siva"}, {nombre: "siva1"}];
  • b = myCopy (a)
  • b === a // falso`
Harish Sekar
fuente
1

Yo mismo encontré estas respuestas el día pasado, tratando de encontrar una manera de copiar en profundidad estructuras complejas, que pueden incluir enlaces recursivos. Como no estaba satisfecho con nada de lo que se sugirió antes, implementé esta rueda yo mismo. Y funciona bastante bien. Espero que ayude a alguien.

Uso de ejemplo:

OriginalStruct.deep_copy = deep_copy; // attach the function as a method

TheClone = OriginalStruct.deep_copy();

Consulte https://github.com/latitov/JS_DeepCopy para ver ejemplos en vivo de cómo usarlo, y también deep_print () está allí.

Si lo necesita rápido, aquí está la fuente de la función deep_copy ():

function deep_copy() {
    'use strict';   // required for undef test of 'this' below

    // Copyright (c) 2019, Leonid Titov, Mentions Highly Appreciated.

    var id_cnt = 1;
    var all_old_objects = {};
    var all_new_objects = {};
    var root_obj = this;

    if (root_obj === undefined) {
        console.log(`deep_copy() error: wrong call context`);
        return;
    }

    var new_obj = copy_obj(root_obj);

    for (var id in all_old_objects) {
        delete all_old_objects[id].__temp_id;
    }

    return new_obj;
    //

    function copy_obj(o) {
        var new_obj = {};
        if (o.__temp_id === undefined) {
            o.__temp_id = id_cnt;
            all_old_objects[id_cnt] = o;
            all_new_objects[id_cnt] = new_obj;
            id_cnt ++;

            for (var prop in o) {
                if (o[prop] instanceof Array) {
                    new_obj[prop] = copy_array(o[prop]);
                }
                else if (o[prop] instanceof Object) {
                    new_obj[prop] = copy_obj(o[prop]);
                }
                else if (prop === '__temp_id') {
                    continue;
                }
                else {
                    new_obj[prop] = o[prop];
                }
            }
        }
        else {
            new_obj = all_new_objects[o.__temp_id];
        }
        return new_obj;
    }
    function copy_array(a) {
        var new_array = [];
        if (a.__temp_id === undefined) {
            a.__temp_id = id_cnt;
            all_old_objects[id_cnt] = a;
            all_new_objects[id_cnt] = new_array;
            id_cnt ++;

            a.forEach((v,i) => {
                if (v instanceof Array) {
                    new_array[i] = copy_array(v);
                }
                else if (v instanceof Object) {
                    new_array[i] = copy_object(v);
                }
                else {
                    new_array[i] = v;
                }
            });
        }
        else {
            new_array = all_new_objects[a.__temp_id];
        }
        return new_array;
    }
}

Salud@!

latitov
fuente
1

Aquí está mi algoritmo de copia profunda.

const DeepClone = (obj) => {
     if(obj===null||typeof(obj)!=='object')return null;
    let newObj = { ...obj };

    for (let prop in obj) {
      if (
        typeof obj[prop] === "object" ||
        typeof obj[prop] === "function"
      ) {
        newObj[prop] = DeepClone(obj[prop]);
      }
    }

    return newObj;
  };
Бектур Муратов
fuente
También debe verificar si 'obj [prop]! == null' como typeof (null) también devuelve 'object'
Pramod Mali
0

Aquí está la función deepClone que maneja todos los tipos de datos primitivos, matrices, objetos y funciones

function deepClone(obj){
	if(Array.isArray(obj)){
		var arr = [];
		for (var i = 0; i < obj.length; i++) {
			arr[i] = deepClone(obj[i]);
		}
		return arr;
	}

	if(typeof(obj) == "object"){
		var cloned = {};
		for(let key in obj){
			cloned[key] = deepClone(obj[key])
		}
		return cloned;	
	}
	return obj;
}

console.log( deepClone(1) )

console.log( deepClone('abc') )

console.log( deepClone([1,2]) )

console.log( deepClone({a: 'abc', b: 'def'}) )

console.log( deepClone({
  a: 'a',
  num: 123,
  func: function(){'hello'},
  arr: [[1,2,3,[4,5]], 'def'],
  obj: {
    one: {
      two: {
        three: 3
      }
    }
  }
}) ) 

Ganesh Phirke
fuente