将JSON字符串转换为带有方法的对象

25

我有一个应用程序,允许用户生成对象并将它们存储(以字符串形式)以备后用(在MySQL表中)。该对象可以是:

function Obj() {
    this.label = "new object";
}

Obj.prototype.setLabel = function(newLabel) {
    this.label = newLabel;
}
如果我在这个对象上使用JSON.stringify,我只会获得Obj.label的信息(字符串化的对象将是类似于{label: "new object"}的字符串)。如果我存储这个字符串,并想要让用户稍后检索该对象,则setLabel方法将丢失。
因此,我的问题是:如何重新实例化对象,以便它保留由JSON.stringify存储的属性,但也获取应属于其原型的不同方法。你会怎么做?我想到了一些像“创建一个空对象”和“合并它与存储对象的属性”之类的东西,但我无法使其正常工作。

我向 @Cystack 表示赞赏!在这里,我们经常讨论将 JavaScript 对象(仅为数据属性,没有方法)转换的概念。然而,这是我遇到的第一个真正关注具有方法的对象的问题。JSON.stringify 方法通常存在足够多的陷阱(循环引用、转义/字符集等),因此大多数问题都集中在此方面,可惜很少有关于这种“自动重新实例化”的内容。 - humanityANDpeace
8个回答

20
为了实现这个目标,您需要在解析JSON字符串时使用“reviver”函数(并在创建它时使用“replacer”函数或构造函数原型上的toJSON函数)。请参见规范的Section 15.12.215.12.3。如果您的环境尚不支持本机JSON解析,则可以使用Crockford的其中一种解析器(Crockford是JSON的发明者),这些解析器还支持“reviver”函数。
这是一个简单的定制示例,适用于符合ES5标准的浏览器(或模拟ES5行为的库)(live copy,在Chrome或Firefox或类似浏览器中运行),但请查看示例以获取更通用的解决方案。
// Our constructor function
function Foo(val) {
  this.value = val;
}
Foo.prototype.nifty = "I'm the nifty inherited property.";
Foo.prototype.toJSON = function() {
  return "/Foo(" + this.value + ")/";
};

// An object with a property, `foo`, referencing an instance
// created by that constructor function, and another `bar`
// which is just a string
var obj = {
  foo: new Foo(42),
  bar: "I'm bar"
};

// Use it
display("obj.foo.value = " + obj.foo.value);
display("obj.foo.nifty = " + obj.foo.nifty);
display("obj.bar = " + obj.bar);

// Stringify it with a replacer:
var str = JSON.stringify(obj);

// Show that
display("The string: " + str);

// Re-create it with use of a "reviver" function
var obj2 = JSON.parse(str, function(key, value) {
  if (typeof value === "string" &&
      value.substring(0, 5) === "/Foo(" &&
      value.substr(-2) == ")/"
     ) {
    return new Foo(value.substring(5, value.length - 2));
  }
  return value;
});

// Use the result
display("obj2.foo.value = " + obj2.foo.value);
display("obj2.foo.nifty = " + obj2.foo.nifty);
display("obj2.bar = " + obj2.bar);

请注意Foo.prototype上的toJSON和我们传递给JSON.parse的函数。
问题在于复活者与Foo构造函数紧密耦合。 您可以在代码中采用通用框架,其中任何构造函数都可以支持fromJSON(或类似)函数,并且您可以使用一个通用的复苏者。
下面是一个通用的复苏者示例,它查找ctor属性和data属性,如果找到,则调用ctor.fromJSON,并将其接收到的完整值传递给它。 实时示例
// A generic "smart reviver" function.
// Looks for object values with a `ctor` property and
// a `data` property. If it finds them, and finds a matching
// constructor that has a `fromJSON` property on it, it hands
// off to that `fromJSON` fuunction, passing in the value.
function Reviver(key, value) {
  var ctor;

  if (typeof value === "object" &&
      typeof value.ctor === "string" &&
      typeof value.data !== "undefined") {
    ctor = Reviver.constructors[value.ctor] || window[value.ctor];
    if (typeof ctor === "function" &&
        typeof ctor.fromJSON === "function") {
      return ctor.fromJSON(value);
    }
  }
  return value;
}
Reviver.constructors = {}; // A list of constructors the smart reviver should know about  

