машинопис - об’єкт клонування


187

У мене є клас супер , який є батьківським ( Entity) для багатьох підкласу ( Customer, Product, ProductCategory...)

Я дивлюся, щоб динамічно клонувати об’єкт, який містить різні під об’єкти в Typescript.

Наприклад: a Customerмає різні, Productхто має aProductCategory

var cust:Customer  = new Customer ();

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

Для клонування всього дерева об’єкта я створив функцію в 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;
}

Під newчас трансляції в javascript виникає така помилка:error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature.

Хоча сценарій працює, я хотів би позбутися від перекладеної помилки

Відповіді:


255

Вирішення конкретного питання

Ви можете використовувати твердження типу, щоб сказати компілятору, що ви краще знаєте:

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

Клонування

Майте на увазі, що іноді краще написати власне відображення, а не бути абсолютно динамічним. Однак, ви можете скористатися декількома трюками "клонування", які дають різницю.

Я буду використовувати наступний код для всіх наступних прикладів:

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

Варіант 1: Поширити

Властивості: Так
Способи: Ні
Копія глибока: Ні

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

Варіант 2: Object.assign

Властивості: Так
Способи: Ні
Копія глибока: Ні

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

Варіант 3: Object.create

Властивості: Успадковані
методи: Успадковане
глибоке копіювання: Неглибоке успадкування (глибокі зміни впливають як на оригінал, так і на клон)

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

Варіант 4: Функція глибокої копіювання

Властивості: Так
Способи: Ні
глибока копія: так

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

Закрити, транспіля перестала скаржитися на машинопис typecript 1.3, але, як тільки в JavaScript, це призведе до помилки. Typescript 1.4.1, не відпускає його.
Девід Лаберже

1
Ви б хотіли роз'яснити, як саме ви цим користуєтеся? Я включив як метод свого об'єкта, а потім отримав помилку, кажучи, що це не функція ...
megalucio

1
Я отримую таку помилку: "ПОМИЛКА TypeError: this.constructor (...) не є конструктором"
michali

3
Ви просто зробили публічний приклад із цього замовника?
Блер Конноллі

1
Чи може хтось TL; DR для мене, який із рішень, наведених у всіх відповідях, зберігає тип клону, тобто cloned instanceof MyClass === true?
Щепан Холішевський

177

1.Використовуйте оператор розповсюдження

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

Оператор розповсюдження приймає всі поля з obj1 і розподіляє їх по obj2. В результаті ви отримуєте новий об'єкт з новою посиланням та тими ж полями, що й оригінальний.

Пам'ятайте, що це неглибока копія, це означає, що якщо об'єкт вкладений, то його вкладені складені парами будуть існувати в новому об'єкті за тією ж посиланням.

2.Object.assign ()

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

Object.assign створює реальну копію, але лише власні властивості, тому властивості в прототипі не будуть існувати в скопійованому об'єкті. Це також неглибока копія.


3.Object.create ()

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

Object.create не робить справжнє клонування , це створює об’єкт з прототипу. Тому використовуйте його, якщо об'єкт повинен клонувати властивості первинного типу, оскільки призначення властивостей первинного типу не виконується за посиланням.

Плюси Object.create в тому , що будь-які функції , оголошені в прототипі будуть доступні в нашому недавно створеному об'єкті.


Мало речей про дрібну копію

Дрібна копія вводить у новий об’єкт усі поля старого, але це також означає, що якщо оригінальний об’єкт має поля складеного типу (об’єкт, масиви тощо), то ці поля ставляться в новий об'єкт з тими ж посиланнями. Мутація такого поля в оригінальному об'єкті відобразиться в новому об'єкті.

Це може виглядати як підводний камінь, але насправді ситуація, коли весь складний об’єкт потрібно копіювати, є рідкісною. Дрібна копія повторно використовуватиме більшу частину пам'яті, що означає, що це дуже дешево порівняно з глибокою копією.


Глибока копія

Оператор розповсюдження може бути зручним для глибокої копії.

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

Над кодом створена глибока копія obj1. Складене поле "комплекс" також було скопійовано в obj2. Поле "Мутація" мутації не відображатиме копію.


8
Я не думаю, що це абсолютно правильно. Object.create(obj1)створює новий об'єкт і призначає obj1 як прототип. Жодне з полів у obj1 не копіюється та не клонується. Тож зміни на obj1 без зміни obj2 будуть помітні, оскільки він по суті не має властивостей. Якщо спочатку змінити obj2, прототип не буде видно для визначеного поля, оскільки поле obj2 з ім'ям ближче в ієрархії.
Кен Рімпл

