Cópia profunda em ES6 usando a sintaxe de propagação

99

Estou tentando criar um método de mapa de cópia profunda para meu projeto Redux que funcionará com objetos em vez de matrizes. Eu li que no Redux cada estado não deve mudar nada nos 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;
        })

No entanto, ele não copia profundamente os itens internos, então preciso ajustá-lo 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;
    }, {});
}

Isso é menos elegante, pois requer saber quais objetos são passados. Existe uma maneira no ES6 de usar a sintaxe de propagação para copiar um objeto em profundidade?

Cara
fonte
8
Este é um problema XY. Você não deve ter que trabalhar muito em propriedades profundas no redux. em vez disso, você deve apenas criar outro redutor que funcione na fatia filho da forma de estado e, em seguida, usar combineReducerspara compor os dois (ou mais) juntos. Se você usar técnicas de redux idiomática, seu problema de clonagem profunda de objetos desaparece.
Obrigado

Respostas:

72

Essa funcionalidade não está embutida no ES6. Acho que você tem algumas opções, dependendo do que deseja fazer.

Se você realmente deseja copiar em profundidade:

  1. Use uma biblioteca. Por exemplo, lodash tem um cloneDeepmétodo.
  2. Implemente sua própria função de clonagem.

Solução alternativa para o seu problema específico (sem cópia profunda)

No entanto, eu acho que se você estiver disposto a mudar algumas coisas, você pode economizar algum trabalho. Presumo que você controle todos os sites de chamada para sua função.

  1. Especifique que todos os retornos de chamada passados ​​para mapCopydevem retornar novos objetos em vez de transformar o objeto existente. Por exemplo:

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

    Isso é usado Object.assignpara criar um novo objeto, define propriedades edesse novo objeto e, em seguida, define um novo título para esse novo objeto. Isso significa que você nunca altera objetos existentes e apenas cria novos quando necessário.

  2. mapCopy pode ser muito simples agora:

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

Basicamente, mapCopyé confiar que seus chamadores farão a coisa certa. É por isso que eu disse que isso pressupõe que você controle todos os sites de chamadas.

Frank tan
fonte
3
Object.assign não copia profundamente os objetos. consulte developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… - Object.assign () copia valores de propriedade. "Se o valor de origem for uma referência a um objeto, ele apenas copia esse valor de referência."
Greg Somers
Certo. Esta é uma solução alternativa que não envolve cópias profundas. Vou atualizar minha resposta para ser mais explícito sobre isso.
Frank Tan
102

Em vez disso, use isso para cópia 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
fonte
63
Isso só funciona se você não precisar clonar funções. JSON irá ignorar todas as funções para que você não as tenha no clone.
Noland
7
Além das funções, você terá problemas com undefined e null usando este método
James Heazlewood
2
Você também terá problemas com quaisquer classes definidas pelo usuário, uma vez que as cadeias de protótipos não são serializadas.
Patrick Roberts
8
Sua solução usando serialização JSON tem alguns problemas. Ao fazer isso, você perderá qualquer propriedade Javascript que não tenha um tipo equivalente em JSON, como Function ou Infinity. Qualquer propriedade atribuída a undefined será ignorada por JSON.stringify, fazendo com que eles sejam perdidos no objeto clonado. Além disso, alguns objetos são convertidos em strings, como Date, Set, Map e muitos outros.
Jonathan Brizio
2
Eu estava tendo um pesadelo horrível tentando criar uma cópia verdadeira de uma série de objetos - objetos que eram essencialmente valores de dados, sem funções. Se você só precisa se preocupar com isso, essa abordagem funciona perfeitamente.
Charlie
29

De MDN

Nota: A sintaxe de difusão efetivamente atinge um nível de profundidade ao copiar um array. Portanto, pode ser inadequado para copiar matrizes multidimensionais, como mostra o exemplo a seguir (é o mesmo com Object.assign () e sintaxe de propagação).

Pessoalmente, sugiro usar a função cloneDeep de Lodash para clonagem de objetos / matrizes de vários níveis.

Aqui está um exemplo prático:

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
fonte
4
arr6 não está funcionando para mim. No navegador (chrome 59.0 que suporta ES6 eu obtenho Uncaught SyntaxError: token inesperado ... e no nó 8.9.3 que suporta ES7 eu obtenho TypeError: undefined não é uma função em resposta: 1: 22
Achi Even-dar
@ AchiEven-dar não cria por que você cometeu um erro. Você pode executar este código diretamente no stackoverflow pressionando o botão azul Run code snippete ele deve ser executado corretamente.
Mina Luke
3
arr6 não está funcionando para mim também. No navegador - Chrome 65
yehonatan yehezkel
17

Costumo usar isto:

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;
        }, {})
    }
}
HectorGuo
fonte
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.stringifye JSON.parseé a melhor maneira. Porque, ao usar o operador spread, não obteremos a resposta eficiente quando o objeto json contiver outro objeto dentro dele. precisamos especificar isso manualmente.

Shashidhar Reddy
fonte
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
fonte
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
}
user10919042
fonte
Os comentários estão no código para quem procura explicação.
Wookies-Will-Code de
1
const cloneData = (dataArray) => {
    newData= []
    dataArray.forEach((value) => {
        newData.push({...value})
    })
    return newData
}
  • a = [{nome: "siva"}, {nome: "siva1"}];
  • b = myCopy (a)
  • b === a // falso`
Harish Sekar
fonte
1

Eu mesmo cheguei a essas respostas no último dia, tentando encontrar uma maneira de copiar estruturas complexas em profundidade, que podem incluir links recursivos. Como não fiquei satisfeito com nada do que foi sugerido antes, implementei esta roda sozinho. E funciona muito bem. Espero que ajude alguém.

Exemplo de uso:

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

TheClone = OriginalStruct.deep_copy();

Por favor, olhe https://github.com/latitov/JS_DeepCopy para exemplos ao vivo de como usá-lo, e também deep_print () está lá.

Se você precisar disso rápido, aqui está a fonte da função 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;
    }
}

Felicidades@!

Latitov
fonte
1

Aqui está o meu algoritmo de cópia 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;
  };
Бектур Муратов
fonte
Você também precisa verificar se 'obj [prop]! == null' como typeof (null) também retorna 'objeto'
Pramod Mali
0

Aqui está a função deepClone que lida com todos os tipos de dados primitivos, matrizes, objetos e funções

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
fonte