为了避免在 toJSONfromJSON 函数中重复编写通用逻辑,你可以创建通用版本:
// A generic "toJSON" function that creates the data expected
// by Reviver.
// `ctorName`  The name of the constructor to use to revive it
// `obj`       The object being serialized
// `keys`      (Optional) Array of the properties to serialize,
//             if not given then all of the objects "own" properties
//             that don't have function values will be serialized.
//             (Note: If you list a property in `keys`, it will be serialized
//             regardless of whether it's an "own" property.)
// Returns:    The structure (which will then be turned into a string
//             as part of the JSON.stringify algorithm)
function Generic_toJSON(ctorName, obj, keys) {
  var data, index, key;

  if (!keys) {
    keys = Object.keys(obj); // Only "own" properties are included
  }

  data = {};
  for (index = 0; index < keys.length; ++index) {
    key = keys[index];
    data[key] = obj[key];
  }
  return {ctor: ctorName, data: data};
}

// A generic "fromJSON" function for use with Reviver: Just calls the
// constructor function with no arguments, then applies all of the
// key/value pairs from the raw data to the instance. Only useful for
// constructors that can be reasonably called without arguments!
// `ctor`      The constructor to call
// `data`      The data to apply
// Returns:    The object
function Generic_fromJSON(ctor, data) {
  var obj, name;

  obj = new ctor();
  for (name in data) {
    obj[name] = data[name];
  }
  return obj;
}

这里的优势在于您将序列化和反序列化的实现交给了特定“类型”(暂时无更好的术语)来处理。因此,您可以使用只使用泛型的“类型”。
// `Foo` is a constructor function that integrates with Reviver
// but doesn't need anything but the generic handling.
function Foo() {
}
Foo.prototype.nifty = "I'm the nifty inherited property.";
Foo.prototype.spiffy = "I'm the spiffy inherited property.";
Foo.prototype.toJSON = function() {
  return Generic_toJSON("Foo", this);
};
Foo.fromJSON = function(value) {
  return Generic_fromJSON(Foo, value.data);
};
Reviver.constructors.Foo = Foo;

...或者出于某种原因需要执行更多自定义操作的应用程序:

// `Bar` is a constructor function that integrates with Reviver
// but has its own custom JSON handling for whatever reason.
function Bar(value, count) {
  this.value = value;
  this.count = count;
}
Bar.prototype.nifty = "I'm the nifty inherited property.";
Bar.prototype.spiffy = "I'm the spiffy inherited property.";
Bar.prototype.toJSON = function() {
  // Bar's custom handling *only* serializes the `value` property
  // and the `spiffy` or `nifty` props if necessary.
  var rv = {
    ctor: "Bar",
    data: {
      value: this.value,
      count: this.count
    }
  };
  if (this.hasOwnProperty("nifty")) {
    rv.data.nifty = this.nifty;
  }
  if (this.hasOwnProperty("spiffy")) {
    rv.data.spiffy = this.spiffy;
  }
  return rv;
};
Bar.fromJSON = function(value) {
  // Again custom handling, for whatever reason Bar doesn't
  // want to serialize/deserialize properties it doesn't know
  // about.
  var d = value.data;
      b = new Bar(d.value, d.count);
  if (d.spiffy) {
    b.spiffy = d.spiffy;
  }
  if (d.nifty) {
    b.nifty = d.nifty;
  }
  return b;
};
Reviver.constructors.Bar = Bar;

以下是我们如何测试FooBar是否按预期工作的方法(实时示例):

// An object with `foo` and `bar` properties:
var before = {
  foo: new Foo(),
  bar: new Bar("testing", 42)
};
before.foo.custom = "I'm a custom property";
before.foo.nifty = "Updated nifty";
before.bar.custom = "I'm a custom property"; // Won't get serialized!
before.bar.spiffy = "Updated spiffy";

