如何克隆 javascript ES6 class 实例

How to clone a javascript ES6 class instance

如何使用 ES6 克隆 Javascript class 实例。

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

我已经看到关于对象克隆的很早的讨论表明这个问题很复杂,但是对于 ES6 来说,一个非常简单的解决方案本身就出现了——我将把它放在下面,看看人们是否认为它令人满意。

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

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

注意Object.assign的特点:浅拷贝,不拷贝class方法

如果你想要深拷贝或对拷贝有更多的控制,那么有lodash clone functions

很复杂;我试了很多!最后,这个单行代码适用于我的自定义 ES6 class 个实例:

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

它避免设置原型,因为 they say 它会大大降低代码速度。

它支持符号,但不适合 getters/setters,并且不能使用不可枚举的属性(参见 Object.assign() docs)。此外,克隆基本的内部 classes(如 Array、Date、RegExp、Map 等)遗憾的是似乎经常需要一些单独的处理。

结论:一团糟。让我们希望有一天会有一个本机和干净的克隆功能。

TLDR;

// 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 中不建议对原型进行扩展,这会导致在 code/components 上进行测试时出现问题。单元测试框架不会自动假设你的原型扩展。所以这不是一个好习惯。 这里有更多关于原型扩展的解释Why is extending native objects a bad practice?

要克隆 JavaScript 中的对象,没有简单或直接的方法。这是使用“浅拷贝”的第一个实例:

1 -> 浅克隆:

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

result: Cassio Seffrin

cloneWithPrototype.logFullName();

result: Cassio Seffrin

original.address.street;

result: 'Street B, 99' // notice that original sub object was changed

注意:如果实例将闭包作为自己的属性,则此方法不会包装它。 (read more about closures)另外,子对象“地址”不会被克隆。

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 -> 第 3 方库:

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

下划线: 让 cloneUnderscore = _(original).clone();

Loadash 克隆: var cloneLodash = _.cloneDeep(原版);

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

您可以使用扩展运算符,例如,如果您想克隆一个名为 Obj 的对象:

let clone = { ...obj};

如果您想更改克隆对象或向其添加任何内容:

let clone = { ...obj, change: "something" };

另一种班轮:

大多数时候...(适用于 Date、RegExp、Map、String、Number、Array),顺便说一句,克隆字符串、数字有点搞笑。

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

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

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

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

几乎所有答案我都喜欢。我遇到了这个问题,为了解决它,我会通过定义一个 clone() 方法并在其中手动完成,我会从头开始构建整个对象。对我来说,这是有道理的,因为生成的对象自然会与克隆对象属于同一类型。

打字稿示例:

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

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

使用与原始对象相同的原型和相同的属性创建对象的副本。

function clone(obj) {
  return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
}

适用于不可枚举的属性、getter、setter 等。无法克隆许多内置 javascript 类型(例如 Array、Map、Proxy)具有的内部插槽

试试这个:

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
);
由于它使用了Object.getOwnPropertyNames,它还会添加不可枚举的属性。

如果我们有多个 class 相互扩展,克隆每个实例的最佳解决方案是定义一个函数来在其 class 定义中创建该对象的新实例,例如:

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

