最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Re-associating an object with its class after deserialization in Node.js - Stack Overflow

programmeradmin0浏览0评论

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?

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?

Share Improve this question edited Aug 12, 2016 at 20:29 Michał Perłakowski 92.5k30 gold badges163 silver badges186 bronze badges asked Aug 12, 2016 at 16:53 fearless_foolfearless_fool 35.2k25 gold badges145 silver badges230 bronze badges 11
  • 2 Meta comment: though this appears to be a duplicate question, the answer given in stackoverflow.com/questions/8039534/… isn't ideal for the node.js environment. The answer given here is much better. So is it still a duplicate? – fearless_fool Commented Aug 12, 2016 at 18:59
  • Yes, it is still a duplicate. The answers given here could just as well be moved to the other question. – Louis Commented Aug 12, 2016 at 19:18
  • @Louis But if this question is better, then that question should be closed as a duplicate of this. – Michał Perłakowski Commented Aug 12, 2016 at 19:20
  • I looked at both question, and did not think that this question is significantly better than the other. – Louis Commented Aug 12, 2016 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. 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 Commented Aug 12, 2016 at 19:36
 |  Show 6 more comments

4 Answers 4

Reset to default 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)

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' }

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)))

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;
  }

发布评论

评论列表(0)

  1. 暂无评论