mecanografiado - objeto de clonación

186

Tengo un super clase que es el padre ( Entity) para muchos subclase ( Customer, Product, ProductCategory...)

Estoy buscando clonar dinámicamente un objeto que contiene diferentes subobjetos en Typecript.

Por ejemplo: un Customerque tiene diferente Productquién tiene unProductCategory

var cust:Customer  = new Customer ();

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

Para clonar todo el árbol de objetos, creé una función en 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;
}

El neweleva el siguiente error cuando se transpiled a javascript:error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature.

Aunque el script funciona, me gustaría deshacerme del error transpilado

David Laberge
fuente

Respuestas:

255

Resolviendo el problema específico

Puede usar una aserción de tipo para decirle al compilador que conoce mejor:

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

Clonación

Tenga en cuenta que a veces es mejor escribir su propio mapeo, en lugar de ser totalmente dinámico. Sin embargo, hay algunos trucos de "clonación" que puedes usar que te dan efectos diferentes.

Usaré el siguiente código para todos los ejemplos posteriores:

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

Opción 1: propagación

Propiedades:
Métodos: No
Copia profunda: 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

Opción 2: Object.assign

Propiedades:
Métodos: No
Copia profunda: 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

Opción 3: Object.create

Propiedades: heredados
Métodos: Se hereda
profundo de copia: Shallow heredada (profundos cambios afectan tanto original y copia)

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

Opción 4: Función de copia profunda

Propiedades:
Métodos: No
Copia profunda:

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
fuente
Cerrar, la transpile dejó de quejarse con el mecanografiado 1.3, pero una vez en JavaScript arrojaría un error. Typecript 1.4.1, no lo dejará pasar.
David Laberge
1
¿Sería capaz de aclarar cómo utiliza exactamente esto? Lo incluí como método de mi objeto y luego recibí un error diciendo que no es una función ...
megalucio
1
Recibo el siguiente error: "ERROR TypeError: this.constructor (...) no es un constructor"
michali
3
¿Acabas de hacer un ejemplo público de ese cliente?
Blair Connolly
1
¿Puede alguien TL; DR para mí cuál de las soluciones dadas en todas las respuestas preserva el tipo OO del clon, es decir cloned instanceof MyClass === true?
Szczepan Hołyszewski
177

1.Utilice el operador de propagación

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

El operador de propagación toma todos los campos de obj1 y los extiende sobre obj2. En el resultado, obtienes un nuevo objeto con una nueva referencia y los mismos campos que el original.

Recuerde que es una copia superficial, significa que si el objeto está anidado, sus parámetros compuestos anidados existirán en el nuevo objeto con la misma referencia.

2.Object.assign ()

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

Object.assign crea una copia real, pero solo propiedades propias, por lo que las propiedades en el prototipo no existirán en el objeto copiado. También es una copia superficial.


3.Object.create ()

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

Object.create no está haciendo una clonación real , está creando un objeto a partir del prototipo. Así que úselo si el objeto debe clonar propiedades de tipo primario, porque la asignación de propiedades de tipo primario no se realiza por referencia.

Las ventajas de Object.create son que cualquier función declarada en prototipo estará disponible en nuestro objeto recién creado.


Pocas cosas sobre copia superficial

La copia superficial coloca en un objeto nuevo todos los campos del anterior, pero también significa que si el objeto original tiene campos de tipo compuesto (objeto, matrices, etc.), esos campos se colocan en un objeto nuevo con las mismas referencias. La mutación de dicho campo en el objeto original se reflejará en un objeto nuevo.

Puede parecer una trampa, pero la situación en la que todo el objeto complejo necesita ser copiado es rara. La copia superficial reutilizará la mayor parte de la memoria, lo que significa que es muy barata en comparación con la copia profunda.


Copia profunda

El operador de propagación puede ser útil para la copia profunda.

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

El código anterior creó una copia profunda de obj1. El campo compuesto "complejo" también se copió en obj2. El campo de mutación "complejo" no reflejará la copia.

