【问题标题】:Re-associating an object with its class after deserialization in Node.js在 Node.js 中反序列化后将对象与其类重新关联
【发布时间】:2016-12-19 18:20:57
【问题描述】:

我正在为一些特定于应用程序的对象编写一个简单的序列化/反序列化框架。

考虑以下几点:

"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

此时,可以问“d1 有哪些 d2 缺少的东西?”

一种部分有效的方法是手动将 d1 的方法分配给 d2:

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

这有几个缺点。首先,我必须手动将 d1 的每个方法分配给 d2。其次,d2 有自己的属性,不使用原型机制共享槽:

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

所以我提炼的问题是:给定一个对象(例如d2),有没有办法将它与另一个对象的原型(例如d1)相关联,从而继承相同的行为?

【问题讨论】:

  • 元评论:虽然这似乎是一个重复的问题,但stackoverflow.com/questions/8039534/… 中给出的答案对于 node.js 环境并不理想。这里给出的答案好多更好。那么它仍然是重复的吗?
  • 是的,它仍然是重复的。这里给出的答案也可以转移到另一个问题。
  • @Louis 但是如果这个问题更好,那么这个问题应该作为这个问题的副本关闭。
  • 我看了这两个问题,并不认为这个问题比另一个问题明显好。
  • @Louis 但是这里的答案肯定更好。另一个问题的答案在 Node.js 中不起作用。在某些情况下它不起作用,例如this。此外,这两个问题略有不同 - 在这个 OP 中引用了类,而在另一个 OP 中只有类的名称作为字符串。

标签: javascript json node.js class serialization


【解决方案1】:

Object.create()Object.getOwnPropertyDescriptors() 是您所需要的。

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

this和OP的方法的区别在于,这个方法在原型上设置prototype属性,而OP的方法直接在对象上设置属性。当您使用带有 hasOwnProperty() 方法的 for-in 循环遍历对象自己的属性时,您可以看到这一点:

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

用我的方法它只输出_name,但用OP的方法它也输出getName

不幸的是,Object.getOwnPropertyDescriptors() 是 ECMAScript 2017 的一部分,目前仅在 Firefox 中受支持,因此您需要使用 Babel。


或者,您可以使用Object.setPrototypeOf()。它比Object.getOwnPropertyDescriptors() 有更好的浏览器支持,但是MDN 不鼓励它,因为它很慢。

const d3 = JSON.parse(JSON.stringify(d1))
Object.setPrototypeOf(d3, Dog.prototype)

【讨论】:

  • 明确的答案 - 谢谢。如果 Object.setPrototypeOf() 太慢,那么如果 getOwnPropertyDescriptors 不可用,则应该很简单地填充 getOwnPropertyDescriptors。谢谢你的指导......
  • 感激不尽。这个答案即使在嵌套类上也很有效。但是,只是好奇它是如何引用内部类的?
【解决方案2】:

在写这篇文章时,我想到了创建一个自定义构造函数,它使用反序列化的 JSON 来初始化对象:

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'

更新:如何动态查找类并分配原型

正如@Louis 指出的那样,@Gothdo 的回答要求您知道反序列化对象属于哪个类。如果您愿意将类名添加到序列化对象中,则可以使用它来动态确定类。因此,例如,扩展 OP 的示例:

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

使用deserialize JSON to JAVASCRIPT object 中描述的技巧(但针对 Node.js 进行了调整),您可以使用字符串通过 Node.js 的 global 对象获取类原型的句柄:

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

现在您可以使用@Gothdo 的技术来动态重建对象。把它们放在一起:

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

【讨论】:

  • 所以,阅读你的 cmets 和你的答案,我的老问题:stackoverflow.com/questions/8039534/… 不能被标记为重复,首先是因为它比这个问题早了 5 年,而且因为两者的描述不同场景。
  • global[d1['_class']].prototype 允许(通过设计)访问所有内容。仅使用它来反序列化受信任的输入,或在 _class 上添加过滤器以仅允许对预期类进行反序列化
【解决方案3】:

简单方法:就地更改类

如果您确实需要就地更改对象的类,此功能将适用于定义了Object.getPrototypeOfObject.setPrototypeOf 的任何系统:

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

使用JSON.serialize()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' }

更好的方法:复制对象

Mozilla 警告不要更改现有对象的原型,因为它是:

在每个浏览器和 JavaScript 引擎中的操作都非常缓慢 - Mozilla

如果您不是绝对需要就地更改,此函数将复制一个对象并更改副本的类:

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

【讨论】:

  • 如果输入不是来自受信任的来源,eval 是一个巨大的安全漏洞。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多