[英]How to clone a javascript ES6 class instance
如何使用 ES6 克隆 Javascript 類實例。
我對基於 jquery 或 $extend 的解決方案不感興趣。
我已經看到關於對象克隆的相當古老的討論表明這個問題非常復雜,但是在 ES6 中出現了一個非常簡單的解決方案 - 我將把它放在下面,看看人們是否認為它令人滿意。
編輯:有人建議我的問題是重復的; 我看到了那個答案,但它已有 7 年歷史,並且涉及使用 ES6 之前的 js 的非常復雜的答案。 我建議我的問題,它允許 ES6,有一個非常簡單的解決方案。
這很復雜; 我嘗試了很多! 最后,這個單行代碼適用於我的自定義 ES6 類實例:
let clone = Object.assign(Object.create(Object.getPrototypeOf(orig)), orig)
它避免了設置原型,因為他們說這會大大降低代碼速度。
它支持符號,但對於 getter/setter 並不完美,並且不適用於不可枚舉的屬性(請參閱Object.assign() 文檔)。 此外,可悲的是,克隆基本的內部類(如 Array、Date、RegExp、Map 等)通常似乎需要一些單獨的處理。
結論:一團糟。 讓我們希望有一天會有一個原生且干凈的克隆功能。
const clone = Object.assign( {}, instanceOfBlah );
Object.setPrototypeOf( clone, Blah.prototype );
注意Object.assign的特點:它只做淺拷貝,不拷貝類方法。
如果你想要一個深拷貝或對拷貝有更多的控制,那么有lodash 克隆功能。
我喜歡幾乎所有的答案。 我遇到了這個問題,為了解決這個問題,我將通過定義一個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);
}
}
我喜歡這個解決方案,因為它看起來更“面向對象”
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 中,不建議對 Prototype 進行擴展,當您對代碼/組件進行測試時會導致問題。 單元測試框架不會自動假設您的原型擴展。 所以這不是一個好習慣。 這里有更多關於原型擴展的解釋為什么擴展原生對象是一種不好的做法?
要在 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();
結果:卡西歐·塞弗林
cloneWithPrototype.logFullName();
結果:卡西歐·塞弗林
original.address.street;
result: 'Street B, 99' // 注意原來的子對象被改變了
注意:如果實例將閉包作為自己的屬性,則此方法不會包裝它。 ( 閱讀更多關於閉包的信息)另外,子對象“地址”不會被克隆。
cloneWithoutPrototype.logFullName()
不管用。 克隆不會繼承原始的任何原型方法。
cloneWithPrototype.logFullName()
會起作用,因為克隆也會復制它的原型。
使用 Object.assign 克隆數組:
let cloneArr = array.map((a) => Object.assign({}, a));
使用 ECMAScript 傳播 sintax 克隆數組:
let cloneArrSpread = array.map((a) => ({ ...a }));
2 -> 深度克隆:
要歸檔一個全新的對象引用,我們可以使用 JSON.stringify() 將原始對象解析為字符串,然后將其解析回 JSON.parse()。
let deepClone = JSON.parse(JSON.stringify(original));
通過深度克隆,將保留對地址的引用。 但是 deepClone 原型將丟失,因此 deepClone.logFullName() 將不起作用。
3 -> 3th 方庫:
另一種選擇是使用像 loadash 或 underscore 這樣的 3th 方庫。 他們將創建一個新對象並將每個值從原始對象復制到新對象,並將其引用保存在內存中。
下划線:讓 cloneUnderscore = _(original).clone();
Loadash 克隆: var cloneLodash = _.cloneDeep(original);
lodash 或 underscore 的缺點是需要在項目中包含一些額外的庫。 然而,它們是很好的選擇,也能產生高性能的結果。
使用與原始對象相同的原型和相同的屬性創建對象的副本。
function clone(obj) {
return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
}
適用於不可枚舉的屬性、getter、setter 等。無法克隆許多內置 javascript 類型(例如數組、映射、代理)具有的內部插槽
如果我們有多個相互擴展的類,則克隆每個實例的最佳解決方案是定義一個函數來在其類定義中創建該對象的新實例,例如:
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();
嘗試這個:
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
,它還將添加不可枚舉的屬性。
另一個班輪:
大多數時候......(適用於日期、正則表達式、映射、字符串、數字、數組),順便說一句,克隆字符串,數字有點有趣。
let clone = new obj.constructor(...[obj].flat())
對於那些沒有復制構造函數的類:
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);
這樣做還不夠嗎?
Object.assign(new ClassName(), obj)
我用過lodash。
import _ from 'lodash'
class Car {
clone() { return _.cloneDeep(this); }
}
這是對 OP 的更完整的答案,因為到目前為止收到的所有答案都存在問題(並不是說它們有時不適用於不同的情況和場景,它們只是不是最簡單的通用答案ES6 按要求)。 為后人。
正如回答者所指出的,Object.assign() 只會做一個淺拷貝。 這實際上是一個大問題,因為 javascript 垃圾收集僅在從原始對象中刪除所有引用時才有效。 這意味着即使對於很少更改的簡單布爾值,任何引用舊對象的克隆都意味着潛在的嚴重內存泄漏。
使用“clone()”方法擴展的類具有與 Object.assign() 相同的垃圾收集問題,如果您創建新實例可能會引用舊實例,即使對象中存在 1 個數據子樹。 這將很難單獨管理。
使用擴展運算符(“...”)也是數組/對象的淺拷貝,與上面的引用和唯一性問題相同。 另外,正如在回答答案中提到的那樣,無論如何這都會丟失原型和類
原型肯定是較慢的方法,但我相信 V8 已經解決了該方法的性能問題,所以我不確定它在 2022 年是否會成為問題。
2022 年的建議答案:正確編寫深拷貝腳本以獲取所有類對象數據。 當想要克隆一個類對象時,創建一個臨時容器並將類對象的深層復制到臨時容器中。 編寫一個包含所有方法的父類(超類),以及對象數據和實例所需的子類。 然后,當從擴展子類調用父類的方法時,將子類的“this”作為參數傳入,並在父類的方法中捕獲該參數(例如,我使用“that”這個詞)。 最后,當您將對象數據克隆到臨時對象中時,為您要克隆的所有對象創建新實例,並將對舊實例的任何引用替換為新實例,以確保它不會在內存中逗留。 例如,在我的示例中,我正在制作 Conway 的 Game of Life 的 hacky 版本。 我會有一個名為“allcells”的數組,然后在每個 requestAnimationFrame(renderFunction) 上更新它時,我會將所有單元格深度復制到 temp 中,運行每個單元格的 update(this) 方法,該方法調用父級的 update(that) 方法,然后創建新的 Cell( temp[0].x、temp[0].y 等)並將所有這些打包到一個數組中,在所有更新完成后,我可以用它替換舊的“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()
您可以使用擴展運算符,例如,如果您想克隆一個名為 Obj 的對象:
let clone = { ...obj};
如果您想更改或向克隆對象添加任何內容:
let clone = { ...obj, change: "something" };
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.