如何克隆一个JavaScript ES6类的实例

153

如何使用ES6克隆JavaScript类实例。

我不感兴趣使用基于jquery或$extend的解决方案。

我看到了相当古老的关于对象克隆的讨论,它表明问题相当复杂,但是在ES6中可以提供一个非常简单的解决方案-我将在下面给出并查看人们是否认为它是令人满意的。

编辑:有人建议我的问题是重复的;我看到了那个答案,但它已经有7年历史了,并且使用了使用ES6之前非常复杂的答案。我认为我的问题,允许使用ES6,有一个极大简化的解决方案。


4
如果您在Stack Overflow上的旧问题中有新的答案,请将该答案添加到原始问题中,而不是创建一个新的问题。 - Heretic Monkey
1
我确实看到Tom所面临的问题,因为ES6类实例与“常规”对象的工作方式不同。 - CherryNerd
2
此外,“可能重复”的答案中第一段代码在我尝试对ES6类的实例运行时实际上会导致崩溃。 - CherryNerd
我认为这不是重复的问题,因为虽然ES6类实例是一个对象,但并不是每个对象都是ES6类实例,因此其他问题并没有解决这个问题。 - Tomáš Zato
7
不是重复问题。另一个问题是关于作为数据容器使用的纯“Object”。这个问题涉及ES6“class”,并解决不丢失类类型信息的问题,需要不同的解决方案。 - flori
显示剩余2条评论
13个回答

190

这很复杂;我尝试了很多!最后,这个一行代码可以用于我的自定义ES6类实例:

let clone = Object.assign(Object.create(Object.getPrototypeOf(orig)), orig)

它避免设置原型,因为他们说它会大大降低代码速度。

它支持符号,但对于getter/setter并不完美,并且不能使用不可枚举的属性(请参见Object.assign()文档)。此外,基本内部类(如Array、Date、RegExp、Map等)的克隆似乎经常需要一些个体处理。

结论:这很混乱。希望有一天会有一个本地和干净的克隆功能。


2
这不会复制静态方法,因为它们实际上不是可枚举的自有属性。 - Mr. Lavalamp
7
@Mr.Lavalamp,你怎么复制(也包括)静态方法? - flori
1
@KeshaAntonov 你可以尝试使用typeof和Array方法来解决问题。我个人更喜欢手动克隆所有属性。 - Vahid
5
不要期望它能够克隆那些本身是对象的属性:https://jsbin.com/qeziwetexu/edit?js,console - jduhls
3
静态方法不需要被克隆!它们是类的一部分,而不是实例。 - pery mimon
显示剩余9条评论

26
const clone = Object.assign( {}, instanceOfBlah );
Object.setPrototypeOf( clone, Blah.prototype );

注意Object.assign的特点:它进行浅拷贝,不复制类方法。

如果你想要深拷贝或更多控制复制,则可以使用lodash克隆函数


3
既然Object.create可以创建指定原型的新对象,那为什么不直接使用const clone = Object.assign(Object.create(instanceOfBlah), instanceOfBlah)呢?此外,类方法也会被复制。 - barbatus
3
@barbatus 的原话使用了错误的原型,Blah.prototype != instanceOfBlah。你应该使用 Object.getPrototypeOf(instanceOfBlah) - Bergi
1
@Bergi 不是的,ES6类实例并不总是有原型。请查看 https://codepen.io/techniq/pen/qdZeZm ,它也适用于实例。 - barbatus
1
@barbatus 抱歉,什么?我不明白。所有实例都有一个原型,这就是使它们成为实例的原因。尝试使用flori答案中的代码。 - Bergi
1
@Bergi 我认为这取决于 Babel 的配置或其他因素。我现在正在实现一个响应式原生应用程序,没有继承属性的实例在那里原型为空。另外,正如您在这里所看到的 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf ,getPrototypeOf 可能返回 null。 - barbatus
显示剩余8条评论

21

我喜欢几乎所有的答案。我曾经遇到过这个问题,为了解决它,我会手动定义一个clone()方法,在其中建立整个对象。对我来说,这很有意义,因为结果对象将自然地与克隆对象相同类型。

typescript示例:

export default class ClassName {
    private name: string;
    private anotherVariable: string;
   
    constructor(name: string, anotherVariable: string) {
        this.name = name;
        this.anotherVariable = anotherVariable;
    }

    public clone(): ClassName {
        return new ClassName(this.name, this.anotherVariable);
    }
}

我喜欢这个解决方案,因为它看起来更像“面向对象”


