Kyle V.
Kyle V.

Reputation: 4792

Deep clone a class object with 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.

Upvotes: 4

Views: 3969

Answers (3)

Rob H
Rob H

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 Schedules would reference N seperate getWeekend() functions instead of referencing the single getWeekend() 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 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 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 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:

  • it actually invokes [[Get]] on the source object (i.e. new Schedule()) and [[Set]] on the target object (i.e. {}).
  • the PropertyDescriptors 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

AsGoodAsItGets
AsGoodAsItGets

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

ernesthm
ernesthm

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

Related Questions