TypeScript - 克隆对象

358

我有一个超级类,它是许多子类(CustomerProductProductCategory...)的父类(Entity)。

我想在Typescript中动态克隆包含不同子对象的对象。

例如:一个包含不同ProductProductCategoryCustomer

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.

虽然脚本能够正常工作,但我想消除转译出现的错误

27个回答

471

解决具体问题

您可以使用类型断言告诉编译器您更清楚:

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

克隆

截至2022年,提议允许structuredClone深度复制多种类型。

const copy = structuredClone(value)

在使用此功能时,有一些限制

请记住,有时编写自己的映射比完全动态地生成更好。但是,您可以使用一些“克隆”技巧来获得不同的效果。

我将在所有后续示例中使用以下代码:

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

1
你能否澄清一下,你是如何使用它的?我将其作为我的对象的一个方法包含进去,然后出现了一个错误,说它不是一个函数... - megalucio
1
我遇到了以下错误:"ERROR TypeError: this.constructor(...) 不是一个构造函数"。 - michali
4
你刚刚是在公开展示那位客户吗? - Blair Connolly
对我来说,“选项3:Object.create”是将属性复制到新对象的技巧。 - Vince I
1
有人能为我简要概括一下,在所有答案中,哪些解决方案保留了克隆的面向对象类型,即 cloned instanceof MyClass === true - Szczepan Hołyszewski
显示剩余8条评论

274
  1. 使用扩展运算符...

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

扩展运算符会将obj1中的所有字段展开到obj2中。其结果是您会获得一个新的对象,具有新的引用和与原始对象相同的字段。

请注意,这是浅层复制,这意味着如果对象嵌套,则其嵌套的组合参数将以相同的引用存在于新对象中。

  1. Object.assign()

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

Object.assign 创建真正的副本,但只有自己的属性,因此原型中的属性在复制的对象中将不存在。它也是浅拷贝。


  1. 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中。更改字段“complex”将不会反映在副本中。


8
我认为这并不完全正确。Object.create(obj1)会创建一个新对象,并将obj1指定为其原型。 obj1中的字段都没有被复制或克隆。因此,如果在不修改obj2的情况下更改obj1,就会看到更改,因为obj1实际上没有属性。如果你先修改obj2,则由于obj2中具有相同名称的字段更接近原型链顶端,所以定义的字段的原型不会被看到。 - Ken Rimple
3
你也会看到使用ES2015和TypeScript的开发人员这样做,它会从第一个参数(在我的例子中是一个空对象)创建一个新对象,并将第二个及之后的参数的属性复制到新对象中:let b = Object.assign({}, a); - Ken Rimple
@KenRimple 你是完全正确的,我添加了更多信息。 - Maciej Sikora
可能会有帮助 => https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign - Emmanuel Touzery
5
Object.assign 会对深层嵌套的对象造成问题。例如 {name: 'x', values: ['a','b','c']}。使用 Object.assign 进行克隆后,两个对象会共享 values 数组,所以更新一个会影响另一个。 请参阅:https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign(“警告深度克隆”一节)。它说:对于深度克隆,我们需要使用其他替代方法。这是因为当被分配的属性是一个对象时,Object.assign() 会复制其属性引用。 - Meir

84

试试这个:

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

9
可以,但需要注意,在序列化/解析时会丢失原型信息和不受 JSON 支持的所有类型。请确保翻译后内容通俗易懂,但不改变原意。 - Stanislav E. Govorov
1
此外,与上面提供的deepCopy函数相比,这种方法似乎效率较低。 - Mojtaba
当我使用“(JSON.parse(JSON.stringify(objectToCopy)))”时,出现了“将循环结构转换为JSON”的错误。 - Cedric Arnould
1
仅在 98% 的情况下有效。可能会导致缺少键和 undefined 值。例如,如果 objectToCopy = { x : undefined};,那么运行您的代码后,Object.keys(objectToCopy).length1,而 Object.keys(copy).length0 - Aidin

62

TypeScript/JavaScript有自己的用于浅拷贝的运算符:

let shallowClone = { ...original };

2
请注意,如果存在嵌套对象,则引用将被保留,因此“Shallow”。使用此功能的任何人都应该知道这一点。 - lk404

19

在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
注意:这将创建一个浅拷贝。 - Jimmy Kane

18

对于可序列化的深度克隆,带有类型信息,

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

2
这可能会改变道具的顺序。对于一些人来说只是一个警告。此外,它不能正确处理日期。 - Pangamma
这可能会改变道具的顺序 - 可以尝试使用 https://www.npmjs.com/package/es6-json-stable-stringify 而不是 JSON.stringify - Polv
3
@Polv,如果有人依赖于对象中键的顺序,我认为他们比“克隆”更大的问题。 :) - Aidin
这个解决方案可能会忽略值为undefined的键。请参考我在上面类似答案的评论:https://dev59.com/d14c5IYBdhLWcg3wOoO6#NgrrnYgBc1ULPQZF043G - Aidin
我确实说过“可序列化”。此外,这取决于用例,但我总是乐意丢弃未定义的内容(我知道,在数组中是不可能的)。对于日期和正则表达式,或更多内容(例如大多数类、大多数函数),我建议使用递归函数--https://dev59.com/83VD5IYBdhLWcg3wAGiD?answertab=votes#tab-top - Polv

12
"lodash.clonedeep": "^4.5.0"添加到您的package.json文件中,然后像这样使用:
import * as _ from 'lodash';

...

const copy = _.cloneDeep(original)

我只是想知道,如果你不真正了解实现/影响,是否可以使用库?(cloneDeep的实现方式为https://github.com/lodash/lodash/blob/master/.internal/baseClone.js)我认为触及非枚举属性的递归函数是最好的解决方案之一。(在[此QA](https://dev59.com/83VD5IYBdhLWcg3wAGiD?answertab=votes#tab-top)中的某个地方。) - Polv

8

以下是TypeScript中 deepCopy 的实现(代码中没有使用any):

const deepCopy = <T, U = T extends Array<infer V> ? V : never>(source: T ): T => {
  if (Array.isArray(source)) {
    return source.map(item => (deepCopy(item))) as T & U[]
  }
  if (source instanceof Date) {
    return new Date(source.getTime()) as T & Date
  }
  if (source && typeof source === 'object') {
    return (Object.getOwnPropertyNames(source) as (keyof T)[]).reduce<T>((o, prop) => {
      Object.defineProperty(o, prop, Object.getOwnPropertyDescriptor(source, prop)!)
      o[prop] = deepCopy(source[prop])
      return o
    }, Object.create(Object.getPrototypeOf(source)))
  }
  return source
}

1
这个答案被低估了,它是唯一一个对我有用的。 - Mohamed Sakr

8

我的看法:

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 链接:https://repl.it/repls/CompetitivePreemptiveKeygen) - Aidin
此外,如果您稍后向 foo 添加属性,则该属性将自动显示在 clonedFoo 中!例如,foo.y = 9; console.log(clonedFoo.y) 将打印出 9 而不是 undefined。很可能这不是您要求的内容! - Aidin
@Aidin 那么如何确保深拷贝? - Muhammad Ali
在这个问题中,任何其他的解决方案都是通过递归进行值复制(例如 marckassay 的 https://dev59.com/d14c5IYBdhLWcg3wOoO6#53025968),这确保了目标对象中没有源对象的引用被维护。 - Aidin

7
您也可以像这样做:

您还可以采取以下方法:

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

请确保在所有Entity子类中覆盖 clone 方法,否则会出现部分克隆对象。

this 的返回类型将始终与实例的类型相匹配。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接