3
Ви також побачите ES2015 та розробники машинописів, які роблять це замість цього, який створює об'єкт із 1-го параметра (у моєму випадку порожній) та копіює властивості з другого та наступних параметрів): let b = Object.assign({}, a);
Ken Rimple,

@KenRimple Ви маєте 100% право, я додав ще трохи інформації.
Мацей Сікора


5
Object.assign створить проблеми для глибоких об'єктів. Наприклад {name: 'x', значень: ['a', 'b', 'c']}. Після використання Object.assign для клонування обидва об’єкти поділяють масив значень, тому оновлення впливає на інший. Див.: Developer.mozilla.org/en/docs/Web/JavaScript/Reference/… ( розділ "Попередження про глибокий клон"). У ній сказано: Для глибокого клонування нам потрібно використовувати інші альтернативи. Це відбувається тому, що Object.assign () копіює посилання на властивість, коли властивість, яка призначається, є об'єктом.
Меїр

48

Спробуйте це:

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

Це гарне рішення до тих пір, поки ви не використовуєте дуже великі об'єкти або ваш об'єкт не має несеріалізаційних властивостей.

Щоб зберегти безпеку типу, ви можете використовувати функцію копіювання в класі, з якого потрібно зробити копії:

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

або статичним способом:

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

5
Це нормально, але слід пам’ятати, що ви втратите інформацію про прототип та всі типи, які не підтримуються в json, при серіалізації / розбору.
Станіслав Євгенович Говоров

1
Також це здається менш ефективним порівняно з функцією deepCopy, представленою вище .
Мойтаба

У мене є ця помилка: "Перетворення кругової структури в JSON", коли я використовую "(JSON.parse (JSON.stringify (objectToCopy))));"
Седрік

Працює лише у 98% випадків. undefinedПринаймні може призвести до відсутності ключів зі значенням. якщо objectToCopy = { x : undefined};тоді після запуску вашого коду Object.keys(objectToCopy).lengthє 1, поки Object.keys(copy).lengthє 0.
Айдін

33

TypeScript / JavaScript має власного оператора для дрібного клонування:

let shallowClone = { ...original };

15

Легко отримати дрібну копію за допомогою "Об'єктного поширення", введеного в TypeScript 2.1

цей TypeScript: let copy = { ...original };

виробляє цей 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
Примітка: це створить дрібну копію
Джиммі Кейн

11

Для серіалізуемого глибокого клону з Тип інформації є,

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

1
Це може змінити порядок реквізиту. Просто попередження для деяких людей. Крім того, він не обробляє дати правильно.
Пангама

Це може змінити порядок реквізиту - може спробувати npmjs.com/package/es6-json-stable-stringify замістьJSON.stringify
Polv

@Polv, якщо хтось покладається на порядок клавіш в об'єкті, я думаю, що у них більша проблема, ніж clone. :)
Айдін

Це рішення може пропустити ключі зі undefinedзначенням. Дивіться мій коментар до подібної відповіді вище: stackoverflow.com/questions/28150967/typescript-cloning-object/…
Aidin

7

Моє взяти на це:

Object.assign(...) лише копіюємо властивості, і ми втрачаємо прототип та методи.

Object.create(...) - це не копіювання властивостей для мене, а просто створення прототипу.

Для мене працювало створення прототипу з використанням Object.create(...)та копіювання властивостей до нього за допомогою Object.assign(...):

Отже, для об'єкта fooзробіть клон таким:

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

Тут відбувається дуже тонка річ. Ви насправді робите fooпрототипним батьком clonedFoo(нового об’єкта). Хоча це може здатися нормальним, ви повинні пам’ятати, що відсутнє властивість буде шукати в ланцюзі прототипу, тому const a = { x: 8 }; const c = Object.assign(Object.create(a), a); delete c.x; console.log(c.x);друкується 8, поки має бути undefined! (Реплєї посилання: repl.it/repls/CompetitivePreemptiveKeygen )
Aidin

Крім того, якщо згодом ви додасте властивість foo, воно автоматично з’явиться для clonedFoo! наприклад, foo.y = 9; console.log(clonedFoo.y)буде надруковано 9замість undefined. Це дуже ймовірно, що це не те, про що ви просите!
Айдін

@Aidin Отже, як забезпечити глибоку копію?
Мухаммед Алі

