在JavaScript中序列化/反序列化对象的最佳方法是什么?

86

我在我的应用程序中有很多JavaScript对象,类似于:

function Person(age) {
    this.age = age;
    this.isOld = function (){
        return this.age > 60;
    }
}
// before serialize, ok
var p1 = new Person(77);
alert("Is old: " + p1.isOld());

// after, got error Object #<Object> has no method 'isOld'
var serialize = JSON.stringify(p1);
var _p1 = JSON.parse(serialize);
alert("Is old: " + _p1.isOld());

JS Fiddle中查看

我的问题是:是否有最佳实践/模式/提示,可以恢复被序列化后的对象为与它之前相同的类型 (在本例中是Person类的实例)?

我需要的要求:

  • 优化磁盘使用: 我在内存中有一个大的对象树。因此,我不想存储函数。
  • 解决方案可以使用jQuery和另一个库来进行序列化/反序列化。

1
你需要提供一些标准来确定“最佳”,例如最快、最灵活、最健壮等。序列化对象的唯一方法是迭代其属性并创建属性及其值的文本表示。无论你使用某些内置函数(例如JSON.stringify)还是编写自己的函数,取决于你是否有特定的要求,这些要求无法通过JSON.stringify满足。 - RobG
@RobG:我更新了我的问题。对我来说最好的是低磁盘使用率和灵活性(我可以在我的树中拥有许多类型的对象)。 - Topera
11个回答

81

JSON没有函数作为数据类型。您只能序列化字符串、数字、对象、数组和布尔值(以及null)。

您可以创建自己的toJson方法,仅传递真正需要序列化的数据:

Person.prototype.toJson = function() {
    return JSON.stringify({age: this.age});
};

反序列化同理:

Person.fromJson = function(json) {
    var data = JSON.parse(json); // Parsing the json string.
    return new Person(data.age);
};

用法如下:

var serialize = p1.toJson();
var _p1 = Person.fromJson(serialize);
alert("Is old: " + _p1.isOld());
为减少工作量,您可以考虑将需要序列化的所有数据存储在每个Person实例的特定“data”属性中。例如:
function Person(age) {
    this.data = {
        age: age
    };
    this.isOld = function (){
        return this.data.age > 60 ? true : false;
    }
}

然后进行序列化和反序列化只需调用JSON.stringify(this.data),设置实例的数据将是instance.data = JSON.parse(json)

这将使toJsonfromJson方法保持简单,但您需要调整其他函数。


顺便说一下:

您应该将isOld方法添加到函数的原型中:

Person.prototype.isOld = function() {}

否则,每个实例都有自己的该函数实例,这也会增加内存。


4
@aroth: 但是为什么你要这样做呢?(a) 这会增加所需的存储量 (b)这是冗余的。如果你存储了100个人,为什么要将同一个函数的字符串表示形式存储100次呢?只有数据应该被序列化。在这种情况下,函数不是数据。 - Felix Kling
1
@Felix - 我不知道你为什么想这样做,我的观点并不是应该这样做,只是在技术上可以将函数序列化和反序列化为JSON。如果有人感兴趣,这里有一个可行的演示:http://jsfiddle.net/8FQzm/4/。 - aroth
应该不是 toJSON 和 fromJSON(大写)吗? - Quickredfox
@快速:如果你想在JSON.stringify的上下文中使用toJSON,那么是的。如果仅仅因为JSON通常大写而让你不舒服,我同意;)我两年前写过这个答案,可能当时并不太关心这个。除此之外,方法名称无关紧要。 - Felix Kling
啊,是的,我明白你在这里做了什么。不过这个答案可能会让一些新手感到困惑。所以新手们:确保你们在之前理解了 JSON。 - Quickredfox
显示剩余9条评论

8
我写了 serialijse 是因为我曾经面临和你同样的问题。

你可以在 https://github.com/erossignon/serialijse 找到它。

它可以在 nodejs 或浏览器中使用,可以用于将复杂的对象从一个上下文(nodejs)序列化和反序列化到另一个上下文(浏览器),反之亦然。

var s = require("serialijse");


var assert = require("assert");


// testing serialization of a simple javascript object with date
function testing_javascript_serialization_object_with_date() {

    var o = {
        date: new Date(),
        name: "foo"
    };
    console.log(o.name, o.date.toISOString());

    // JSON will fail as JSON doesn't preserve dates
    try {
        var jstr = JSON.stringify(o);
        var jo = JSON.parse(jstr);
        console.log(jo.name, jo.date.toISOString());
    } catch (err) {
        console.log(" JSON has failed to preserve Date during stringify/parse ");
        console.log("  and has generated the following error message", err.message);
    }
    console.log("");



    var str = s.serialize(o);
    var so = s.deserialize(str);
    console.log(" However Serialijse knows how to preserve date during serialization/deserialization :");
    console.log(so.name, so.date.toISOString());
    console.log("");
}
testing_javascript_serialization_object_with_date();