Maciej Sikora
fuente
8
No creo que sea completamente correcto. Object.create(obj1)crea un nuevo objeto y asigna obj1 como prototipo. Ninguno de los campos en obj1 se copia o clona. Por lo tanto, se verán cambios en obj1 sin modificar obj2, ya que esencialmente no tiene propiedades. Si modifica obj2 primero, el prototipo no se verá para el campo que defina ya que el campo de obj2 con el nombre está más cerca en la jerarquía.
Ken Rimple
3
También verá que ES2015 y los desarrolladores de mecanografía hacen esto en su lugar, lo que crea un objeto a partir del primer parámetro (en mi caso, uno vacío) y copia las propiedades del segundo y los parámetros posteriores): let b = Object.assign({}, a);
Ken Rimple
@KenRimple Tienes 100% de razón, agregué más información.
Maciej Sikora
tal vez sea útil => developer.mozilla.org/en/docs/Web/JavaScript/Reference/…
Emmanuel Touzery
55
Object.assign creará problemas para objetos profundos. Por ejemplo {nombre: 'x', valores: ['a', 'b', 'c']}. Después de usar Object.assign para clonar, ambos objetos comparten la matriz de valores, por lo que la actualización de uno afecta al otro. Ver: developer.mozilla.org/en/docs/Web/JavaScript/Reference/… (sección 'Advertencia para Deep Clone'). Dice: Para la clonación profunda, necesitamos usar otras alternativas. Esto se debe a que Object.assign () copia la referencia de propiedad cuando la propiedad que se asigna es un objeto.
Meir
48

Prueba esto:

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

Es una buena solución hasta que esté utilizando objetos muy grandes o su objeto tenga propiedades no serializables.

Para preservar la seguridad de tipos, puede usar una función de copia en la clase de la que desea hacer copias:

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

o de forma estática:

static createCopy(objectToCopy: YourClassName): YourClassName{
    return (JSON.parse(JSON.stringify(objectToCopy)));
}
Lars
fuente
55
Esto está bien, pero debe tener en cuenta que perderá la información del prototipo y todos los tipos no admitidos en json al serializar / analizar.
Stanislav E. Govorov
1
Además, esto parece menos eficiente en comparación con la función deepCopy proporcionada anteriormente .
Mojtaba
Tengo este error: "Convertir estructura circular a JSON" cuando uso "(JSON.parse (JSON.stringify (objectToCopy))));"
Cedric Arnould
Solo funciona en el 98% de los casos. Puede conducir a la falta de claves con undefinedvalor, al menos. si objectToCopy = { x : undefined};luego de ejecutar su código Object.keys(objectToCopy).lengthes 1, mientras que Object.keys(copy).lengthes 0.
Aidin
33

TypeScript / JavaScript tiene su propio operador para la clonación superficial:

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

Es fácil obtener una copia superficial con "Object Spread" introducido en TypeScript 2.1

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

produce 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

Homero
fuente
2
Nota: esto creará una copia superficial
Jimmy Kane
11

Para clones profundos serializables, con información de tipo es,

export function clone<T>(a: T): T {
  return JSON.parse(JSON.stringify(a));
}
Polv
fuente
1
Esto puede cambiar el orden de los accesorios. Solo una advertencia para algunas personas. Además, no maneja las fechas correctamente.
Pangamma
Esto puede cambiar el orden de los accesorios, puede probar npmjs.com/package/es6-json-stable-stringify en lugar deJSON.stringify
Polv
@Polv, si alguien confía en el orden de las teclas en un objeto, creo que tienen un problema mayor que clone. :)
Aidin
Esta solución puede perder claves con undefinedvalor. Vea mi comentario sobre la respuesta similar anterior: stackoverflow.com/questions/28150967/typescript-cloning-object/…
Aidin
7

Mi opinión sobre esto:

Object.assign(...) solo copiamos propiedades y perdemos el prototipo y los métodos.

Object.create(...) no está copiando propiedades para mí y solo creando un prototipo.

Lo que funcionó para mí es crear un prototipo usando Object.create(...)y copiando propiedades usando Object.assign(...):

Entonces, para un objeto foo, haga un clon como este:

Object.assign(Object.create(foo), foo)
Muhammad Ali
fuente
Aquí está sucediendo algo muy sutil. Realmente estás haciendo fooser el padre prototípico del clonedFoo(nuevo objeto). Si bien esto puede sonar bien, debe tener en cuenta que se buscará una propiedad faltante en la cadena del prototipo, por lo que const a = { x: 8 }; const c = Object.assign(Object.create(a), a); delete c.x; console.log(c.x);imprime 8, ¡mientras que debería serlo undefined! (Enlace REPL: repl.it/repls/CompetitivePreemptiveKeygen )
Aidin
Además, si luego agrega una propiedad foo, ¡aparecerá automáticamente clonedFoo! Por ejemplo, foo.y = 9; console.log(clonedFoo.y)se imprimirá en 9lugar de undefined. ¡Es muy probable que no sea lo que estás pidiendo!
Aidin
@Aidin Entonces, ¿cómo asegurar una copia profunda?
Muhammad Ali
cualquier otra solución en esta pregunta, que está haciendo una copia por valor de forma recursiva (por ejemplo, stackoverflow.com/a/53025968 por marckassay) asegura que, dado que no hay referencia al objeto de origen que se mantiene en el objeto de destino.
Aidin hace
5

