dattiloscritto - oggetto di clonazione


186

Ho una classe super-che è il padre ( Entity) per molti sottoclasse ( Customer, Product, ProductCategory...)

Sto cercando di clonare dinamicamente un oggetto che contiene diversi oggetti secondari in Typescript.

Ad esempio: a Customerche ha un diverso Productchi ha aProductCategory

var cust:Customer  = new Customer ();

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

Per clonare l'intero albero dell'oggetto ho creato una funzione 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;
}

Il newsorge il seguente errore quando viene transpiled a JavaScript:error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature.

Sebbene lo script funzioni , vorrei eliminare l'errore traspilato

Risposte:


255

Risolvere il problema specifico

Puoi usare un'asserzione di tipo per dire al compilatore che conosci meglio:

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;
}

clonazione

Ricorda che a volte è meglio scrivere la tua mappatura, piuttosto che essere totalmente dinamico. Tuttavia, ci sono alcuni trucchi di "clonazione" che puoi usare che ti danno effetti di differenza.

Userò il seguente codice per tutti gli esempi successivi:

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'));

Opzione 1: diffusione

Proprietà:
Metodi: No
Deep Copy: No

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

Opzione 2: Object.assign

Proprietà:
Metodi: No
Deep Copy: No

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

Opzione 3: Object.create

Immobili: ereditati
Metodi: Inherited
copia completa: Shallow Inherited (profondi cambiamenti influenzano sia originale e 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

Opzione 4: funzione Deep Copy

Proprietà:
Metodi: No
Deep Copy:

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

Chiudi, la transpile ha smesso di lamentarsi con dattiloscritto 1.3, ma una volta in JavaScript avrebbe generato un errore. Dattiloscritto 1.4.1, non lo lascerà andare.
David Laberge,

1
Saresti in grado di chiarire come usi esattamente questo? Ho incluso come metodo il mio oggetto e poi ho ricevuto un errore dicendo che non è una funzione ...
megalucio,

1
Ricevo il seguente errore: "ERRORE TypeError: this.constructor (...) non è un costruttore"
michali

3
Hai appena fatto un esempio pubblico da quel cliente?
Blair Connolly,

1
Qualcuno può TL; DR per me quale delle soluzioni fornite in tutte le risposte preservare il tipo OO del clone, vale a dire cloned instanceof MyClass === true?
Szczepan Hołyszewski il

177

1. Utilizzare l'operatore di diffusione

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

L'operatore di diffusione prende tutti i campi da obj1 e li distribuisce su obj2. Nel risultato si ottiene un nuovo oggetto con un nuovo riferimento e gli stessi campi di quello originale.

Ricorda che è una copia superficiale, significa che se l'oggetto è nidificato, i suoi parametri compositi nidificati esisteranno nel nuovo oggetto con lo stesso riferimento.

2.Object.assign ()

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

Object.assign crea una copia reale, ma solo proprietà proprie, quindi le proprietà nel prototipo non esistono nell'oggetto copiato. È anche una copia superficiale.


3.Object.create ()

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

Object.create non sta eseguendo la clonazione reale , sta creando l'oggetto dal prototipo. Quindi usalo se l'oggetto deve clonare le proprietà del tipo primario, perché l'assegnazione delle proprietà del tipo primario non viene eseguita per riferimento.

I vantaggi di Object.create sono che qualsiasi funzione dichiarata nel prototipo sarà disponibile nel nostro oggetto appena creato.


Poche cose sulla copia superficiale

La copia superficiale inserisce nel nuovo oggetto tutti i campi di quello vecchio, ma significa anche che se l'oggetto originale ha campi di tipo composito (oggetto, matrici ecc.), Tali campi vengono inseriti in un nuovo oggetto con gli stessi riferimenti. La mutazione di tale campo nell'oggetto originale verrà riflessa nel nuovo oggetto.

Sembra forse una trappola, ma la situazione reale in cui l'intero oggetto complesso deve essere copiato è rara. La copia superficiale riutilizzerà la maggior parte della memoria, il che significa che è molto economica rispetto alla copia profonda.


Copia profonda

L'operatore di diffusione può essere utile per la copia approfondita.

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

Sopra il codice creato copia profonda di obj1. Anche il campo composito "complesso" è stato copiato in obj2. Il campo di mutazione "complesso" non rifletterà la copia.


8
Non penso sia del tutto corretto. Object.create(obj1)crea un nuovo oggetto e assegna obj1 come prototipo. Nessuno dei campi in obj1 viene copiato o clonato. Quindi si vedranno cambiamenti su obj1 senza modificare obj2, poiché essenzialmente non ha proprietà. Se prima modifichi obj2, il prototipo non verrà visualizzato per il campo definito poiché il campo di obj2 con il nome è più vicino nella gerarchia.
Ken Rimple,

3
Vedrai anche ES2015 e sviluppatori di dattiloscritto fare questo, che crea un oggetto dal primo parametro (nel mio caso uno vuoto) e copia le proprietà dal secondo e dai successivi parametri): let b = Object.assign({}, a);
Ken Rimple