// Use it
display("before.foo.nifty = " + before.foo.nifty);
display("before.foo.spiffy = " + before.foo.spiffy);
display("before.foo.custom = " + before.foo.custom + " (" + typeof before.foo.custom + ")");
display("before.bar.value = " + before.bar.value + " (" + typeof before.bar.value + ")");
display("before.bar.count = " + before.bar.count + " (" + typeof before.bar.count + ")");
display("before.bar.nifty = " + before.bar.nifty);
display("before.bar.spiffy = " + before.bar.spiffy);
display("before.bar.custom = " + before.bar.custom + " (" + typeof before.bar.custom + ")");

// Stringify it with a replacer:
var str = JSON.stringify(before);

// Show that
display("The string: " + str);

// Re-create it with use of a "reviver" function
var after = JSON.parse(str, Reviver);

// Use the result
display("after.foo.nifty = " + after.foo.nifty);
display("after.foo.spiffy = " + after.foo.spiffy);
display("after.foo.custom = " + after.foo.custom + " (" + typeof after.foo.custom + ")");
display("after.bar.value = " + after.bar.value + " (" + typeof after.bar.value + ")");
display("after.bar.count = " + after.bar.count + " (" + typeof after.bar.count + ")");
display("after.bar.nifty = " + after.bar.nifty);
display("after.bar.spiffy = " + after.bar.spiffy);
display("after.bar.custom = " + after.bar.custom + " (" + typeof after.bar.custom + ")");

display("(Note that after.bar.custom is undefined because <code>Bar</code> specifically leaves it out.)");

有趣的建议,但这仅适用于您的“Foo”类。如果您看一下我的Obj(),我无法使用它来恢复对象,因为“.label”属性将丢失。 - Cystack
根据您的示例查看此内容:http://jsfiddle.net/cBBd4/,nifty属性已丢失。 - Cystack
@Cystack:实际上,我添加了一个通用的恢复器(并只是用更好的替换了它)。关于你的小测试:当然,这取决于“Foo”对象的“toJSON”函数,它需要确保保存适当的属性(许多对象将具有不应序列化的属性)。您还可以执行通用操作,如我最新更新中所示。 - T.J. Crowder
我很感激这些想法的展示。重新实例化一个对象,以便序列化过程再次以具有方法的对象结束的概念是非常有用的。除了循环引用和JSON.stringify的经典问题外,存储真正的对象(而不仅仅是数据)是必要的。无论如何,手动引用对象名称似乎还不是最完美的解决方案。那么使用这个自动获取构造函数引用怎么样? - humanityANDpeace

5

您确实可以创建一个空实例,然后将该实例与数据合并。我建议使用一个库函数以便于使用(例如jQuery.extend)。