// serializing a instance of a class
function testing_javascript_serialization_instance_of_a_class() {

    function Person() {
        this.firstName = "Joe";
        this.lastName = "Doe";
        this.age = 42;
    }

    Person.prototype.fullName = function () {
        return this.firstName + " " + this.lastName;
    };


    // testing serialization using  JSON.stringify/JSON.parse
    var o = new Person();
    console.log(o.fullName(), " age=", o.age);

    try {
        var jstr = JSON.stringify(o);
        var jo = JSON.parse(jstr);
        console.log(jo.fullName(), " age=", jo.age);

    } catch (err) {
        console.log(" JSON has failed to preserve the object class ");
        console.log("  and has generated the following error message", err.message);
    }
    console.log("");

    // now testing serialization using serialijse  serialize/deserialize
    s.declarePersistable(Person);
    var str = s.serialize(o);
    var so = s.deserialize(str);

    console.log(" However Serialijse knows how to preserve object classes serialization/deserialization :");
    console.log(so.fullName(), " age=", so.age);
}
testing_javascript_serialization_instance_of_a_class();


// serializing an object with cyclic dependencies
function testing_javascript_serialization_objects_with_cyclic_dependencies() {

    var Mary = { name: "Mary", friends: [] };
    var Bob = { name: "Bob", friends: [] };

    Mary.friends.push(Bob);
    Bob.friends.push(Mary);

    var group = [ Mary, Bob];
    console.log(group);

    // testing serialization using  JSON.stringify/JSON.parse
    try {
        var jstr = JSON.stringify(group);
        var jo = JSON.parse(jstr);
        console.log(jo);

    } catch (err) {
        console.log(" JSON has failed to manage object with cyclic deps");
        console.log("  and has generated the following error message", err.message);
    }

    // now testing serialization using serialijse  serialize/deserialize
    var str = s.serialize(group);
    var so = s.deserialize(str);
    console.log(" However Serialijse knows to manage object with cyclic deps !");
    console.log(so);
    assert(so[0].friends[0] == so[1]); // Mary's friend is Bob
}
testing_javascript_serialization_objects_with_cyclic_dependencies();

谢谢这个库,如何在浏览器中使用它而不需要 require? - TroyWorks
现在您可以使用 bower 安装 serialijsebower install serialijse - Etienne
似乎有些类无法反序列化。"class PreEnactedPolicy未在Factory类中注册 - 反序列化将不可能" - jdbertron
没有运气:'class PreEnactedPolicy在类Factory中未注册 - 无法进行反序列化' - jdbertron

5

我是https://github.com/joonhocho/seri的作者。

Seri是支持JSON和自定义(嵌套)类的库。

您只需要提供toJSONfromJSON方法即可序列化和反序列化任何类实例。

下面是一个嵌套类对象的示例:

import seri from 'seri';

class Item {
  static fromJSON = (name) => new Item(name)

  constructor(name) {
    this.name = name;
  }

  toJSON() {
    return this.name;
  }
}

class Bag {
  static fromJSON = (itemsJson) => new Bag(seri.parse(itemsJson))

  constructor(items) {
    this.items = items;
  }

  toJSON() {
    return seri.stringify(this.items);
  }
}

// register classes
seri.addClass(Item);
seri.addClass(Bag);


const bag = new Bag([
  new Item('apple'),
  new Item('orange'),
]);


const bagClone = seri.parse(seri.stringify(bag));


// validate
bagClone instanceof Bag;

bagClone.items[0] instanceof Item;
bagClone.items[0].name === 'apple';

bagClone.items[1] instanceof Item;
bagClone.items[1].name === 'orange';

希望这能解决您的问题。