6
这确实是正确的方向。要得到一个通用的克隆机制非常困难,对于每种情况正确地使其工作是不可能的。总会存在着奇怪和不一致的类。因此,确保对象本身是可克隆的是唯一确定的方式。作为替代选择(或补充),可以有一个从实例中进行克隆的方法,类似于 public static clone(instance: MyClass): MyClass),它具有特定处理克隆的相同想法,只是将其外部化到实例之外。 - VLAZ
4
这是一个很好的回答,上面的评论也提出了一个很好的建议。还值得指出的是,对象和数组属性是按引用传递的,因此您需要克隆它们,否则可能会遇到意外的副作用!这里有一个代码片段供参考:https://gist.github.com/sscovil/def81066dc59e6ff5084a499d9855253 - Shaun Scovil
这里最好的答案! - Frederik Krautwald
这确实看起来很面向对象。也就是说,过于啰嗦和“重复自己4次”在扩展上。如果TypeScript能够提供默认实现克隆的合适数据类,那就太好了。 - undefined

5

简而言之;

// Use this approach
//Method 1 - clone will inherit the prototype methods of the original.
    let cloneWithPrototype = Object.assign(Object.create(Object.getPrototypeOf(original)), original); 

在JavaScript中,不建议进行原型(Prototype)的扩展。这样做会在对代码/组件进行测试时导致问题。单元测试框架将不会自动假定你的原型扩展。因此,这不是一个好的实践方法。 这里有更多关于原型扩展的解释:为什么扩展本地对象是一种不好的实践? 在JavaScript中,没有一种简单或直接的方式来克隆对象。以下是第一个实例使用“浅拷贝”(Shallow Copy):
1 -> 浅拷贝(Shallow clone):
class Employee {
    constructor(first, last, street) {
        this.firstName = first;
        this.lastName = last;
        this.address = { street: street };
    }

    logFullName() {
        console.log(this.firstName + ' ' + this.lastName);
    }
}

let original = new Employee('Cassio', 'Seffrin', 'Street A, 23');

//Method 1 - clone will inherit the prototype methods of the original.
let cloneWithPrototype = Object.assign(Object.create(Object.getPrototypeOf(original)), original); 

//Method 2 - object.assing() will not clone the Prototype.
let cloneWithoutPrototype =  Object.assign({},original); 

//Method 3 - the same of object assign but shorter syntax using "spread operator"
let clone3 = { ...original }; 

//tests
cloneWithoutPrototype.firstName = 'John';
cloneWithoutPrototype.address.street = 'Street B, 99'; //will not be cloned

结果:

original.logFullName();

结果:Cassio Seffrin

cloneWithPrototype.logFullName();

结果:Cassio Seffrin

original.address.street;

结果:'Street B, 99' // 注意原始子对象已更改

注意:如果实例具有闭包作为自己的属性,则此方法不会将其包装。(了解有关闭包的更多信息) 而且,子对象“address”将不会被克隆。

cloneWithoutPrototype.logFullName()

将不起作用。 克隆品不会继承原型的任何方法。

cloneWithPrototype.logFullName()

将起作用,因为克隆品还将复制其原型。

使用Object.assign克隆数组:

let cloneArr = array.map((a) => Object.assign({}, a));

使用ECMAScript扩展语法克隆数组:

let cloneArrSpread = array.map((a) => ({ ...a }));

2 -> 深度克隆:

为了获得一个全新的对象引用,我们可以使用JSON.stringify()将原始对象解析为字符串,然后再使用JSON.parse()将其解析回来。

let deepClone = JSON.parse(JSON.stringify(original));

深度克隆将保留对地址的引用。但是,deepClone原型将会丢失,因此deepClone.logFullName()将无法正常工作。

3 ->第三方库:

另一种选择是使用第三方库,如loadash或underscore。 它们将创建一个新对象,并将原始对象中的每个值复制到新对象中,在内存中保留其引用。

Underscore: let cloneUnderscore = _(original).clone();

Loadash clone: var cloneLodash = _.cloneDeep(original);

lodash或underscore的缺点在于需要在项目中包含一些额外的库。然而,它们是不错的选择,也能产生高性能的结果。