También puedes tener algo como esto:

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

Solo asegúrate de anular el clonemétodo en todas las Entitysubclases, de lo contrario terminarás con clones parciales.

El tipo de retorno de thissiempre coincidirá con el tipo de la instancia.

Decade Moon
fuente
4

Añadir "lodash.clonedeep": "^4.5.0"a tu package.json. Luego use así:

import * as _ from 'lodash';

...

const copy = _.cloneDeep(original)
usuario2878850
fuente
3

Si obtiene este error:

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

Este es el script correcto:

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
fuente
44
Es correcto cloneObj[attribut] = this.clone();? o quieres decircloneObj[attribut] = this[attribut].clone();
Serginho
2

Encontré este problema yo mismo y al final escribí una pequeña biblioteca cloneable-ts que proporciona una clase abstracta, que agrega un método de clonación a cualquier clase que la extienda. La clase abstracta toma prestada la función de copia profunda descrita en la respuesta aceptada por Fenton solo reemplazando copy = {};con copy = Object.create(originalObj)para preservar la clase del objeto original. Aquí hay un ejemplo del uso de la clase.

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

O simplemente podría usar el 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
fuente
2

Aquí está mi mash-up! Y aquí hay un enlace de StackBlitz . Actualmente está limitado a copiar solo tipos simples y tipos de objetos, pero podría modificarse fácilmente, creo.

   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;
    };
ensayo
fuente
1
Funciona bastante bien hasta donde puedo ver. Sin embargo, typeof nulltambién es un objeto, por lo que la consulta debería serlo if (source[P] !== null && typeof source[P] === 'object'). De lo contrario, sus valores nulos se convertirán en un objeto vacío.
MortenMoulder
1

Desde que se lanzó TypeScript 3.7, los alias de tipo recursivo ahora son compatibles y nos permite definir una deepCopy()función de tipo seguro :

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

Patio de recreo

Valeriy Katkov
fuente
0

Para un clon simple del contenido del objeto del agujero, simplemente encadenar y analizar la instancia:

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

Mientras que cambio los datos en el árbol objectToClone, no hay ningún cambio en cloneObject. Ese fue mi requisito.

Espero que ayude

Ferhatos
fuente
1
Puede faltar claves con undefinedvalor. Vea mi comentario sobre la respuesta similar anterior: stackoverflow.com/questions/28150967/typescript-cloning-object/…
Aidin
0

Terminé haciendo:

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

de @Fenton dio errores de tiempo de ejecución.

Versión mecanografiada: 2.4.2

Bernoulli IT
fuente
0

¿Qué tal el viejo jQuery? Aquí está el clon profundo:

var clone = $.extend(true, {}, sourceObject);
alehro
fuente
Esta pregunta no fue etiquetada como JQuery ni JQuery fue mencionada en la pregunta. También sería una sobrecarga masiva incluir a JQuery en un proyecto solo para hacer un clon profundo.
LewisM
Eso es bastante justo, pero el OP no se trata de cómo clonar, se trata de identificar un problema en el código que proporcionó y usted respondió con la forma de clonación jQuery sin realmente responder la pregunta. No fui yo quien te rechazó, pero creo que puede ser por eso que te rechazaron.
LewisM
0

Intenté crear un servicio genérico de copia / clonación que retiene los tipos de objetos anidados. Me encantaría recibir comentarios si estoy haciendo algo mal, pero parece funcionar hasta ahora ...

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
fuente
0

En typeScript pruebo con angular, y está funcionando bien

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
fuente
0

Para la clonación profunda de un objeto que puede contener otros objetos, matrices, etc., 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
}

Utilizar:

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

Podría usar la asignación de desestructuración con sintaxis extendida :

var obj = {id = 1, name = 'product1'};
var clonedObject = {...obj};
SOUVIK SAHA
fuente
1
Si bien este código puede responder la pregunta, proporcionar un contexto adicional sobre cómo y / o por qué resuelve el problema mejoraría el valor a largo plazo de la respuesta.
leopal
-2

Si ya tiene el objeto de destino, por lo que no desea crearlo de nuevo (como si actualizara una matriz), debe copiar las propiedades.
Si lo ha hecho de esta manera:

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

Se deben alabar. (mira el título "versión 2")

LosManos
fuente
Funciones? Matrices? Fecha de objetos? Preservación de tipos? Y, por supuesto, ¿qué pasa con los objetos? Si la función anterior encuentra alguno de los tipos anteriores, no podrá clonar en profundidad. Habrás copiado las referencias a los mismos datos. Cuando van a editar las propiedades secundarias del objeto clonado, terminarán editando también el objeto original.
Pangamma