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.
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.
- What if you just add a copy constructor and recreate the instance that way? Oh, you only have a type... well, what if you create a class for your object? – pushkin Commented Jun 11, 2018 at 19:43
-
@pushkin Sorry,
Schedule
is actually a class -- I'll fix the question. But yes I guess I could use a copy constructor but a generalized solution would be nicer if possible – Kyle V. Commented Jun 11, 2018 at 20:05
3 Answers
Reset to default 5Object.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.
Arrow function methods
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());
...but why?
The reason this works is twofold:
- because the arrow function syntax binds the method to the resulting object instead of its prototype.
- because
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__: , … }
How does this differ from a 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 pile 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.pile (~/dev/tmp/node_modules/ts-node/src/index.ts:558:11)
at Module._pile (~/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 constructor
Arrow 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__: , … }
Future Bonus: Autobind Decorators
⚠️ Also not necessarily remended 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 pact 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:
- autobind-decorator
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:
- it actually invokes
[[Get]]
on the source object (i.e.new Schedule()
) and[[Set]]
on the target object (i.e.{}
). - the
PropertyDescriptor
s that@boundMethod
uses to overhaulgetWeekend()
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__: … , … }
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);
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./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.