不过,您在代码中存在一些错误(function ... = function(...),以及 JSON 要求在键周围加上")。

http://jsfiddle.net/sc8NU/1/

var data = '{"label": "new object"}';  // JSON
var inst = new Obj;                    // empty instance
jQuery.extend(inst, JSON.parse(data)); // merge

请注意,这样合并会直接设置属性,因此如果 setLabel 进行一些检查操作,则无法通过此方式完成。

非常好!这是我想到的最接近的。现在唯一的缺点是,由于各种原因,我花了几个小时不包括jQuery,我会讨厌自己为此而包括它^^不过我会研究.extend函数的代码。 - Cystack
@Cystack:许多库中都有很多可用的 extend 函数; 你也可以借用其中一个并将其复制到你的项目中。underscore.js 是不依赖其他库的免费选择:http://documentcloud.github.com/underscore/underscore.js。 - pimvdb
2
@Cystack & pimvdb:这里的问题在于它假定 Obj 可以不带参数进行调用,正如pimvdb所指出的那样,它绕过了类型的API(这可能是完全可以的,或者不行)。在我看来,你最好将这些决策推迟到类型本身(如果适当的话,它当然可以愉快地使用 extend)。 - T.J. Crowder

1

如果你想使用 Obj 的设置器:

Obj.createFromJSON = function(json){
   if(typeof json === "string") // if json is a string
      json = JSON.parse(json); // we convert it to an object
   var obj = new Obj(), setter; // we declare the object we will return
   for(var key in json){ // for all properties
      setter = "set"+key[0].toUpperCase()+key.substr(1); // we get the name of the setter for that property (e.g. : key=property => setter=setProperty
      // following the OP's comment, we check if the setter exists :
      if(setter in obj){
         obj[setter](json[key]); // we call the setter
      }
      else{ // if not, we set it directly
         obj[key] = json[key];
      }
   }
   return obj; // we finally return the instance
};

这需要你的类为其所有属性设置setter方法。 此方法是静态的,因此您可以像这样使用:

var instance = Obj.createFromJSON({"label":"MyLabel"});
var instance2 = Obj.createFromJSON('{"label":"MyLabel"}');

不错的解决方案;也许在没有 setXXX 可用的情况下,可以直接设置它(因为相当多的属性可能不需要单独的 setter 函数)。 - pimvdb

1
据我所知,这意味着远离JSON;现在您正在自定义它,因此您将承担所有可能带来的头痛。JSON的想法是仅包含数据,而不是代码,以避免允许包含代码时出现的所有安全问题。允许代码意味着您必须使用eval来运行该代码,而eval是邪恶的。

我不想将代码放入JSON中,我只是提到它没有以任何方式存储方法。但我对任何解决方案都持开放态度! - Cystack
据我所知,这意味着远离JSON... 实际上,恢复函数已经存在很长时间了。 - T.J. Crowder
我想我没有理解问题的本质。一个reviver函数允许您使用现有函数(不是JSON中的函数)来处理读取的数据,包括构建对象和添加自己的代码。这意味着您可以存储属性名称,以标记应添加到重构对象中的函数。但它不会在JSON中存储代码。除非我漏掉了什么...如果是这样,请随时启发我。 - dnuttle

1
从ECMAScript 6开始,您只需要执行以下操作:

Object.assign(new Obj(), JSON.parse(rawJsonString))

注意:首先创建一个定义类型的新空对象,然后使用解析的JSON覆盖其属性。不要反过来。
方法定义行为并不包含变量数据。它们被视为您的代码的一部分。因此,您实际上不需要将它们存储在数据库中。

0

尝试在方法上使用toString。

更新:

迭代obj中的方法并将它们存储为字符串,然后使用new Function实例化它们。

storedFunc = Obj.prototype.setLabel.toString();
Obj2.prototype['setLabel'] = new Function("return (" + storedFunc + ")")();

你的意思是什么?将方法存储为对象中的字符串?那不是违背了原型的目的吗? - Cystack
不一定要将方法存储在对象中或数据库中,只需添加一个表格来记录方法及其所属的对象,然后将普通的 JSON 返回给对象,再将方法字符串添加到对象中。 - j-a
由于OP询问对象是否具有方法,当涉及到JSON时,我认为使用“replacer”将方法存储在字符串中(可能应该进行转义和/或base64以不破坏JSON符号),然后再恢复方法的想法是可行的。建议可能不被认为是很好的(即在DB中存储方法),但我认为它与OP相关,并且答案绝对丰富了解决方案空间,@j-a谢谢! - humanityANDpeace

0

JavaScript 是一种基于原型的编程语言,它是一种无类的语言,其中对象导向是通过复制现有对象作为原型的过程来实现的。

序列化 JSON 可以考虑任何方法,例如,如果您有一个对象

var x = {
    a: 4
    getText: function() {
       return x.a;
    }
};

你将会得到一个只有 { a:4 } 的结果,其中 getText 方法被序列化器跳过了。

我曾经在一年前遇到过同样的问题,我不得不为每个领域对象维护一个单独的帮助类,并在需要时使用 $.extend() 将其添加到我的反序列化对象中,就像为领域对象的基类添加方法一样。


0

你需要编写自己的stringify方法,将函数转换为字符串并存储为属性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接