Convert javascript class instance to plain object preserving methods
Asked Answered
S

8

30

I want to convert an instance class to plain object, without losing methods and/or inherited properties. So for example:

class Human {
    height: number;
    weight: number;
    constructor() {
        this.height = 180;
        this.weight = 180;
    }
    getWeight() { return this.weight; }
    // I want this function to convert the child instance
    // accordingly
    toJSON() {
        // ???
        return {};
    }
}
class Person extends Human {
    public name: string;
    constructor() {
        super();
        this.name = 'Doe';
    }
    public getName() {
        return this.name;
    }
}
class PersonWorker extends Person {
    constructor() {
        super();
    }
    public report() {
        console.log('I am Working');
    }
    public test() {
        console.log('something');
    }
}
let p = new PersonWorker;
let jsoned = p.toJSON();

jsoned should look like this:

{
    // from Human class
    height: 180,
    weight: 180,
    // when called should return this object's value of weight property
    getWeight: function() {return this.weight},

    // from Person class
    name: 'Doe'
    getName(): function() {return this.name},

    // and from PersonWorker class
    report: function() { console.log('I am Working'); },

    test: function() { console.log('something'); }
}

Is this possible to achieve, and if so, how?

In case you're wondering, I need this because I am using a framework that, unfortunately, accepts as input only an object, whereas I am trying to use TypeScript and class inheritance.

Also, I am doing the above conversion once so performance isn't an issue to consider.

The solutions consisting of iterating through object properties will not work if the compiler's target option is set to es6. On es5, the existing implementations by iterating through object properties (using Object.keys(instance)) will work.

So far, I have this implementation:

toJSON(proto?: any) {
    // ???

    let jsoned: any = {};
    let toConvert = <any>proto || this;

    Object.getOwnPropertyNames(toConvert).forEach((prop) => {
        const val = toConvert[prop];
        // don't include those
        if (prop === 'toJSON' || prop === 'constructor') {
            return;
        }
        if (typeof val === 'function') {
            jsoned[prop] = val.bind(this);
            return;
        }
        jsoned[prop] = val;
        const proto = Object.getPrototypeOf(toConvert);
        if (proto !== null) {
            Object.keys(this.toJSON(proto)).forEach(key => {
                if (!!jsoned[key] || key === 'constructor' || key === 'toJSON') return;
                if (typeof proto[key] === 'function') {
                    jsoned[key] = proto[key].bind(this);
                    return;
                }
                jsoned[key] = proto[key];
            });
        }
    });
    return jsoned;
}

But this is still not working. The resulted object includes only all the properties from all classes but only methods from PersonWorker. What am I missing here?

Sporran answered 9/1, 2016 at 22:12 Comment(7)
@wmehanna Not quite. Yes, I am using es6 with babel. So the final output is es5 already. What I want is to get an object just like my in example, because the class instance is not the same with that object.Sporran
Why can't you pass the instance? You're basically just copying it.Catron
toJSON() {return Object.keys(this).reduce((obj, key) => {obj[key] = this[key]; return obj;}, {});}Catron
@Louy When targeting es6 via babel or node 4+ the old method with Object.keys doesn't workSporran
To be precise, this isn't converting to JSON, this is converting to an object. JSON is explicitly an object notation.Presbyterate
To follow up on @DaveNewton's comment, JSON is a text format, and there is no such thing as a JSON object, since objects are in the realm of JavaScript, not JSON. Note that toJSON is also a special method name, used by JSON.stringify if present to get the string representation of an object.Winterize
try using p.__proto__, if your still making the toJSON on classImprovement
A
14

This is what's working for me

Updated Answer (with recursion)

const keys = x => Object.getOwnPropertyNames(x).concat(Object.getOwnPropertyNames(x?.__proto__))
const isObject = v => Object.prototype.toString.call(v) === '[object Object]'

const classToObject = clss => keys(clss ?? {}).reduce((object, key) => {
  const [val, arr, obj] = [clss[key], Array.isArray(clss[key]), isObject(clss[key])]
  object[key] = arr ? val.map(classToObject) : obj ? classToObject(val) : val
  return object
}, {})

var classs = new Response()
var obj = classToObject(classs)
console.log({ obj, classs })

Original Answer

const classToObject = theClass => {
  const originalClass = theClass || {}
  const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(originalClass))
  return keys.reduce((classAsObj, key) => {
    classAsObj[key] = originalClass[key]
    return classAsObj
  }, {})
}

enter image description here

Autoionization answered 26/10, 2019 at 4:43 Comment(4)
That answer is awesome Man, I don't understand what it has been ignored. Well Done and thank for sharing!Coop
This answer looks nice and concise. But, my OP required nested classes to be also converted to plain javascript object (preserving methods). I think you need to call your fn recursively to solve that specific scenario as well. I'll mark your answer as the accepted one after you edit your solution to cover the nested scenario. Thanks for this! :)Sporran
@alex-cory if you solve the problem mentioned in my previous comment please notify me to change the accepted answer! Thanks!Sporran
@Sporran I updated to have recursionAutoionization
D
37

Lots of answers already, but this is the simplest yet by using the spread syntax and de-structuring the object:

const {...object} = classInstance
Decaffeinate answered 24/3, 2021 at 3:22 Comment(5)
Best answer ! <3Gorlin
It doesn't preserve methods.Jurywoman
@AleksandarBencun it actually does preserve methods, but the constructor is being overwritten because when using {...SomeClass} for spreading the class to a new object the new constructor will be the native Object constructor of JS objectsNoemi
This is not answering the question; OP said explicitly: without losing methods and/or inherited propertiesMartinelli
This does not perserve methodsHutson
A
14

This is what's working for me

Updated Answer (with recursion)

