打字稿-克隆对象


208

我有一个超类是父(Entity)对于很多子类(CustomerProductProductCategory...)

我正在寻找一个可以动态克隆在Typescript中包含不同子对象的对象的方法。

例如:Customer具有不同的Product人,具有ProductCategory

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上升时,它被transpiled为JavaScript以下错误:error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature.

虽然该脚本有效,但我想摆脱已编译的错误

Answers:


276

解决具体问题

您可以使用类型断言来告诉编译器您更了解:

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

关闭,转换停止使用TypeScript 1.3进行抱怨,但是一旦使用javascript,它将引发错误。打字稿1.4.1不会放手。
David Laberge

1
您是否愿意澄清使用该方法的确切方式?我将其作为对象的一种方法包含在内,然后收到一条错误消息,指出该函数不是函数...
megalucio

1
我收到以下错误:“ ERROR TypeError:this.constructor(...)不是构造函数”
michali

3
您是否只是从该客户中做出公开榜样?
布莱尔·康诺利

1
有人TL; DR可以为我提供所有答案中的哪个解决方案保留克隆的OO类型,即cloned instanceof MyClass === true
SzczepanHołyszewski

192

1,使用点差算子

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

Spread运算符从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的深层副本。复合字段“ complex”也被复制到obj2中。突变字段“复杂”将不反映副本。


8
我认为那不是完全正确的。Object.create(obj1)创建一个新对象,并将obj1分配为原型。obj1中的任何字段均未复制或克隆。因此将看到不修改obj2而对obj1进行的更改,因为它实际上没有属性。如果您首先修改obj2,则由于定义的obj2字段在层次结构中更接近,因此您定义的字段将看不到原型。
Ken Rimple

3
您还会看到ES2015和打字稿开发人员正在执行此操作,该操作从第一个参数创建一个对象(在我的情况下为空),并从第二个及后续参数复制属性): let b = Object.assign({}, a);
Ken Rimple

@KenRimple您100%正确,我添加了更多信息。
Maciej Sikora


5
Object.assign将为深层对象创建问题。例如{name:'x',values:['a','b','c']}。使用Object.assign克隆后,两个对象共享values数组,因此更新一个对象会影响另一个对象。请参阅:developer.mozilla.org/en/docs/Web/JavaScript/Reference/…(“深度克隆警告”部分)。它说:对于深度克隆,我们需要使用其他替代方法。这是因为当分配的属性是对象时,Object.assign()复制属性引用。
Meir

54

试试这个:

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不支持的所有类型。
Stanislav E. Govorov

1
另外这似乎不太有效的比较所提供的功能deepcopy的上面
Mojtaba

