typescript - objeto de clonagem

186

Eu tenho uma classe de super que é o pai ( Entity) para muitos subclasse ( Customer, Product, ProductCategory...)

Eu estou olhando para clonar dinamicamente um objeto que contém subobjetos diferentes no TypeScript.

Por exemplo: um Customerque tem diferente Productquem tem umProductCategory

var cust:Customer  = new Customer ();

cust.name = "someName";
cust.products.push(new Product(someId1));
cust.products.push(new Product(someId2));

Para clonar toda a árvore do objeto, criei uma função no Entity

public clone():any {
    var cloneObj = new this.constructor();
    for (var attribut in this) {
        if(typeof this[attribut] === "object"){
           cloneObj[attribut] = this.clone();
        } else {
           cloneObj[attribut] = this[attribut];
        }
    }
    return cloneObj;
}

O newsobe o seguinte erro quando ele é transpiled para javascript:error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature.

Embora o script funcione, eu gostaria de me livrar do erro transpilado

David Laberge
fonte

Respostas:

255

Resolvendo a questão específica

Você pode usar uma asserção de tipo para informar ao compilador que você conhece melhor:

public clone(): any {
    var cloneObj = new (this.constructor() as any);
    for (var attribut in this) {
        if (typeof this[attribut] === "object") {
            cloneObj[attribut] = this[attribut].clone();
        } else {
            cloneObj[attribut] = this[attribut];
        }
    }
    return cloneObj;
}

Clonagem

Lembre-se de que às vezes é melhor escrever seu próprio mapeamento - em vez de ser totalmente dinâmico. No entanto, existem alguns truques de "clonagem" que você pode usar que oferecem efeitos diferentes.

Usarei o seguinte código para todos os exemplos subseqüentes:

class Example {
  constructor(public type: string) {

  }
}

class Customer {
  constructor(public name: string, public example: Example) {

  }

  greet() {
    return 'Hello ' + this.name;
  }
}

var customer = new Customer('David', new Example('DavidType'));

Opção 1: espalhar

Propriedades: Sim
Métodos: Não
Cópia Profunda: Não

var clone = { ...customer };

alert(clone.name + ' ' + clone.example.type); // David DavidType
//alert(clone.greet()); // Not OK

clone.name = 'Steve';
clone.example.type = 'SteveType';

alert(customer.name + ' ' + customer.example.type); // David SteveType

Opção 2: Object.assign

Propriedades: Sim
Métodos: Não
Cópia Profunda: Não

var clone = Object.assign({}, customer);

alert(clone.name + ' ' + clone.example.type); // David DavidType
alert(clone.greet()); // Not OK, although compiler won't spot it

clone.name = 'Steve';
clone.example.type = 'SteveType';

alert(customer.name + ' ' + customer.example.type); // David SteveType

Opção 3: Object.create

Propriedades: Herdado
Métodos: Herdado
Cópia Profunda: Raso Herdado (mudanças profundas afetam o original e o clone)

var clone = Object.create(customer);

alert(clone.name + ' ' + clone.example.type); // David DavidType
alert(clone.greet()); // OK

customer.name = 'Misha';
customer.example = new Example("MishaType");

// clone sees changes to original 
alert(clone.name + ' ' + clone.example.type); // Misha MishaType

clone.name = 'Steve';
clone.example.type = 'SteveType';

// original sees changes to clone
alert(customer.name + ' ' + customer.example.type); // Misha SteveType

Opção 4: Função de cópia profunda

Propriedades: Sim
Métodos: Não
Cópia Profunda: Sim