@KenRimple Hai ragione al 100%, ho aggiunto qualche informazione in più.
Maciej Sikora,


5
Object.assign creerà problemi per oggetti profondi. Ad esempio {nome: 'x', valori: ['a', 'b', 'c']}. Dopo aver utilizzato Object.assign per clonare, entrambi gli oggetti condividono l'array dei valori in modo che l'aggiornamento dell'uno influisca sull'altro. Vedi: developer.mozilla.org/en/docs/Web/JavaScript/Reference/… (sezione 'Warning for Deep Clone'). Dice: per la clonazione profonda, dobbiamo usare altre alternative. Questo perché Object.assign () copia il riferimento alla proprietà quando la proprietà assegnata è un oggetto.
Meir

47

Prova questo:

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

È una buona soluzione fino a quando non si utilizzano oggetti molto grandi o l'oggetto non ha proprietà non serializzabili.

Al fine di preservare la sicurezza dei tipi, è possibile utilizzare una funzione di copia nella classe da cui si desidera eseguire copie:

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

o in modo statico:

static createCopy(objectToCopy: YourClassName): YourClassName{
    return (JSON.parse(JSON.stringify(objectToCopy)));
}

5
Questo va bene, ma dovresti tenere presente che perderai le informazioni sul prototipo e tutti i tipi non supportati in json durante la serializzazione / analisi.
Stanislav E. Govorov il

1
Anche questo sembra meno efficiente rispetto alla funzione deepCopy fornita sopra .
Mojtaba,

Ho questo errore: "Conversione della struttura circolare in JSON" quando utilizzo "(JSON.parse (JSON.stringify (objectToCopy)));"
Cedric Arnould

Funziona solo nel 98% dei casi. undefinedAlmeno può portare a chiavi mancanti con valore. se objectToCopy = { x : undefined};poi dopo aver eseguito il tuo codice Object.keys(objectToCopy).lengthè 1, while Object.keys(copy).lengthis 0.
Aidin,

33

TypeScript / JavaScript ha un proprio operatore per la clonazione superficiale:

let shallowClone = { ...original };

15

È facile ottenere una copia superficiale con "Object Spread" introdotto in TypeScript 2.1

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

produce questo 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


2
Nota: questo creerà una copia superficiale
Jimmy Kane il

11

Per cloni profondi serializzabili, con Tipo informazioni è

export function clone<T>(a: T): T {
  return JSON.parse(JSON.stringify(a));
}

1
Questo può cambiare l'ordine degli oggetti di scena. Solo un avvertimento per alcune persone. Inoltre non gestisce correttamente le date.
Pangamma,

Questo può cambiare l'ordine degli oggetti di scena - potrebbe provare npmjs.com/package/es6-json-stable-stringify invece diJSON.stringify
Polv

@Polv, se qualcuno fa affidamento sull'ordine delle chiavi in ​​un oggetto, penso che abbiano un problema maggiore di clone. :)
Aidin,

Questa soluzione può perdere le chiavi con undefinedvalore. Vedi il mio commento sulla risposta simile sopra: stackoverflow.com/questions/28150967/typescript-cloning-object/…
Aidin

7

La mia opinione su di esso:

Object.assign(...) copia solo le proprietà e perdiamo il prototipo e i metodi.

Object.create(...) non mi sta copiando le proprietà e sto solo creando un prototipo.

Quello che ha funzionato per me è creare un prototipo usando Object.create(...)e copiando le proprietà su di esso usando Object.assign(...):

Quindi, per un oggetto foo, fai il clone in questo modo:

Object.assign(Object.create(foo), foo)

C'è una cosa molto sottile che sta succedendo qui. Stai effettivamente diventando fooil genitore prototipo del clonedFoo(nuovo oggetto). Anche se questo potrebbe sembrare ok, dovresti tenere presente che una proprietà mancante verrà cercata nella catena del prototipo, quindi const a = { x: 8 }; const c = Object.assign(Object.create(a), a); delete c.x; console.log(c.x);stampa 8, mentre dovrebbe essere undefined! (Link REPL: repl.it/repls/CompetitivePreemptiveKeygen )
Aidin

Inoltre, se successivamente aggiungi una proprietà foo, verrà automaticamente visualizzata per clonedFoo! ad es. foo.y = 9; console.log(clonedFoo.y)stamperà 9invece di undefined. È molto probabile che non sia quello che stai chiedendo!
Aidin,

@Aidin Quindi come garantire una copia approfondita?
Muhammad Ali,

qualsiasi altra soluzione in questione, che sta facendo copia per valore ricorsivo (es stackoverflow.com/a/53025968 da marckassay) garantire che, poiché non v'è alcun riferimento all'oggetto sorgente essendo mantenuto nell'oggetto target.
Aidin