我有这个错误:当我使用“((JSON.parse(JSON.stringify(objectToCopy)));””时,“将循环结构转换为JSON”;
Cedric Arnould

仅在98%的情况下有效。undefined至少可能导致缺少值的键。如果objectToCopy = { x : undefined};然后运行代码后Object.keys(objectToCopy).length1,而Object.keys(copy).length0
艾丁

36

TypeScript / JavaScript拥有自己的浅层克隆运算符:

let shallowClone = { ...original };

15

使用TypeScript 2.1中引入的“ Object Spread”可以轻松获得浅表副本

此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
注意:这将创建浅表副本
Jimmy Kane,

13

对于可序列化的深层克隆,使用类型信息为,

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

1
这可以改变道具的顺序。只是对某些人的警告。而且它不能正确处理日期。
庞加玛

这可以改变道具的顺序-可以尝试npmjs.com/package/es6-json-stable-stringify代替JSON.stringify
聚{

@Polv,如果有人依赖对象中键的顺序,我认为它们的问题要比更大clone。:)
艾丁

此解决方案可能会遗漏undefined有价值的键。看到类似的答案我上面的评论:stackoverflow.com/questions/28150967/typescript-cloning-object/...
Aidin

我确实明确地说过“可序列化”。而且,它的确取决于用例,但是我总是会很乐意扔掉未定义的(我知道,在数组中是不可能的)。对于日期和正则表达式,或者比这更多(例如,大多数类,多数功能),我建议递归函数- stackoverflow.com/questions/122102/...
聚{

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链接:repl.it/repls/CompetitivePreemptiveKeygen
Aidin

此外,如果您以后向中添加属性foo,它将自动显示为clonedFoo!例如foo.y = 9; console.log(clonedFoo.y)将打印出来,9而不是undefined。这很可能不是您要的!
艾丁

@Aidin那么如何确保深拷贝?
穆罕默德·阿里

此问题中的任何其他解决方案(按递归方式按值复制)(例如marckassay的stackoverflow.com/a/53025968)都应确保,因为在目标对象中没有对源对象的维护。

6

您还可以具有以下内容:

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将始终与实例的类型匹配。


5

添加"lodash.clonedeep": "^4.5.0"到您的中package.json。然后像这样使用:

import * as _ from 'lodash';

...

const copy = _.cloneDeep(original)

我只是想知道是否可以使用库,如果您真的不知道/不了解实现/含义?(cloneDeep的实现是github.com/lodash/lodash/blob/master/.internal/baseClone.js)我认为涉及不可枚举属性的递归函数是最好的解决方案之一。(某处在此QA
聚{

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

2

我自己遇到了这个问题,最后编写了一个小的库cloneable-ts,它提供了一个抽象类,该类向任何扩展它的类添加了一个clone方法。抽象类借由芬顿只替换接受的答案中描述的深复印功能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.clonehelper方法:

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

自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

操场


// compile time errorforUncopiableUser总是很好,但是它对递归函数解决方案的适用性如何?
聚{

1

下面是占了一个现代化的实现SetMap太:

export function deepClone<T extends object>(value: T): T {
  if (typeof value !== 'object' || value === null) {
    return value;
  }

  if (value instanceof Set) {
    return new Set(Array.from(value, deepClone)) as T;
  }

  if (value instanceof Map) {
    return new Map(Array.from(value, ([k, v]) => [k, deepClone(v)])) as T;
  }

  if (value instanceof Date) {
    return new Date(value) as T;
  }

  if (value instanceof RegExp) {
    return new RegExp(value.source, value.flags) as T;
  }

  return Object.keys(value).reduce((acc, key) => {
    return Object.assign(acc, { [key]: deepClone(value[key]) });
  }, (Array.isArray(value) ? [] : {}) as T);
}

尝试一下:

deepClone({
  test1: { '1': 1, '2': {}, '3': [1, 2, 3] },
  test2: [1, 2, 3],
  test3: new Set([1, 2, [1, 2, 3]]),
  test4: new Map([['1', 1], ['2', 2], ['3', 3]])
});

test1:
  1: 1
  2: {}
  3: [1, 2, 3]

test2: Array(3)
  0: 1
  1: 2
  2: 3

test3: Set(3)
  0: 1
  1: 2
  2: [1, 2, 3]

test4: Map(3)
  0: {"1" => 1}
  1: {"2" => 2}
  2: {"3" => 3}


0

为了简单地复制孔对象的内容,我只需对实例进行字符串化和解析:

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

这足够公平,但是OP并不是关于如何克隆,而是关于在他提供的代码中识别问题,并且您使用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中,我用angular进行了测试,一切正常

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

2
尽管此代码可以回答问题,但提供有关如何和/或为什么解决问题的其他上下文将提高​​答案的长期价值。
leopal

-1
function instantiateEmptyObject(obj: object): object {
    if (obj == null) { return {}; }

    const prototype = Object.getPrototypeOf(obj);
    if (!prototype) {
        return {};
    }

    return Object.create(prototype);
}

function quickCopy(src: object, dest: object): object {
    if (dest == null) { return dest; }

    return { ...src, ...dest };
}

quickCopy(src, instantiateEmptyObject(new Customer()));

当前状态的这个答案没有什么用。您能否添加更多有关如何使用它来解决原始问题的详细信息?
phsource

-2

如果已经有了目标对象,那么就不想重新创建它(例如更新数组),则必须复制属性。
如果以这种方式完成:

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

赞美是应该的。(请查看标题“版本2”)


职能?数组?日期对象?保存类型?当然,对象呢?如果以上函数遇到以上任何类型,则将无法深度克隆。您将把引用复制到相同的数据。当他们去编辑克隆对象的子属性时,它们也将最终编辑原始对象。
庞加玛
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.