function deepCopy(obj) {
    var copy;

    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = deepCopy(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = deepCopy(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

var clone = deepCopy(customer) as Customer;

alert(clone.name + ' ' + clone.example.type); // David DavidType
// alert(clone.greet()); // Not OK - not really a customer

clone.name = 'Steve';
clone.example.type = 'SteveType';

alert(customer.name + ' ' + customer.example.type); // David DavidType
Fenton
fonte
Fechar, o transpile parou de reclamar com o texto datilografado 1.3, mas uma vez em javascript geraria erro. O texto datilografado 1.4.1, não o deixa ir.
David Laberge 05/02
1
Você poderia esclarecer como exatamente usa isso? Eu incluído como um método de meu objeto e, em seguida, tenho um erro dizendo não é uma função ...
megalucio
1
Estou recebendo o seguinte erro: "ERRO TypeError: this.constructor (...) não é um construtor"
michali
3
Você acabou de fazer um exemplo público desse cliente?
Blair Connolly
1
Alguém pode TL; DR para mim, quais das soluções fornecidas em todas as respostas preservam o tipo OO do clone, ou seja cloned instanceof MyClass === true?
Szczepan Hołyszewski 25/01
177

Operador da propagação 1.Use

const obj1 = { param: "value" };
const obj2 = { ...obj1 };

O operador Spread pega todos os campos do obj1 e os espalha sobre o obj2. No resultado, você obtém um novo objeto com nova referência e os mesmos campos que o original.

Lembre-se de que é uma cópia superficial, significa que se o objeto estiver aninhado, seus parâmetros compostos aninhados existirão no novo objeto pela mesma referência.

2.Object.assign ()

const obj1={ param: "value" };
const obj2:any = Object.assign({}, obj1);

Object.assign cria cópia real, mas possui apenas propriedades, portanto, as propriedades no protótipo não existirão no objeto copiado. Também é cópia rasa.


3.Object.create ()

const obj1={ param: "value" };
const obj2:any = Object.create(obj1);

Object.create não está realizando clonagem real , está criando objeto a partir do protótipo. Portanto, use-o se o objeto deve clonar propriedades de tipo primário, porque a atribuição de propriedades de tipo primário não é feita por referência.

As vantagens de Object.create são que quaisquer funções declaradas em protótipo estarão disponíveis em nosso objeto recém-criado.


Poucas coisas sobre cópia superficial

A cópia rasa coloca no novo objeto todos os campos do antigo, mas também significa que, se o objeto original tiver campos de tipo composto (objeto, matrizes etc.), esses campos serão colocados no novo objeto com as mesmas referências. A mutação desse campo no objeto original será refletida no novo objeto.

Talvez pareça uma armadilha, mas realmente a situação em que todo o objeto complexo precisa ser copiado é rara. A cópia rasa reutilizará a maior parte da memória, o que significa que é muito barato em comparação à cópia profunda.


Cópia profunda

O operador de propagação pode ser útil para cópias profundas.

const obj1 = { param: "value", complex: { name: "John"}}
const obj2 = { ...obj1, complex: {...obj1.complex}};

O código acima criou uma cópia profunda do obj1. O campo composto "complexo" também foi copiado para obj2. O campo de mutação "complexo" não refletirá a cópia.

Maciej Sikora
fonte
8
Eu não acho que isso esteja completamente correto. Object.create(obj1)cria um novo objeto e atribui obj1 como o protótipo. Nenhum dos campos no obj1 é copiado ou clonado. Portanto, alterações no obj1 sem modificar o obj2 serão vistas, uma vez que essencialmente não possui propriedades. Se você modificar o obj2 primeiro, o protótipo não será visto para o campo definido, pois o campo do obj2 com o nome está mais próximo na hierarquia.
Ken Rimple
3
Você também verá ES2015 e os desenvolvedores do texto dactilografado fazer isso em vez disso, o que cria um objeto a partir do 1º parâmetro (no meu caso uma vazia) e copia as propriedades do segundo e parâmetros seguintes): let b = Object.assign({}, a);
Ken Rimple
@KenRimple Você está 100% certo, adicionei mais algumas informações.
Maciej Sikora
talvez seja útil => developer.mozilla.org/en/docs/Web/JavaScript/Reference/…
Emmanuel Touzery 16/16
5
Object.assign criará problemas para objetos profundos. Por exemplo {name: 'x', valores: ['a', 'b', 'c']}. Depois de usar Object.assign para clonar, os dois objetos compartilham a matriz de valores, portanto, a atualização de um afeta o outro. Consulte: developer.mozilla.org/en/docs/Web/JavaScript/Reference/… (seção 'Warning for Deep Clone'). Diz: Para clonagem profunda, precisamos usar outras alternativas. Isso ocorre porque Object.assign () copia a referência da propriedade quando a propriedade que está sendo atribuída é um objeto.
Meir
47

Tente o seguinte:

let copy = (JSON.parse(JSON.stringify(objectToCopy)));

É uma boa solução até que você esteja usando objetos muito grandes ou seu objeto tenha propriedades não serializáveis.

Para preservar a segurança do tipo, você pode usar uma função de cópia na classe da qual deseja fazer cópias:

getCopy(): YourClassName{
    return (JSON.parse(JSON.stringify(this)));
}

ou de forma estática:

static createCopy(objectToCopy: YourClassName): YourClassName{
    return (JSON.parse(JSON.stringify(objectToCopy)));
}
Lars
fonte
5
Tudo bem, mas lembre-se de que você perderá as informações do protótipo e todos os tipos não suportados no json ao serializar / analisar.
Stanislav E. Govorov 27/03
1
Além disso, isso parece menos eficiente em comparação com a função deepCopy fornecida acima .
Mojtaba
Eu tenho este erro: "Convertendo estrutura circular em JSON" quando uso "(JSON.parse (JSON.stringify (objectToCopy)));"
Cedric Arnould
Só funciona em 98% dos casos. Pode levar a falta de chaves com undefinedvalor, pelo menos. se objectToCopy = { x : undefined};depois de executar seu código Object.keys(objectToCopy).lengthé 1, enquanto Object.keys(copy).lengthé 0.
Aidin
33

O TypeScript / JavaScript possui seu próprio operador para clonagem superficial:

let shallowClone = { ...original };
Luca C.
fonte
15

É fácil obter uma cópia superficial com o "Object Spread" introduzido no TypeScript 2.1

este TypeScript: let copy = { ...original };

produz este JavaScript:

var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
var copy = __assign({}, original);

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html

Homer
fonte
2
Nota: isso criará uma cópia superficial
Jimmy Kane
11

Para clone profundo serializável, com Informações do tipo é,

export function clone<T>(a: T): T {
  return JSON.parse(JSON.stringify(a));
}
Polv
fonte
1
Isso pode alterar a ordem dos objetos. Apenas um aviso para algumas pessoas. Também não lida com datas corretamente.
Pangamma
Isso pode alterar a ordem dos adereços - tente npmjs.com/package/es6-json-stable-stringify em vez deJSON.stringify
Polv
@Polv, se alguém está confiando na ordem das chaves em um objeto, acho que eles têm um problema maior do que isso clone. :)
Aidin
Esta solução pode perder chaves com undefinedvalor. Veja meu comentário na resposta semelhante acima: stackoverflow.com/questions/28150967/typescript-cloning-object/…
Aidin
7

Minha opinião:

Object.assign(...) apenas copia propriedades e perdemos o protótipo e os métodos.

Object.create(...) não está copiando propriedades para mim e apenas criando um protótipo.

O que funcionou para mim é criar um protótipo usando Object.create(...)e copiando propriedades para ele usando Object.assign(...):

Portanto, para um objeto foo, faça clone assim:

Object.assign(Object.create(foo), foo)
Muhammad Ali
fonte
Há uma coisa muito sutil acontecendo aqui. Você está realmente fazendo fooo pai prototípico do clonedFoo(novo objeto). Embora isso possa parecer bom, lembre-se de que uma propriedade ausente será pesquisada na cadeia de protótipos, portanto, const a = { x: 8 }; const c = Object.assign(Object.create(a), a); delete c.x; console.log(c.x);imprime 8, enquanto deveria ser undefined! (Link REPL: repl.it/repls/CompetitivePreemptiveKeygen )
Aidin
Além disso, se você adicionar uma propriedade posteriormente foo, ela aparecerá automaticamente paraclonedFoo ! por exemplo, foo.y = 9; console.log(clonedFoo.y)será impresso em 9vez de undefined. É muito provável que não seja o que você está pedindo!
Aidin
@Aidin Então, como garantir uma cópia profunda?
Muhammad Ali
Qualquer outra solução nesta pergunta, que é copiada por valor de forma recursiva (por exemplo, stackoverflow.com/a/53025968 por marckassay) assegura que, uma vez que não há referência ao objeto de origem que está sendo mantido no objeto de destino.
Aidin
5

Você também pode ter algo parecido com isto:

class Entity {
    id: number;

    constructor(id: number) {
        this.id = id;
    }

    clone(): this {
        return new (this.constructor as typeof Entity)(this.id) as this;
    }
}

class Customer extends Entity {
    name: string;

    constructor(id: number, name: string) {
        super(id);
        this.name = name;
    }

    clone(): this {
        return new (this.constructor as typeof Customer)(this.id, this.name) as this;
    }
}

Apenas certifique-se de substituir o clonemétodo em todas as Entitysubclasses, caso contrário, você terminará com clones parciais.

O tipo de retorno thissempre corresponderá ao tipo da instância.

Lua da década
fonte
4

Adicione "lodash.clonedeep": "^4.5.0"ao seu package.json. Então use assim:

import * as _ from 'lodash';

...

const copy = _.cloneDeep(original)
user2878850
fonte
3

Se você receber este erro:

TypeError: this.constructor(...) is not a function

Este é o script correto:

public clone(): any {
    var cloneObj = new (<any>this.constructor)(); // line fixed
    for (var attribut in this) {
        if (typeof this[attribut] === "object") {
            cloneObj[attribut] = this[attribut].clone();
        } else {
            cloneObj[attribut] = this[attribut];
        }
    }
    return cloneObj;
}
pablorsk
fonte
4
Está correto cloneObj[attribut] = this.clone();? ou você quer dizer #cloneObj[attribut] = this[attribut].clone();
Serginho
2

Eu me deparei com esse problema e, no final, escrevi uma pequena biblioteca cloneable-ts que fornece uma classe abstrata, que adiciona um método clone a qualquer classe que o estende. A classe abstrata empresta a Função de Cópia Profunda descrita na resposta aceita por Fenton, substituindo apenas copy = {};por copy = Object.create(originalObj)para preservar a classe do objeto original. Aqui está um exemplo de uso da classe

import {Cloneable, CloneableArgs} from 'cloneable-ts';

// Interface that will be used as named arguments to initialize and clone an object
interface PersonArgs {
    readonly name: string;
    readonly age: number;
}

// Cloneable abstract class initializes the object with super method and adds the clone method
// CloneableArgs interface ensures that all properties defined in the argument interface are defined in class
class Person extends Cloneable<TestArgs>  implements CloneableArgs<PersonArgs> {
    readonly name: string;
    readonly age: number;

    constructor(args: TestArgs) {
        super(args);
    }
}

const a = new Person({name: 'Alice', age: 28});
const b = a.clone({name: 'Bob'})
a.name // Alice
b.name // Bob
b.age // 28

Ou você pode simplesmente usar o Cloneable.clonemétodo auxiliar:

import {Cloneable} from 'cloneable-ts';

interface Person {
    readonly name: string;
    readonly age: number;
}

const a: Person = {name: 'Alice', age: 28};
const b = Cloneable.clone(a, {name: 'Bob'})
a.name // Alice
b.name // Bob
b.age // 28    
Tim Osadchiy
fonte
2

Aqui está o meu mash-up! E aqui está um link StackBlitz para ele. Atualmente, ele está limitado a apenas copiar tipos simples e tipos de objetos, mas poderia ser modificado facilmente, eu acho.

   let deepClone = <T>(source: T): { [k: string]: any } => {
      let results: { [k: string]: any } = {};
      for (let P in source) {
        if (typeof source[P] === 'object') {
          results[P] = deepClone(source[P]);
        } else {
          results[P] = source[P];
        }
      }
      return results;
    };
Marckassay
fonte
1
Funciona muito bem, tanto quanto eu posso ver. No entanto, typeof nulltambém é um objeto, portanto, a consulta deve ser if (source[P] !== null && typeof source[P] === 'object'). Caso contrário, seus valores nulos serão transformados em um objeto vazio.
MortenMoulder
1

Desde que o TypeScript 3.7 é lançado, os aliases de tipo recursivo agora são suportados e isso nos permite definir uma deepCopy()função de segurança de tipo :

// DeepCopy type can be easily extended by other types,
// like Set & Map if the implementation supports them.
type DeepCopy<T> =
    T extends undefined | null | boolean | string | number ? T :
    T extends Function | Set<any> | Map<any, any> ? unknown :
    T extends ReadonlyArray<infer U> ? Array<DeepCopy<U>> :
    { [K in keyof T]: DeepCopy<T[K]> };

function deepCopy<T>(obj: T): DeepCopy<T> {
    // implementation doesn't matter, just use the simplest
    return JSON.parse(JSON.stringify(obj));
}

interface User {
    name: string,
    achievements: readonly string[],
    extras?: {
        city: string;
    }
}

type UncopiableUser = User & {
    delete: () => void
};

declare const user: User;
const userCopy: User = deepCopy(user); // no errors

declare const uncopiableUser: UncopiableUser;
const uncopiableUserCopy: UncopiableUser = deepCopy(uncopiableUser); // compile time error

Parque infantil

Valeriy Katkov
fonte
0

Para um clone simples do conteúdo do objeto de furo, simplesmente codifico e analiso a instância:

let cloneObject = JSON.parse(JSON.stringify(objectToClone))

Enquanto eu altero os dados na árvore objectToClone, não há alterações no cloneObject. Esse foi o meu requisito.

Espero que ajude

Ferhatos
fonte
1
Pode perder chaves com undefinedvalor. Veja meu comentário na resposta semelhante acima: stackoverflow.com/questions/28150967/typescript-cloning-object/…
Aidin
0

Acabei fazendo:

public clone(): any {
  const result = new (<any>this.constructor);

  // some deserialization code I hade in place already...
  // which deep copies all serialized properties of the
  // object graph
  // result.deserialize(this)

  // you could use any of the usggestions in the other answers to
  // copy over all the desired fields / properties

  return result;
}

Porque:

var cloneObj = new (<any>this.constructor());

do @Fenton deu erros de tempo de execução.

Versão datilografada: 2.4.2

Bernoulli IT
fonte
0

Que tal o bom e velho jQuery ?! Aqui está o clone profundo:

var clone = $.extend(true, {}, sourceObject);
alehro
fonte
Esta pergunta não foi marcada como JQuery nem foi mencionada na pergunta. Também seria uma sobrecarga enorme incluir o JQuery em um projeto apenas para criar um clone profundo.
LewisM
Isso é justo o suficiente, mas o OP não é sobre como clonar, é sobre identificar um problema no código que ele forneceu e você respondeu com a maneira de clonar jQuery sem realmente responder à pergunta. Não fui eu quem votou contra você, mas acredito que talvez seja por isso que você foi votado.
precisa saber é o seguinte
0

Fiz uma facada na criação de um serviço genérico de cópia / clone que retém tipos para objetos aninhados. Gostaria de receber feedback se estou fazendo algo errado, mas parece funcionar até agora ...

import { Injectable } from '@angular/core';

@Injectable()
export class CopyService {

  public deepCopy<T>(objectToClone: T): T {
    // If it's a simple type or null, just return it.
    if (typeof objectToClone === 'string' ||
      typeof objectToClone === 'number' ||
      typeof objectToClone === 'undefined' ||
      typeof objectToClone === 'symbol' ||
      typeof objectToClone === 'function' ||
      typeof objectToClone === 'boolean' ||
      objectToClone === null
    ) {
      return objectToClone;
    }

    // Otherwise, check if it has a constructor we can use to properly instantiate it...
    let ctor = Object.getPrototypeOf(objectToClone).constructor;
    if (ctor) {
      let clone = new ctor();

      // Once we've instantiated the correct type, assign the child properties with deep copies of the values
      Object.keys(objectToClone).forEach(key => {
        if (Array.isArray(objectToClone[key]))
          clone[key] = objectToClone[key].map(item => this.deepCopy(item));
        else
          clone[key] = this.deepCopy(objectToClone[key]);
      });

      if (JSON.stringify(objectToClone) !== JSON.stringify(clone))
        console.warn('object cloned, but doesnt match exactly...\nobject: ' + JSON.stringify(objectToClone) + "\nclone: " + JSON.stringify(clone))

      // return our cloned object...
      return clone;
    }
    else {
      //not sure this will ever get hit, but figured I'd have a catch call.
      console.log('deep copy found something it didnt know: ' + JSON.stringify(objectToClone));
      return objectToClone;
    }
  }
}
patrickbadley
fonte
0

No typeScript eu testei com angular, e está indo bem

deepCopy(obj) {


        var copy;

        // Handle the 3 simple types, and null or undefined
        if (null == obj || "object" != typeof obj) return obj;

        // Handle Date
        if (obj instanceof Date) {
            copy = new Date();
            copy.setTime(obj.getTime());
            return copy;
        }

        // Handle Array
        if (obj instanceof Array) {
            copy = [];
            for (var i = 0, len = obj.length; i < len; i++) {
                copy[i] = this.deepCopy(obj[i]);
            }
            return copy;
        }

        // Handle Object
        if (obj instanceof Object) {
            copy = {};
            for (var attr in obj) {
                if (obj.hasOwnProperty(attr)) copy[attr] = this.deepCopy(obj[attr]);
            }
            return copy;
        }

        throw new Error("Unable to copy obj! Its type isn't supported.");
    }
maty jimenez
fonte
0

Para clonagem profunda de um objeto que pode conter outros objetos, matrizes e assim por diante, eu uso:

const clone = <T>(source: T): T => {
  if (source === null) return source

  if (source instanceof Date) return new Date(source.getTime()) as any

  if (source instanceof Array) return source.map((item: any) => clone<any>(item)) as any

  if (typeof source === 'object' && source !== {}) {
    const clonnedObj = { ...(source as { [key: string]: any }) } as { [key: string]: any }
    Object.keys(clonnedObj).forEach(prop => {
      clonnedObj[prop] = clone<any>(clonnedObj[prop])
    })

    return clonnedObj as T
  }

  return source
}

Usar:

const obj = {a: [1,2], b: 's', c: () => { return 'h'; }, d: null, e: {a:['x'] }}
const objClone = clone(obj)
RTW
fonte
0

Você pode usar a atribuição de desestruturação com sintaxe de propagação :

var obj = {id = 1, name = 'product1'};
var clonedObject = {...obj};
SOUVIK SAHA
fonte
1
Embora esse código possa responder à pergunta, fornecer um contexto adicional sobre como e / ou por que resolve o problema melhoraria o valor a longo prazo da resposta.
leopal 18/02
-2

Se você já possui o objeto de destino, não deseja criá-lo novamente (como se estivesse atualizando uma matriz), deverá copiar as propriedades.
Se você fez dessa maneira:

Object.keys(source).forEach((key) => {
    copy[key] = source[key]
})

Louvor é devido. (veja o título "versão 2")

LosManos
fonte
Funções? Matrizes? Objetos de data? Preservação de tipos? E, claro, e os objetos? Se a função acima encontrar algum dos tipos acima, ela falhará ao clonar profundamente. Você terá copiado as referências para os mesmos dados. Quando eles editarem as propriedades filho do objeto clonado, eles também editarão o objeto original.
Pangamma