const keys = x => Object.getOwnPropertyNames(x).concat(Object.getOwnPropertyNames(x?.__proto__))
const isObject = v => Object.prototype.toString.call(v) === '[object Object]'

const classToObject = clss => keys(clss ?? {}).reduce((object, key) => {
  const [val, arr, obj] = [clss[key], Array.isArray(clss[key]), isObject(clss[key])]
  object[key] = arr ? val.map(classToObject) : obj ? classToObject(val) : val
  return object
}, {})

var classs = new Response()
var obj = classToObject(classs)
console.log({ obj, classs })

Original Answer

const classToObject = theClass => {
  const originalClass = theClass || {}
  const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(originalClass))
  return keys.reduce((classAsObj, key) => {
    classAsObj[key] = originalClass[key]
    return classAsObj
  }, {})
}

enter image description here

Autoionization answered 26/10, 2019 at 4:43 Comment(4)
That answer is awesome Man, I don't understand what it has been ignored. Well Done and thank for sharing!Coop
This answer looks nice and concise. But, my OP required nested classes to be also converted to plain javascript object (preserving methods). I think you need to call your fn recursively to solve that specific scenario as well. I'll mark your answer as the accepted one after you edit your solution to cover the nested scenario. Thanks for this! :)Sporran
@alex-cory if you solve the problem mentioned in my previous comment please notify me to change the accepted answer! Thanks!Sporran
@Sporran I updated to have recursionAutoionization
S
9

Ok, so the implementation in my OP was wrong, and the mistake was simply stupid.

The correct implementation when using es6 is:

toJSON(proto) {
    let jsoned = {};
    let toConvert = proto || this;
    Object.getOwnPropertyNames(toConvert).forEach((prop) => {
        const val = toConvert[prop];
        // don't include those
        if (prop === 'toJSON' || prop === 'constructor') {
            return;
        }
        if (typeof val === 'function') {
            jsoned[prop] = val.bind(jsoned);
            return;
        }
        jsoned[prop] = val;
    });

    const inherited = Object.getPrototypeOf(toConvert);
    if (inherited !== null) {
        Object.keys(this.toJSON(inherited)).forEach(key => {
            if (!!jsoned[key] || key === 'constructor' || key === 'toJSON')
                return;
            if (typeof inherited[key] === 'function') {
                jsoned[key] = inherited[key].bind(jsoned);
                return;
            }
            jsoned[key] = inherited[key];
        });
    }
    return jsoned;
}
Sporran answered 10/1, 2016 at 1:56 Comment(1)
This is the only proposed solution that works with inheritance.Martinelli
R
4

This solution will lose methods, but it is a very simple solution to convert a class instance to an object.

obj = JSON.parse(JSON.stringify(classInstance))
Romanic answered 13/1, 2021 at 16:22 Comment(2)
This is theoretically slow because it converts two times.Gardenia
This doesn't preserve methods.Sporran
V
2

Here is the implementation for the toJSON() method. We are copying over the properties & methods from the current instance to a new object and excluding the unwanted methods i.e. toJSON and constructor.

toJSON() {
    var jsonedObject = {};
    for (var x in this) {

        if (x === "toJSON" || x === "constructor") {
            continue;
        }
        jsonedObject[x] = this[x];
    }
    return jsonedObject;
}

I have tested the object returned by toJSON() in Chrome and I see the object behaving the same way as you are expecting.

Vasculum answered 9/1, 2016 at 23:13 Comment(6)
Nope, not working. I have a class hierarchy there, so it's not going to workSporran
It works. I have tested calling toJSON on Human, Person and PersonWorker. It is working as expected.Vasculum
It works on that example, indeed. But on my usecase it's still not working. Don't understand what's the problem :|Sporran
Let me try something differentSporran
It seems that the problem appears when targeting es6. I have updated my question.Sporran
This does not work with es6. However the solution privided by eAbi does.Hers
B
2

I'm riffing on Alex Cory's solution a lot, but this is what I came up with. It expects to be assigned to a class as a Function with a corresponding bind on this.

const toObject = function() {
  const original = this || {};
  const keys = Object.keys(this);
  return keys.reduce((classAsObj, key) => {
    if (typeof original[key] === 'object' && original[key].hasOwnProperty('toObject') )
      classAsObj[key] = original[key].toObject();
    else if (typeof original[key] === 'object' && original[key].hasOwnProperty('length')) {
      classAsObj[key] = [];
      for (var i = 0; i < original[key].length; i++) {
        if (typeof original[key][i] === 'object' && original[key][i].hasOwnProperty('toObject')) {
          classAsObj[key].push(original[key][i].toObject());
        } else {
          classAsObj[key].push(original[key][i]);
        }
      }
    }
    else if (typeof original[key] === 'function') { } //do nothing
    else
      classAsObj[key] = original[key];
    return classAsObj;
  }, {})
}

then if you're using TypeScript you can put this interface on any class that should be converted to an object:

export interface ToObject {
  toObject: Function;
}

and then in your classes, don't forget to bind this

class TestClass implements ToObject {
   toObject = toObject.bind(this);
}
Bul answered 17/12, 2019 at 23:14 Comment(0)
C
0

using Lodash

This method isn't recursive.

  toPlainObject() {
    return _.pickBy(this, item => {
      return (
        !item ||
        _.isString(item) ||
        _.isArray(item) ||
        _.isNumber(item) ||
        _.isPlainObject(item)
      );
    });
  }
Cabbageworm answered 19/4, 2020 at 14:51 Comment(0)
O
0

A current alternative (2022) is to use structuredClone():

https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

Ozzie answered 28/3 at 17:14 Comment(1)
This doesn't preserve methods as I've asked in my original post.Sporran

© 2022 - 2024 — McMap. All rights reserved.