будь-яке інше рішення в цьому питанні, який робить копію за значенням рекурсивно (наприклад stackoverflow.com/a/53025968 по marckassay) гарантувати , що, так як немає посилання на об'єкт - джерело не підтримується в цільовому об'єкті.
Айдін

5

Ви також можете мати щось подібне:

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

Просто переконайтесь, що ви перекриєте cloneметод у всіх Entityпідкласах, інакше ви отримаєте часткові клони.

Тип повернення thisзавжди буде відповідати типу екземпляра.


4

Додайте "lodash.clonedeep": "^4.5.0"до свого package.json. Потім використовуйте так:

import * as _ from 'lodash';

...

const copy = _.cloneDeep(original)

3

Ось моя мішанка! І ось посилання на StackBlitz на нього. Наразі його обмежено лише копіюванням простих типів та типів об’єктів, але я міг би легко змінити.

   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
Працює досить добре, наскільки я бачу. Однак typeof nullтакож є об'єктом, тому запит повинен бути if (source[P] !== null && typeof source[P] === 'object')замість цього. Інакше ваші нульові значення перетворяться на порожній об'єкт.
MortenMoulder

3

Якщо ви отримаєте цю помилку:

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

Це правильний сценарій:

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
Правильно cloneObj[attribut] = this.clone();? або ти маєш на увазіcloneObj[attribut] = this[attribut].clone();
Серхіньо

2

Я сам зіткнувся з цією проблемою, і врешті-решт написав невелику бібліотеку- клонінг-ts, яка забезпечує абстрактний клас, який додає метод клонування до будь-якого класу, що розширює його. Абстрактний клас запозичує функцію Deep Copy , описану в загальноприйнятому відповіді на Фентоні тільки заміну copy = {};з , copy = Object.create(originalObj)щоб зберегти клас вихідного об'єкта. Ось приклад використання класу.

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

Або ви можете просто скористатися Cloneable.cloneдопоміжним методом:

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    

1

З моменту випуску TypeScript 3.7 псевдоніми рекурсивного типу тепер підтримуються, і це дозволяє нам визначити безпечну deepCopy()функцію типу :

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

Ігровий майданчик


0

Для простого клонування вмісту об'єкта hole я просто впорядкував і проаналізував екземпляр:

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

Тоді як я змінюю дані в дереві objectToClone, в cloneObject немає змін. Це була моя вимога.

Сподіваюся, це допоможе


1
Можна пропустити ключі зі undefinedзначенням. Дивіться мій коментар до подібної відповіді вище: stackoverflow.com/questions/28150967/typescript-cloning-object/…
Aidin

0

Я закінчив робити:

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

Тому що:

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

від @Fenton видав помилки під час виконання.

Версія машинопису: 2.4.2


0

Як щодо старого доброго jQuery ?! Ось глибокий клон:

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

Це питання не було позначене JQuery, а також JQuery не згадувалося в питанні. Також було б величезним накладними витратами включити JQuery у проект просто для того, щоб зробити глибокий клон.
LewisM

Це досить справедливо, але ОП не в тому, як клонувати, це в тому, щоб визначити проблему в наданому ним коді, і ви відповіли способом клонування jQuery, не відповідаючи на це питання. Я не той, хто виступав проти тебе, але я вважаю, що саме тому ти був порушений.
LewisM

0

Я взяв на себе аналіз створення загальної служби копіювання / клонування, яка зберігає типи для вкладених об'єктів. Буду любити відгук, якщо я роблю щось не так, але, здається, працюю поки що ...

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

У typeScript я тестую з кутовим, і це все добре

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

Для глибокого клонування об'єкта, який може містити інші об'єкти, масиви тощо, я використовую:

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
}

Використання:

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

0

Ви можете використовувати завдання деструктуризації із синтаксисом розширення :

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

1
Хоча цей код може відповісти на питання, надаючи додатковий контекст щодо того, як та / або чому він вирішує проблему, покращить довгострокове значення відповіді.
leopal

-2

Якщо у вас вже є цільовий об’єкт, тому ви не хочете створювати його заново (наприклад, при оновленні масиву), ви повинні скопіювати властивості.
Якщо ви зробили це так:

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

Похвала належить. (дивіться заголовок "версія 2")


Функції? Масиви? Об'єкти дати? Збереження видів? І звичайно, що з предметами? Якщо вищевказана функція стикається з будь-яким з перерахованих вище типів, вона не зможе глибоко клонуватись. Ви скопіювали посилання на ті самі дані. Коли вони перейдуть до редагування дочірніх властивостей клонованого об'єкта, вони також редагують оригінальний об'єкт.
Пангама
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.