14

I'm writing a simple serialization / deserialization framework for some application-specific objects.

Consider the following:

"use strict";
function Dog(name) { this._name = name; };
Dog.prototype.constructor = Dog;
Dog.prototype.getName = function() { return this._name; }

var d1 = new Dog('fido');
var d2 = JSON.parse(JSON.stringify(d1));  // serialize / deserialize

> d1
Dog { _name: 'fido' }
> d1.getName()
'fido'
> d2
{ _name: 'fido' }
> d2.getName()
TypeError: d2.getName is not a function

At this point, one can ask "What does d1 have that d2 lacks?"

One approach that partially works is to manually assign the methods of d1 to d2:

> d2.constructor = d1.constructor
> d2.getName = d1.getName
> d2.getName()
'fido'

This has a couple of disadvantages. First, I have to manually assign each method of d1 to d2. Second, d2 gets its own properties, and doesn't share slots using the prototype mechanism:

> d2
Dog {
   _name: 'fido',
  constructor: [Function: Dog],
  getName: [Function] }

So my refined question is: given an object (e.g. d2), is there a way to associate it with the prototype of another object (e.g. d1) so it inherits the same behavior?

Michał Perłakowski
  • 88,409
  • 26
  • 156
  • 177
fearless_fool
  • 33,645
  • 23
  • 135
  • 217
  • 2
    Meta comment: though this appears to be a duplicate question, the answer given in http://stackoverflow.com/questions/8039534/deserialize-json-to-javascript-object isn't ideal for the node.js environment. The answer given here is *much* better. So is it still a duplicate? – fearless_fool Aug 12 '16 at 18:59
  • Yes, it is still a duplicate. The answers given here could just as well be moved to the other question. – Louis Aug 12 '16 at 19:18
  • @Louis But if this question is better, then that question should be closed as a duplicate of this. – Michał Perłakowski Aug 12 '16 at 19:20
  • I looked at both question, and did not think that this question is *significantly* better than the other. – Louis Aug 12 '16 at 19:23
  • @Louis But the answer here is definitely better. The answer in the other question doesn't work in Node.js. And it doesn't work in some cases, like [this](https://jsbin.com/yebihutozu/1/edit?js,console). Also, these two questions are slightly different - in this OP has a reference to the class, and in the other OP has only the name of the class as a string. – Michał Perłakowski Aug 12 '16 at 19:36
  • @Gothdo The difference you point out between the two questions is immaterial. Whatever is going to work to solve the other question is necessarily going to work here too. Your answer is better yes, but that's not the only factor to be considered. The relative age of the questions is also a factor. It is true that we don't just close the newer questions just *because they are newer* but age is not to be ignored either. I've weighed all the factors. I'm not going to *ignore the history of a 4 year-old question* when a duplicate that is not even one day old gets posted. – Louis Aug 12 '16 at 19:55
  • I won't belabor the fact that this answer is MUCH better for a node.js environment. But I've made a notation that this is specifically for node.js and voted to re-open the question. – fearless_fool Aug 12 '16 at 20:10
  • @fearless_fool The other question is opened to answers in any and all JS environments. Your question here is a subset of the other by targeting Node.js. We really do not need to have a plethora of questions about how to do this in Node, Chorme, Firefox, Rhino, JS-environment-du-jour. – Louis Aug 12 '16 at 20:19
  • @louis: I know you looked at both answers, but you do understand that the other solution simply doesn't work in node.js, right? (e.g. `var obj = new window[name]();`) – fearless_fool Aug 13 '16 at 12:18
  • @fearless_fool What the answer there is doing is that instead of hardcoding class like in Gothdo's answer, there's a lookup done in the global scope. In a browser the global scope is `window`, in Node that's `global`, and in a Worker that's `self`. In an actual implementation, I'd replace all that with a lookup in an ad-hoc map, which would work anywhere. The `window` thing is really not essential to that answer. – Louis Aug 13 '16 at 12:26
  • Louis: I owe you an apology. http://stackoverflow.com/questions/8039534/deserialize-json-to-javascript-object clearly does something important that @Gothdo's given here doesn't, namely allow you to dynamically choose the containing class. See my update in http://stackoverflow.com/a/38922992/558639 for what I think is the best of both worlds. – fearless_fool Aug 15 '16 at 16:48

4 Answers4

11

Object.create() and Object.getOwnPropertyDescriptors() is what you need.

const obj = JSON.parse(JSON.stringify(d1))
const d3 = Object.create(Dog.prototype, Object.getOwnPropertyDescriptors(obj))

The difference between this and OP's method is that this method sets prototype properties on the prototype, whereas OP's method sets properties directly on the object. You can see this when you loop through object own properties using for-in loop with hasOwnProperty() method:

for (const i in d1) {
  if (d3.hasOwnProperty(i)) {
    console.log(i)
  }
}

With my method it outputs only _name, but with OP's method it outputs also getName.

Unfortunately, Object.getOwnPropertyDescriptors() is part of ECMAScript 2017 and it's supported only in Firefox for now, so you'll need to use Babel.


Alternatively, you can use Object.setPrototypeOf(). It has better browser support than Object.getOwnPropertyDescriptors(), but it's discouraged by MDN, because it's slow.

const d3 = JSON.parse(JSON.stringify(d1))
Object.setPrototypeOf(d3, Dog.prototype)
Graham
  • 7,431
  • 18
  • 59
  • 84
Michał Perłakowski
  • 88,409
  • 26
  • 156
  • 177
  • Clear answer - thanks. If Object.setPrototypeOf() is too slow, then it should be simple to polyfill getOwnPropertyDescriptors if unavailable. Thank you for the direction... – fearless_fool Aug 12 '16 at 18:45
  • can't thank you enough. This answer works well even on nested classes. However, just curious how does it reference the internal classes? – Kevindra Dec 17 '18 at 21:28
3

As I was writing this, I had the idea of creating a custom constructor that uses the deserialized JSON to initialize the object:

Dog.createFromJSON = function(obj) {
  var d = new Dog();
  Object.keys(obj).forEach(function(key) {
    d[key] = obj[key];
  });
  return d;
}

> d3 = Dog.createFromJSON(JSON.parse(JSON.serialize(d1)))
> d3
Dog { _name: 'fido' }
> d3.getName()
'fido'

Update: how to dynamically find the class and assign the prototype

As @Louis points out, @Gothdo's answer requires that you know what class the deserialized object belongs to. If you're willing to add the class name to the serialized object, you can use that to determine the class dynamically. So, for example, to expand on the OP's example:

> var d1 = new Dog('fido');
> d1['_class'] = 'Dog';
> let jsonString = JSON.stringify(d1)
'{"_name":"fido","_class":"Dog"}'

Using the trick described in deserialize JSON to JAVASCRIPT object (but tweaked for Node.js) you can use a string to get a handle to a class prototype via Node.js's global object:

> global[d1['_class']].prototype
Dog { getName: [Function] }

Now you can use that to dynamically reconstruct the object using @Gothdo's technique. Putting it all together:

/**
 * Dynamically create an object from a JSON string of properties.
 * Assumes the presence of a _class meta-property that names the
 * resulting class.
 */
function reconstitute(jsonString) {
    let obj = JSON.parse(jsonString);
    let cls = global[obj['_class']];
    delete obj['_class'];  // remove meta-property
    return Object.setPrototypeOf(obj, cls.prototype);
}

> reconstitute('{"_name":"fido","_class":"Dog"}')
Dog { _name: 'fido' }
Community
  • 1
  • 1
fearless_fool
  • 33,645
  • 23
  • 135
  • 217
  • So, reading your comments and your answers, my old question: http://stackoverflow.com/questions/8039534/deserialize-json-to-javascript-object can't be marked as duplicated, first because it made 5 years before than this question, and because both are described in different scenarios. – Kalamarico Nov 11 '16 at 23:13
  • `global[d1['_class']].prototype` gives access (by design) to everything. Only use this to deserialize trusted input, or add a filter on `_class` to only permit deserialization of expected classes – mrm Oct 09 '20 at 18:15
1

The Best method so far would be:

let obj = Object.assign(new ClassyObject(), JSON.parse(JSON.serialize(the_obj_that_will_lost_prototype)))

Just improved and more direct:

let obj = Object.assign(new the_obj_that_will_lost_prototype.constructor(), JSON.parse(JSON.serialize(the_obj_that_will_lost_prototype)))
Punisher
  • 30
  • 6
0

Simple Method: Change Class In-Place

If you really need to change the class of an object in-place, this function will work on any system that has Object.getPrototypeOf and Object.setPrototypeOf defined:

    // set class of object to `name`
    function setObjectClassName(obj, name) {
      let newObj = eval('new ' + name + '()');
      let proto = Object.getPrototypeOf(newObj);
      Object.setPrototypeOf(obj, proto);
      return obj;
    }

Example using JSON.serialize() and JSON.parse():

    class MyClass extends Object {}
    let original = new MyClass();
    original.foo = "bar";

    console.log(original.constructor.name, original);
    //  MyClass { "foo": 'bar' }

    let originalClassName = original.constructor.name;

    let serialized = JSON.stringify(original);
    console.log(serialized.constructor.name, serialized);
    // String '{"foo":"bar"}'

    let restored = JSON.parse(serialized);
    console.log(restored.constructor.name, restored);
    // Object { foo: 'bar' }

    restored = setObjectClassName(restored, originalClassName);
    console.log(restored.constructor.name, restored);
    // MyClass { foo: 'bar' }

Better Method: Copy the Object

Mozilla warns against changing the prototype of an existing object, as it is:

a very slow operation in every browser and JavaScript engine - Mozilla

If you don't absolutely need to change in-place, this function will copy an object and change the class of the copy:

  function copyObjectAndChangeClass(obj, name) {
    let newObj = eval('new ' + name + '()');
    Object.assign(newObj, obj);
    return newObj;
  }

terrymorse
  • 6,771
  • 1
  • 21
  • 27
  • 1
    That `eval` is a huge security hole if the input is not from a trusted source. – mrm Oct 09 '20 at 18:12