Reputation: 4792
How do I deep clone an object of a user-defined class and also keep the object methods of the class?
For example, I have a Object class called Schedule
with member days: number[]
and function getWeekdays()
So if I want to make a new Schedule
object which would be a clone of an existing Schedule
with cloned properties and also to have the getWeekdays()
function how would I do that? I tried Object.assign()
but that only shallow-copies days
and I know JSON.parse()
won't work because I won't get the object methods. I tried lodash's _.cloneDeep()
but unfortunately the object that creates is missing the object methods.
Upvotes: 4
Views: 3969
Reputation: 81
Object.assign()
will keep the getWeekdays()
method if you bind the method to the object instead of its prototype with one of the following approaches:
⚠️ Binding methods directly to an object instead of its prototype is generally considered an antipattern- especially in cases where performance is a higher priority- since N
Schedule
s would reference N seperategetWeekend()
functions instead of referencing the singlegetWeekend()
function that would otherwise be shared by the prototype.
The first approach is to declare your method in the class
definition using an arrow function, like so:
class Schedule {
public days: Array<number> = [];
public getWeekdays = (): Array<number> => {
return this.days;
}
}
const clone = Object.assign({}, new Schedule());
The reason this works is twofold:
Object.assign()
copies an object's own properties but not its inherited properties.If you run console.log(new Schedule());
you can see the first point in action:
// with arrow function:
▼ Schedule {days: Array(0), getWeekdays: } ⓘ
▷ days: Array(0) []
▷ getWeekdays: () => { … }
▷ __proto__: Object { constructor: … }
// without arrow function:
▼ Schedule { days: Array(0) } ⓘ
▷ days: Array(0) []
▼ __proto__: Object { constructor: , getWeekdays: }
▷ constructor: class Schedule { … }
▷ getWeekdays: getWeekdays() { … }
▷ __proto__: Object { constructor: , __defineGetter__: , __defineSetter__: , … }
static
method?A static
method is bound not the the object's prototype, but to the class
itself, which is the prototype's constructor:
class Schedule {
public static days: Array<number> = [];
public static getWeekdays(): Array<number> {
return this.days;
}
}
const clone = Object.assign({}, new Schedule());
console.log(new Schedule());
// console
▼ Schedule {} ⓘ
▼ __proto__: Object { constructor: … }
▼ constructor: class Schedule { … }
[[FunctionLocation]]: internal#location
▷ [[Scopes]]: Scopes[1]
arguments: …
caller: …
▷ days: Array(0) []
▷ getWeekdays: getWeekdays() { … }
length: 0
name: "Schedule"
▷ prototype: Object { constructor: … }
▷ __proto__: function () { … }
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
This means that a static
method may not be bound directly to an object. If you try, you'll get this TSError:
~/dev/tmp/node_modules/ts-node/src/index.ts:261
return new TSError(diagnosticText, diagnosticCodes)
^
TSError: ⨯ Unable to compile TypeScript:
index.ts(14,14): error TS2334: 'this' cannot be referenced in a static property initializer.
at createTSError (~/dev/tmp/node_modules/ts-node/src/index.ts:261:12)
at getOutput (~/dev/tmp/node_modules/ts-node/src/index.ts:367:40)
at Object.compile (~/dev/tmp/node_modules/ts-node/src/index.ts:558:11)
at Module._compile (~/dev/tmp/node_modules/ts-node/src/index.ts:439:43)
at internal/modules/cjs/loader.js:733:10
at Object..ts (~/dev/tmp/node_modules/ts-node/src/index.ts:442:12)
at Module.load (internal/modules/cjs/loader.js:620:32)
at tryModuleLoad (internal/modules/cjs/loader.js:560:12)
at Function._load (internal/modules/cjs/loader.js:552:3)
at Function.runMain (internal/modules/cjs/loader.js:775:12)
.bind()
in the constructorArrow functions (including those used in class
method definitions) are an ES6 feature which provide a more concise syntax to function declaration expressions in regards to the behavior of the this
keyword. Unlike regular functions, arrow functions use the this
value of their enclosing lexical scope rather than establishing their own this
value based on the context of their invocation. They also do not receive their own arguments
object (or super
, or new.target
).
Prior to ES6, if you needed to use this
in a method being used as a callback, you'd have to bind the host object's value of this
to the method's value of this
with .bind()
, which returns an updated function with its this
value set to the provided value, like so:
var clone;
function Schedule() {
this.days = [];
this.setWeekdays = function(days) {
this.days = days;
}
this.setWeekdays = this.setWeekdays.bind(this);
}
clone = Object.assign({}, new Schedule());
console.log(clone);
// console
▼ Object {days: Array(0), setWeekdays: }
▷ days:Array(0) []
▷ setWeekdays:function () { … }
▷ __proto__:Object {constructor: , __defineGetter__: , __defineSetter__: , …}
In an ES6 class
, you can achieve the same results by calling .bind()
on the method in the constructor:
class Schedule {
public days: Array<number> = [];
constructor() {
this.getWeekdays = this.getWeekdays.bind(this);
}
public getWeekdays(): Array<number> {
return this.days;
}
}
const clone = Object.assign({}, new Schedule());
console.log(clone);
// console
▼ Object {days: Array(0), setWeekdays: … } ⓘ
▷ days: Array(0) []
▷ setWeekdays: function () { … }
▷ __proto__: Object { constructor: , __defineGetter__: , __defineSetter__: , … }
⚠️ Also not necessarily recommended since you end up allocating functions which are usually never called, as explained below.
Decorators are considered an experimental feature in TypeScript and require that you set experimentalDecorators
to true
explicitly in your tsconfig.json
.
Using an autobind decorator would allow you to rebind the getWeekdays()
method "on demand"- just like using the .bind()
key in the constructor but the binding occurs when getWeekdays()
is invoked instead of when new Schedule()
is called- only in a more compact way:
class Schedule {
public days: Array<number> = [];
@bound
public getWeekdays(): Array<number> {
return this.days;
}
}
However, because Decorators are still in Stage 2, enabling decorators in TypeScript only exposes interfaces for the 4 types of decorator functions (i.e. ClassDecorator
, PropertyDecorator
, MethodDecorator
, ParameterDecorator
.) The built-in decorators proposed in Stage 2, including @bound
, are not included out of the box.
In order to use @bound
, you'll have to let Babel handle your TypeScript transpilation with @babel/preset-typescript
along with @babel/preset-stage-2
.
Alternatively, this functionality can be (somewhat) polyfilled with this NPM package:
This package's @boundMethod
will bind the getWeekdays()
method to the resulting object of new Schedule()
in addition to its prototype but will not be copied by Object.assign()
:
// console.log(new Schedule());
▼ Schedule { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ getWeekdays: function () { … }
▼ __proto__: Object { constructor: , getWeekdays: <accessor> }
▷ constructor: class Schedule { … }
▷ getWeekdays: getWeekdays() { … }
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
// console.log(clone);
▼ Object { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
This is because the @boundMethod
decorator overrides the method's get
and set
accessors to call .bind()
(since the value of this
in these accessors is set to the object through which the property is assigned), attach it to the object with Object.defineProperty()
, then return the PropertyDescriptor
for the bound method, which has some interesting effects:
const instance = new Schedule();
console.log('instance:', instance);
console.log('\ninstance.hasOwnProperty(\'getWeekdays\'):', instance.hasOwnProperty('getWeekdays'));
console.log('\ninstance.getWeekdays():', instance.getWeekdays());
console.log('\ninstance.hasOwnProperty(\'getWeekdays\'):', instance.hasOwnProperty('getWeekdays'));
// console
instance:
▼ Schedule { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ getWeekdays: function () { … }
▷ __proto__: Object { constructor: , getWeekdays: <accessor> }
instance.hasOwnProperty('getWeekdays'): false
instance.getWeekdays():
▷ Array(0) []
instance.hasOwnProperty('getWeekdays'): true
The reason Object.assign()
won't work is actually twofold:
[[Get]]
on the source object (i.e. new Schedule()
) and [[Set]]
on the target object (i.e. {}
).PropertyDescriptor
s that @boundMethod
uses to overhaul getWeekend()
s accessors are not enumerable.If we were to change that last point and use enumerable accessors, we could get Object.assign()
to work, but only after getWeekdays()
has already been invoked at least once:
const instance = new Schedule();
const clone1 = Object.assign({}, instance);
void instance.getWeekdays();
const clone2 = Object.assign({}, instance);
console.log('clone1:', clone1);
console.log('clone2:', clone2);
// console
clone1:
▼ Object { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
clone2:
▼ Object { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ getWeekdays: function () { … }
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
Upvotes: 5
Reputation: 3181
You need to first serialize the object into JSON, make a deep clone of the result, and then deserialize it back into the class object. You can use a library such as this: https://github.com/typestack/class-transformer
So in the end it would look like this:
import { classToPlain, plainToClass } from "class-transformer";
let a = new Schedule();
let aSerialized = classToPlain(a);
let b = plainToClass(Schedule, aSerialized);
Or you can use the classToClass
method:
import { classToClass } from "class-transformer";
let b = classToClass(a);
The gotcha is that you have to annotate the class with some annotations from the above library, but I don't think there's a better way to do it.
Upvotes: 1
Reputation: 421
Try the copy
function from here
// from https://www.codementor.io/avijitgupta/deep-copying-in-js-7x6q8vh5d
function copy(o) {
var output, v, key;
output = Array.isArray(o) ? [] : {};
for (key in o) {
v = o[key];
output[key] = (typeof v === "object") ? copy(v) : v;
}
return output;
}
var Event = /** @class */ (function () {
function Event(name) {
this.name = name;
}
Event.prototype.getName = function () {
return "Event " + this.name;
};
return Event;
}());
var Schedule = /** @class */ (function () {
function Schedule() {
}
Schedule.prototype.getWeekdays = function () {
return this.weekDays;
};
return Schedule;
}());
var schedule = new Schedule();
schedule.days = [3, 11, 19];
schedule.weekDays = [1, 2, 3];
schedule.event = new Event("Event");
var clone = copy(schedule);
console.log(clone);
Upvotes: 1