5

Puoi anche avere qualcosa del genere:

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;
    }
}

Assicurati solo di sovrascrivere il clonemetodo in tutte le Entitysottoclassi, altrimenti finirai con cloni parziali.

Il tipo restituito di thiscorrisponderà sempre al tipo di istanza.


4

Aggiungi "lodash.clonedeep": "^4.5.0"al tuo package.json. Quindi utilizzare in questo modo:

import * as _ from 'lodash';

...

const copy = _.cloneDeep(original)

3

Se ricevi questo errore:

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

Questo è lo script corretto:

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;
}

4
È corretto cloneObj[attribut] = this.clone();? o vuoi direcloneObj[attribut] = this[attribut].clone();
Serginho,

2

Mi sono imbattuto in questo problema da solo e alla fine ho scritto una piccola libreria clonabile-ts che fornisce una classe astratta, che aggiunge un metodo clone a qualsiasi classe che lo estende. La classe astratta prende in prestito la funzione Deep Copy descritta nella risposta accettata da Fenton che sostituisce solo copy = {};con copy = Object.create(originalObj)per preservare la classe dell'oggetto originale. Ecco un esempio dell'uso della 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

Oppure potresti semplicemente usare il Cloneable.clonemetodo helper:

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    

2

Ecco il mio mash-up! Ed ecco un link StackBlitz ad esso. Al momento si limita a copiare solo tipi semplici e tipi di oggetto ma potrebbe essere modificato facilmente, penso.

   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;
    };

1
Funziona abbastanza bene per quanto posso vedere. Tuttavia, typeof nullè anche un oggetto, quindi la query dovrebbe essere if (source[P] !== null && typeof source[P] === 'object')invece. Altrimenti i tuoi valori null verranno trasformati in un oggetto vuoto.
MortenMoulder

1

Da quando è stato rilasciato TypeScript 3.7, gli alias di tipo ricorsivo sono ora supportati e ci consentono di definire una deepCopy()funzione di tipo sicuro :

// 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

Terreno di gioco


0

Per un semplice clone del contenuto dell'oggetto hole, devo semplicemente stringere e analizzare l'istanza:

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

Mentre cambio i dati nell'albero objectToClone, non vi sono cambiamenti in cloneObject. Quello era il mio requisito.

Spero che sia d'aiuto


1
Può mancare le chiavi con undefinedvalore. Vedi il mio commento sulla risposta simile sopra: stackoverflow.com/questions/28150967/typescript-cloning-object/…
Aidin

0

Ho finito per fare:

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;
}

Perché:

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

da @Fenton ha dato errori di runtime.

Versione dattiloscritta: 2.4.2


0

Che ne dici del buon vecchio jQuery ?! Ecco il clone profondo:

var clone = $.extend(true, {}, sourceObject);

Questa domanda non è stata etichettata come JQuery né JQuery è stato menzionato nella domanda. Sarebbe anche enorme sovraccarico includere JQuery in un progetto solo per fare un clone profondo.
LewisM,

È abbastanza giusto, ma l'OP non riguarda come clonare, si tratta di identificare un problema nel codice che ha fornito e tu hai risposto con il modo jQuery di clonare senza rispondere davvero alla domanda. Non sono io quello che ti ha votato in negativo, ma credo che potrebbe essere il motivo per cui sei stato votato in downgrade.
LewisM,

0

Ho iniziato a creare un servizio di copia / clonazione generico che conserva i tipi per gli oggetti nidificati. Mi piacerebbe un feedback se sto facendo qualcosa di sbagliato, ma sembra funzionare finora ...

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;
    }
  }
}

0

In typeScript ho testato con angolare, e sta andando bene

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.");
    }

0

Per la clonazione profonda di un oggetto che può contenere altri oggetti, matrici e così via, utilizzo:

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
}

Uso:

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

0

È possibile utilizzare il compito di destrutturazione con sintassi diffusa :

var obj = {id = 1, name = 'product1'};
var clonedObject = {...obj};

1
Mentre questo codice può rispondere alla domanda, fornire un contesto aggiuntivo riguardo a come e / o perché risolve il problema migliorerebbe il valore a lungo termine della risposta.
leopale il

-2

Se hai già l'oggetto target, quindi non vuoi crearlo di nuovo (come se aggiorni un array) devi copiare le proprietà.
Se l'hai fatto in questo modo:

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

La lode è dovuta. (guarda il titolo "versione 2")


Funzioni? Array? Oggetti data? Conservazione dei tipi? E ovviamente che dire degli oggetti? Se la funzione sopra incontra uno dei tipi sopra non riuscirà a clonare in profondità. Avrai copiato i riferimenti negli stessi dati. Quando vanno a modificare le proprietà figlio dell'oggetto clonato finiscono per modificare anche l'oggetto originale.
Pangamma,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.