3
赋值给{}时,克隆对象将不继承原始对象的任何原型方法。clone.logFullName() 将完全无法运作。你之前使用的 Object.assign( Object.create(Object.getPrototypeOf(eOriginal)), eOriginal) 是可以的,为什么要改变它呢? - Bergi
1
@Bergi 感谢您的贡献,我正在编辑我的答案,我已经添加了您的建议来复制原型! - Cassio Seffrin
2
我感激你的帮助@Bergi,请现在给出你的意见。我已经完成了编辑。我认为现在答案几乎覆盖了所有问题。谢谢! - Cassio Seffrin
1
是的,就像 Object.assign({},original) 一样,它也不起作用。 - Bergi
2
有时候,我们只需要更简单的方法。如果您不需要原型和复杂对象,只需使用“clone = { ...original }”即可解决问题。 - Cassio Seffrin
显示剩余3条评论

2
使用与原始对象相同的原型和属性创建对象的副本。
function clone(obj) {
  return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
}

可以处理非枚举属性、getter、setter等。但无法克隆内部插槽,许多内置的JavaScript类型都有这些插槽(例如Array、Map、Proxy)。


2
这是一个不错的方法,因为它将所有处理工作委托给了JavaScript。然而,它在任何潜在的对象值方面存在问题,因为它们将在原始对象和克隆对象之间共享。例如,数组值将被两个实例同时更新。 - VLAZ

1
如果我们有多个继承自彼此的类,克隆每个实例的最佳解决方案是在其类定义中定义一个函数,用于创建该对象的新实例,如下所示:

如果我们有多个继承自彼此的类,克隆每个实例的最佳解决方案是在其类定义中定义一个函数,用于创建该对象的新实例,如下所示:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  clone() {
    return new Point(this.x, this.y);
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color;
  }
  clone() {
    return new ColorPoint(
      this.x, this.y, this.color.clone()); // (A)
  }
}

现在我可以使用对象的clone(0函数,例如:
let p = new ColorPoint(10,10,'red');
let pclone=p.clone();

类型错误:this.color.clone 不是一个函数。 - lawrence-witt

0

试试这个:

function copy(obj) {
   //Edge case
   if(obj == null || typeof obj !== "object") { return obj; }

   var result = {};
   var keys_ = Object.getOwnPropertyNames(obj);

   for(var i = 0; i < keys_.length; i++) {
       var key = keys_[i], value = copy(obj[key]);
       result[key] = value;
   }

   Object.setPrototypeOf(result, obj.__proto__);

   return result;
}

//test
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
};

var myPoint = new Point(0, 1);

var copiedPoint = copy(myPoint);

console.log(
   copiedPoint,
   copiedPoint instanceof Point,
   copiedPoint === myPoint
);

Since it uses Object.getOwnPropertyNames, it will also add non-enumerable properties.


-2

class A {
  constructor() {
    this.x = 1;
  }

  y() {
    return 1;
  }
}

const a = new A();

const output =  Object.getOwnPropertyNames(Object.getPrototypeOf(a))
  .concat(Object.getOwnPropertyNames(a))
  .reduce((accumulator, currentValue, currentIndex, array) => {
    accumulator[currentValue] = a[currentValue];
    return accumulator;
  }, {});
  
console.log(output);

enter image description here


1
这里有两个问题 - 1. 这会丢失类信息 - output instanceof Afalse。2. 克隆只在原型链上向上一级,如果有一个 class B extends A { b() { return 2; }}class C extends B { c() { return 3; }},那么“克隆”一个 C 实例最终只会复制 b()c(),但不会复制 A 的属性 (y)。属性 x 之所以会被复制,是因为它在构造函数中直接设置在实例上。 - VLAZ

-2

另一个一行代码:

大多数情况下...(适用于Date、RegExp、Map、String、Number、Array),顺便说一下,克隆字符串和数字有点有趣。

let clone = new obj.constructor(...[obj].flat())

对于那些没有复制构造函数的类:

let clone = Object.assign(new obj.constructor(...[obj].flat()), obj)

fn(...[obj].flat()) === fn(obj) 没有真正的理由去使用额外的 1. 数组,2. 将其扁平化为只有一个成员的数组。3. 将该单个成员展开为一个参数。即使这样,这仅适用于具有复制构造函数的类型。第二个版本不一定适用于没有复制构造函数的类 - 它甚至可能导致错误,考虑 constructor(a, b) { this.c = a + b },它通常期望数字但对于 a 得到了自身的实例,对于 b 得到了 undefined - VLAZ

-3

这样做不够吗?

Object.assign(new ClassName(), obj)

这取决于类。如果它很简单,那么这可能已经足够了。但是构造函数中的代码呢?它做什么以及您是否希望在克隆此对象时运行它?箭头函数之类的闭包又怎么办?这些你无法复制,否则this将指向旧实例,然后还有私有字段...很多陷阱。 - Thomas
好的,我正在这样使用,我想在我的情况下这已经足够了。 - Danny

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