4
浏览器的原生JSON API在调用JSON.stringify后可能无法返回您的idOld函数,但是如果您自己可以将JSON字符串化(也许使用Crockford's json2.js而不是浏览器的API),那么如果您有一个JSON字符串,例如:
var person_json = "{ \"age:\" : 20, \"isOld:\": false, isOld: function() { return this.age > 60; } }";

然后你可以调用。
eval("(" + person + ")") 

你将在json对象中获得你的函数。


2
我在GitHub上又添加了一个JavaScript序列化库。
与将JavaScript对象序列化和反序列化为内部格式的方法不同,这里的方法是将JavaScript对象序列化为本机JavaScript。这有一个优点,即格式完全与序列化程序无关,并且可以通过简单地调用eval()来重新创建对象。

https://github.com/iconico/JavaScript-Serializer


2

我曾经也遇到过同样的问题,写了一个小工具来混合数据和模型。请参见https://github.com/khayll/jsmix

下面是使用方法:

//model object (or whatever you'd like the implementation to be)
var Person = function() {}
Person.prototype.isOld = function() {
    return this.age > RETIREMENT_AGE;
}

//then you could say:
var result = JSMix(jsonData).withObject(Person.prototype, "persons").build();

//and use
console.log(result.persons[3].isOld());

它可以处理复杂的对象,例如递归嵌套的集合。

至于序列化JS函数,出于安全原因,我不会这样做。


1

我曾经遇到过类似的问题,由于找不到足够的解决方案,我也为JavaScript创建了一个序列化库:https://github.com/wavesoft/jbb(实际上它更多一些,因为它主要用于捆绑资源)

它接近于Binary-JSON,但添加了一些附加功能,例如编码对象的元数据和一些额外的优化,如数据去重、与其他包的交叉引用和结构级压缩。

然而,有一个问题:为了保持包的大小较小,包中没有类型信息。这些信息在单独的“配置文件”中提供,用于描述您的对象进行编码和解码。出于优化原因,这些信息以脚本形式提供。

但是,您可以使用gulp-jbb-profilehttps://github.com/wavesoft/gulp-jbb-profile)实用程序,从简单的YAML对象规范生成编码/解码脚本,使您的生活更加轻松:

# The 'Person' object has the 'age' and 'isOld'
# properties
Person:
  properties:
    - age
    - isOld

例如,您可以查看名为jbb-profile-three的个人资料。 当您准备好您的个人资料后,可以像这样使用JBB:
var JBBEncoder = require('jbb/encode');
var MyEncodeProfile = require('profile/profile-encode');

// Create a new bundle
var bundle = new JBBEncoder( 'path/to/bundle.jbb' );

// Add one or more profile(s) in order for JBB
// to understand your custom objects
bundle.addProfile(MyEncodeProfile);

// Encode your object(s) - They can be any valid
// javascript object, or objects described in
// the profiles you added previously.

var p1 = new Person(77);
bundle.encode( p1, 'person' );

var people = [
        new Person(45),
        new Person(77),
        ...
    ];
bundle.encode( people, 'people' );

// Close the bundle when you are done
bundle.close();

并且您可以像这样读回它:
var JBBDecoder = require('jbb/decode');
var MyDecodeProfile = require('profile/profile-decode');

// Instantiate a new binary decoder
var binaryLoader = new JBBDecoder( 'path/to/bundle' );

// Add your decoding profile
binaryLoader.addProfile( MyDecodeProfile );

// Add one or more bundles to load
binaryLoader.add( 'bundle.jbb' );

// Load and callback when ready
binaryLoader.load(function( error, database ) {

    // Your objects are in the database
    // and ready to use!
    var people = database['people'];

});

1
你可以创建一个空的类实例,并使用 Object.assign 给它赋值。
let p1 = new Person(77);
let serialized = JSON.stringify(p1);
let deserialized = Object.assign(new Person(), JSON.parse(serialized))

1
我制作了一个名为esserializer的npm模块来解决这个问题:在普通JSON格式中保存JavaScript类对象值时不存储任何函数。在序列化期间,唯一的开销是保存类名信息。因此,磁盘使用得到优化。
稍后在反序列化阶段,esserializer可以递归地反序列化对象实例,并保留所有类型/函数信息。它可以在浏览器和Node.js环境中使用。
在OP的情况下,代码将非常简单。
var ESSerializer = require('esserializer');

function Person(age) {
    this.age = age;
    this.isOld = function (){
        return this.age > 60;
    }
}
// before serialize, ok
var p1 = new Person(77);
alert("Is old: " + p1.isOld());

// serialize
var serializedText = ESSerializer.serialize(p1);
//...do something, or send the above serializedText to another JavaScript environment.
// deserialize
var deserializedObj = ESSerializer.deserialize(serializedText, [Person]);
alert("Is old: " + deserializedObj.isOld());

deserializedObj 是一个 Person 实例,其中包含所有值/函数/超类信息。

希望它能有所帮助。


只是想向esserializer表示我的推荐。我有一个ArrayBuffer和一个uInt8Array,具有同样的担忧-没有eval,在浏览器和服务器中都能正常工作,积极维护等。我能够立即序列化和反序列化回相同的值,并删除了我自己编写的大量自定义(有缺陷)代码。 - mikemaccana

0

json-stash 可以序列化任何你可以为其定义序列化器的东西。

import { stash, unstash, addSerializers } from 'json-stash'

function Person(age) {
  this.age = age;
  this.isOld = function () {
    return this.age > 60;
  };
}
var p1 = new Person(77);
expect(p1.isOld()).toBe(true);

// add a serializer
addSerializers({ type: Person, save: (p) => [p.age] });

// serialize to JSON
var serialize = stash(p1);
expect(serialize).toBe('{"$type":"Person","data":[77]}');

// deserialize to Person object
var _p1 = unstash(serialize);
expect(_p1).toBeInstanceOf(Person);
expect(_p1.isOld()).toBe(true);

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