现在我可以使用对象的克隆(0 函数,例如:

let p = new ColorPoint(10,10,'red');
let pclone=p.clone();

这样做还不够吗?

Object.assign(new ClassName(), obj)

我用的是 lodash。

import _ from 'lodash'
class Car {
    clone() { return _.cloneDeep(this); }
}

这是对 OP 的更完整的回答,因为到目前为止收到的所有回答都存在问题(并不是说它们有时不适用于不同的案例和场景,只是它们不是根据要求仅使用 ES6 的最简单的通用答案)。为了子孙后代。

Object.assign() 只会进行浅拷贝,如 answer-er 所述。这实际上是一个大问题,因为 javascript 垃圾收集仅在所有引用都从原始对象中删除时才有效。这意味着任何引用旧对象的克隆,即使是很少更改的简单布尔值,也意味着潜在的严重内存泄漏。

Class 使用“clone()”方法扩展具有与 Object.assign() 相同的垃圾收集问题,如果您正在创建可能引用旧实例的新实例,即使 1 sub-tree 对象中存在数据。这很难单独管理。

使用扩展运算符(“...”)也是arrays/objects的浅拷贝,与上面的引用和唯一性问题相同。此外,正如对答案的回应中也提到的那样,这会丢失原型并且 class 无论如何

原型绝对是较慢的方法,但我相信 V8 已经解决了这种方法的性能问题,所以我不确定它在 2022 年是否还会成为问题。

2022 年的建议答案:正确编写深层复制脚本以获取所有 class 对象数据。当想要克隆一个 class 对象时,创建一个临时容器并将 class 对象深拷贝到临时容器中。写一个包含所有方法的父 class (superclass),以及你想要的对象数据和实例的子class。然后当从扩展 subclass 调用父方法时,将 subclass 的“this”作为参数传入,并在父方法中捕获该参数(我使用“that”这个词,例如)。最后,当您将对象数据克隆到临时对象中时,为您要克隆的所有对象创建新实例,并将对旧实例的任何引用替换为新实例,以确保它不会在内存中逗留。例如,在我的示例中,我正在制作康威生命游戏的 hacky 版本。我会有一个名为“allcells”的数组,然后在每个 requestAnimationFrame(renderFunction) 上更新它时,我会将 allcells 深度复制到 temp,运行 每个单元格的 update(this) 方法调用父级的 update(that) 方法,然后创建新的 Cell(temp[0].x, temp[0].y, etc) 并将所有这些打包到一个数组中,我可以在完成所有更新后用它替换旧的“allcells”容器。在生命游戏示例中,如果不在临时容器中进行更新,前一个更新会影响同一时间步内后一个更新的输出,这可能是不可取的。

完成!没有 lodash,没有打字稿,没有 jQuery,只是 ES6 所要求的和通用的。它看起来很粗糙,但是如果你编写一个通用的 recursiveCopy() 脚本,你可以轻松地编写一个函数来使用它来创建一个 clone() 函数,如果你想遵循我上面概述的步骤并使用下面的示例代码作为参考.

function recursiveCopy(arr_obj){
    if(typeof arr_obj === "object") {
        if ( Array.isArray(arr_obj) ) {
            let result = []
            // if the current element is an array
            arr_obj.forEach( v => { result.push(recursiveCopy(v)) } )
            return result 
        }
        else {
            // if it's an object by not an array then it’s an object proper { like: “so” }
            let result = {}
            for (let item in arr_obj) {
                result[item] = recursiveCopy(arr_obj[item]) // in case the element is another object/array
            }
            return result
        }
    }
    // above conditions are skipped if current element is not an object or array, so it just returns itself
    else if ( (typeof arr_obj === "number") || (typeof arr_obj === "string") || (typeof arr_obj === "boolean") ) return arr_obj
    else if(typeof arr_obj === "function") return console.log("function, skipping the methods, doing these separately")
    else return new Error( arr_obj ) // catch-all, likely null arg or something
}

// PARENT FOR METHODS
class CellMethods{
    constructor(){
        this.numNeighboursSelected = 0
    }

    // method to change fill or stroke color
    changeColor(rgba_str, str_fill_or_stroke, that) {
        // DEV: use switch so we can adjust more than just background and border, maybe text too
        switch(str_fill_or_stroke) {
        case 'stroke':
            return that.border = rgba_str
        default:      // fill is the default
            return that.color = rgba_str
        }
    }

    // method for the cell to draw itself
    drawCell(that){
        // save existing values
        let tmp_fill = c.fillStyle
        let tmp_stroke = c.strokeStyle
        let tmp_borderwidth = c.lineWidth
        let tmp_font = c.font
        
        // fill and stroke cells
        c.fillStyle = (that.isSelected) ? highlightedcellcolor : that.color
        c.strokeStyle = that.border
        c.lineWidth = border_width
        c.fillRect(that.x, that.y, that.size.width, that.size.height)
        c.strokeRect(that.x, that.y, that.size.width+border_width, that.size.height+border_width)
        
        // text id labels
        c.fillStyle = that.textColor
        c.font = `${that.textSize}px Arial`
        c.fillText(that.id, that.x+(cellgaps*3), that.y+(that.size.height-(cellgaps*3)))
        c.font = tmp_font

        // restore canvas stroke and fill
        c.fillStyle = tmp_fill
        c.strokeStyle = tmp_stroke
        c.lineWidth = tmp_borderwidth    
    }
    checkRules(that){
        console.log("checking that 'that' works: " + that)
        if ((that.leftNeighbour !== undefined) && (that.rightNeighbour !== undefined) && (that.topNeighbour !== undefined) && (that.bottomNeighbour !== undefined) && (that.bottomleft !== undefined) && (that.bottomright !== undefined) && (that.topleft !== undefined) && (that.topright !== undefined)) {
            that.numNeighboursSelected = 0
            if (that.leftNeighbour.isSelected) that.numNeighboursSelected++
            if (that.rightNeighbour.isSelected) that.numNeighboursSelected++
            if (that.topNeighbour.isSelected) that.numNeighboursSelected++
            if (that.bottomNeighbour.isSelected) that.numNeighboursSelected++
            // // if my neighbours are selected
            if (that.numNeighboursSelected > 5) that.isSelected = false
        }
    }
}

// write a class to define structure of each cell
class Cell extends CellMethods{
    constructor(id, x, y, selected){
        super()
        this.id = id
        this.x = x
        this.y = y
        this.size = cellsize
        this.color = defaultcolor
        this.border = 'rgba(0,0,0,1)'
        this.textColor = 'rgba(0,0,0,1)'
        this.textSize = cellsize.height/5     // dynamically adjust text size based on the cell's height, since window is usually wider than it is tall
        this.isSelected = (selected) ? selected : false
    }
    changeColor(rgba_str, str_fill_or_stroke){ super.changeColor(rgba_str, str_fill_or_stroke, this)} // THIS becomes THAT
    checkRules(){ super.checkRules(this) } // THIS becomes THAT
    drawCell(){ super.drawCell(this) } // THIS becomes THAT
}

let [cellsincol, cellsinrow, cellsize, defaultcolor] = [15, 10, 25, 'rgb(0,0,0)'] // for building a grid
// Bundle all the cell objects into an array to pass into a render function whenever we want to draw all the objects which have been created
function buildCellTable(){
    let result = []  // initial array to push rows into
    for (let col = 0; col < cellsincol; col++) {  // cellsincol aka the row index within the column
    let row = []
    for (let cellrow = 0; cellrow < cellsinrow; cellrow++) {  // cellsinrow aka the column index
        let newid = `col${cellrow}_row${col}` // create string for unique id's based on array indices
        row.push( new Cell(newid, cellrow*(cellsize.width),col*(cellsize.height) ))
    }
    result.push(row)
    }
    return result
}

// poplate array of all cells, final output is a 2d array
let allcells = buildCellTable()

// create hash table of allcells indexes by cell id's
let cellidhashtable = {}
allcells.forEach( (v,rowindex)=>{
    v.forEach( (val, colindex)=>{
    cellidhashtable[val.id] = [rowindex, colindex]  // generate hashtable 
    val.allcellsposition = [rowindex, colindex]     // add cell indexes in allcells to each cell for future reference if already selected    
    } )
})

// DEMONSTRATION
let originalTable = {'arr': [1,2,3,4,5], 'nested': [['a','b','c'], ['d','e','f']], 'obj': {'nest_obj' : 'object value'}}
let newTable = recursiveCopy(originalTable) // works to copy
let testingDeepCopy = recursiveCopy(newTable)
let testingShallowCopy = {...newTable}  // spread operator does a unique instance, but references nested elements
newTable.arr.pop() // removes an element from a nested array after popping
console.log(testingDeepCopy)   // still has the popped value
console.log(testingShallowCopy)  // popped value is remove even though it was copies before popping

// DEMONSTRATION ANSWER WORKS
let newCell = new Cell("cell_id", 10, 20)
newCell.checkRules()