将JSON数据转换为带有附加方法的对象的最简单方法是什么?

17

如何最快最简单地将包含对象数据的json转换为具有方法的实际对象?

例如,我获取一个水果碗的数据,其中包含一个水果对象数组,这些对象又包含一个种子数组:

{"fruitbowl": [{
     "name": "apple", 
     "color": "red", 
     "seeds": []
   },{
     "name": "orange", 
     "color": "orange", 
     "seeds": [
        {"size":"small","density":"hard"},
        {"size":"small","density":"soft"}
    ]}
}

这些都很好,但是在客户端我们会对这个水果进行一些操作,比如吃它和种树……

 var fruitbowl = []
 function Fruit(name, color, seeds){
     this.name = name
     this.color = color
     this.seeds = seeds
     this.eat = function(){
         // munch munch
     }
 }
 function Seed(size, density){
     this.size = size
     this.density = density
     this.plant = function(){
          // grow grow
     }
 }

我的ajax的成功处理程序当前正在循环遍历该物体,并依次构建每个对象,它还没有处理种子,因为在我开始循环种子构造函数之前,我正在思考

难道没有更好的方法吗?

    success: function(data){           
        fruitbowl.length = 0
        $.each(data.fruitbowl, function(i, f){
            fruitbowl.push(new Fruit(f.name, f.color, f.seeds))
        })

我还没有尝试过遍历对象并附加所有方法。那样可行吗?


额外加分项:在某些时候,我可能需要使用 "instanceof" 来识别我的水果和种子。 - John Mee
7个回答

2
使用ES5的Object.create 只需静态定义对象,然后使用Object.create进行扩展即可。
这就像Object.create(Bowl, transform(data));一样简单。
// declare 3 Objects to use as prototypes for your data
var Fruit = {
  eat: function() { }
}

var Seed = {
  plant: function() { }
}

var Bowl = {};

// data object
var data = { ... };


// Transform JSON to a valid defineProperties hash.
Object.create(Bowl, transform(data));

您需要定义转换函数,并更重要的是告诉它嵌套数据数组的对象类型。

// hash map of property names of arrays to the Object they should prototype from.
var collectionClassHash = {
  fruitbowl: Fruit,
  seeds: Seed
}

var transform = function(obj) {
  // return value
  var ret = {};
  // for each key
  Object.keys(obj).forEach(function(key) {
    // value of key
    var temp = obj[key];
    // if array 
    if (Array.isArray(temp) {
      // override value with an array of the correct objects
      temp = obj[key].map(function(val) {
        // recurse for nested objects
        return Object.create(collectionClassHash[key], transform(val));
      });
    } 
    // define getter/setter for value
    ret[key] = {
      get: function() { return temp; },
      set: function(v) { temp = v; }
    }
  });
  return ret;
}

2

是的,它可以工作,但这并不理想。除了在我看来稍微有点hacky之外,您正在将方法附加到每个水果和种子实例上,而应该使用原型链。如果您将来要使用instanceof,则此方法将无效。

您当前正在执行的操作是最佳解决方案;您将能够使用instanceof

如果您感到冒险,可以使用JSONP代替AJAX,JSONP响应看起来像:

buildFruitbowl([new Fruit("orange", "blue", [new Seed("small", "hard"), new Seed("big", "soft")]), new Fruit("banana", "yellow", [new Seed("small", "hard"), new Seed("big", "soft")])]);

使用这种方法可以避免进行所有对象循环,您可以按照自己的需求获取水果和种子(并支持instanceof); 但是我仍然建议您继续使用您已经在做的事情。

祝您种香蕉顺利。


2
“祝你种香蕉好运” - 我以前从未听过这个。 - Anurag

2
将数据传递给对象构造函数,然后使用jQuery的“extend”方法将数据和方法结合起来:
 function Fruit(data){
     $.extend(this, data)
     this.eat = function(){
         // munch munch
     }
 }
 ...
       $.each(data.fruitbowl, function(i, f){
           fruitbowl.push(new Fruit(f))
       })

您仍然需要使用循环; 并且必须手动编写嵌套对象(如种子)的循环,但这仍然是一个非常简单的方法来解决问题。


2
您可以修改JSON结构以存储类型信息。如果您有大量对象需要进行序列化和反序列化,这将节省编写每个对象的自定义代码所需的时间。
另外,请注意,这会修改JSON结构并为每个自定义对象添加一个__type__属性。我认为这比保留单独的配置文件更清晰。因此,下面是它的基本工作原理:
var fruitBowl = {..};
fruitBowl[0].eat();
fruitBowl[1].seeds[0].plant();

调用对象上的serialize方法以获取JSON表示。
var json = fruitBowl.serialize();

调用deserialize方法对JSON编码的字符串进行反序列化,以重建对象。
var resurrected = json.deserialize();

现在您可以访问对象的属性并调用方法:

resurrected[0].eat();
resurrected[1].seeds[0].plant();

它适用于任何深度嵌套对象的级别,尽管它现在可能有一些错误。此外,它很可能不是跨浏览器的(仅在Chrome上进行了测试)。由于反序列化程序不熟悉对象的构造函数,因此它基本上创建每个自定义对象而不传递任何参数。我在jsfiddle上设置了一个工作演示,网址为:http://jsfiddle.net/kSATj/1/
构造函数必须修改以考虑其对象可以创建的两种方式:
1. 直接在Javascript中 2. 从JSON重建
所有构造函数都需要适应从两端创建,因此每个属性都需要分配默认回退值,以防未传递任何内容。
function SomeObject(a, b) {
    this.a = a || false; // defaultValue can be anything
    this.b = b || null; // defaultValue can be anything
}

// one type of initialization that you can use in your code
var o = new SomeObject("hello", "world");

// another type of initialization used by the deserializer
var o = new SomeObject();;
o.a = "hello";
o.b = "world";

作为参考,修改后的JSON如下:

{"fruitbowl": 
    [
        {
            "__type__": "Fruit",
            "name": "apple",
            "color": "red",
            "seeds": []           
        },
        {
            "__type__": "Fruit",
            "name": "orange",
            "color": "orange",
            "seeds": 
            [
                {
                    "__type__": "Seed",
                    "size": "small",
                    "density": "hard"
                },
                {
                    "__type__": "Seed",
                    "size": "small",
                    "density": "soft"
                }
            ]
        }
    ]
}

这只是一个用于识别简单类型的辅助函数:
function isNative(object) {
    if(object == null) {
        return true;
    }

    var natives = [Boolean, Date, Number, String, Object, Function];
    return natives.indexOf(object.constructor) !== -1;
}

将对象序列化为JSON(保留类型信息):
Object.prototype.serialize = function() {
    var injectTypes = function(object) {
        if(!isNative(object)) {
            object.__type__ = object.constructor.name;
        }

        for(key in object) {
            var property = object[key];
            if(object.hasOwnProperty(key) && !isNative(property)) {
                injectTypes(property);
            }
        }
    };

    var removeTypes = function(object) {
        if(object.__type) {
            delete object.__type__;
        }
        for(key in object) {
            var property = object[key];
            if(object.hasOwnProperty(key) && !isNative(property)) {
                removeTypes(property);
            }
        }
    }

    injectTypes(this);
    var json = JSON.stringify(this);
    removeTypes(this);

    return json;
};

反序列化(包括重构自定义对象):

String.prototype.deserialize = function() {
    var rawObject = JSON.parse(this.toString());

    var reconstruct = function(object) {
        var reconstructed = {};

        if(object.__type__) {
            reconstructed = new window[object.__type__]();
            delete object.__type__;
        }
        else if(isNative(object)) {
            return object;
        }

        for(key in object) {
            var property = object[key];

            if(object.hasOwnProperty(key)) {
                reconstructed[key] = reconstruct(property);
            }
        }

        return reconstructed;
    }

    return reconstruct(rawObject);
};

哇,努力得到了满分!并不是因为更容易。传递 type 的想法已经被注意到了。 - John Mee
我同意,如果你只有少量的对象,这并不是很有帮助。在这种情况下,手动从JSON构建对象绝对是更好、更容易的方法。我认为我已经提到了我的解决方案的缺点——有bug不跨浏览器污染JSON,但是再多付出一点努力,随着需要重建的对象数量的增加,这种方法应该会开始产生回报。 - Anurag

1

实际上,我花了一些时间才弄明白,我真的很惊讶为什么没有更多关于这个的页面。

正如@Pointy指出的那样,JSON有一个reviver函数,可以用于替换解析结果内联,从而避免第二次遍历树。JSON页面文档记录了reviver(在我看来有点薄弱) - http://json.org/js.html

Reviver是ECMA 5的一部分,在Firefox、WebKit(Opera/Chrome)和JSON2.js中受支持。

以下是一个基于JSON文档的代码示例。您可以看到我们正在为Dog设置一个类型属性,然后使用识别该类型属性的reviver函数。

function Dog(args) {
    this.name = args.name;
    this.bark = function() {
        return "bark, bark, my name is " + this.name;
    };
    this.toJSON = function() {
        return {
            name: this.name,
            type: 'Dog'   // this.constructor.name will work in certain browsers/cases
        }
    }
};

var d = new Dog({name:'geti'});

var dAsJson = JSON.stringify(d);
var dFromJson = JSON.parse(dAsJson, function (key, value) {
    var type;
    if (value && typeof value === 'object') {
        type = value.type;
        if (typeof type === 'string' && typeof window[type] === 'function') {
            return new (window[type])(value);
        }
    }
    return value;
}
);

我对他们的示例有一些疑虑。首先,它依赖于构造函数是全局的(在窗口上)。其次,存在安全问题,因为恶意 JSON 可以通过向其 JSON 添加类型属性来让我们调用任何构造函数。

我选择了一个明确的类型列表和它们的构造函数。这确保只会调用我知道是安全的构造函数,并且还允许我使用自定义类型映射方法(而不是依赖于构造函数名称和它在全局空间中)。我还验证 JSON 对象具有类型(有些可能没有,它们将被正常处理)。

var jsonReviverTypes = {
    Dog: Dog
};

var dAsJsonB = JSON.stringify(d);
var dFromJsonB = JSON.parse(dAsJsonB, function (key, value) {
    var type;
    if (value && typeof value === 'object' && value.type) {
        type = value.type;
        if (typeof type === 'string' && jsonReviverTypes[type]) {
            return new (jsonReviverTypes[type])(value);
        }
    }
    return value;
});

注意,FF 3.6版本中的JSON.replacer方法存在一个错误,正如@Sky所指出并在此处记录 - http://skysanders.net/subtext/archive/2010/02/24/confirmed-bug-in-firefox-3.6-native-json-implementation.aspx。对于上述解决方案,我通过使用对象的toJSON而不是使用replacer来解决这个问题。

1
使用D Crockford的“json2”库,您可以向解析过程提供一个“reviver”函数。 Reviver函数会传递每个键和每个值,并应返回要在解析结果中使用的实际有效值。
在“stringify”方法中有一个相应的可选参数。

1
警告:每当您决定使用reviver时,您需要强制覆盖mozilla/firefox < 3.6本机JSON与json2.js。在reviver实现中有一个已记录的错误。 - Sky Sanders

0

约翰,

希望来得不太晚。我上周遇到了一个非常相似的问题,并通过以下js代码解决了它(也可以轻松转换为jquery)。

这是基本用法:

    $(document).ready(function() {
        var bowl = { "fruitbowl": [{
            "name": "apple",
            "color": "red",
            "seeds": []
        },
        {
            "name": "orange",
            "color": "orange",
            "seeds": [
            { "size": "small", "density": "hard" },
            { "size": "small", "density": "soft"}]
        }
        ]
        };

        var serialized = jsonToObject.serialize(bowl);
        var deserialized = jsonToObject.deserialize(serialized);
        // basic tests on serialize/deserializing...
        alert(deserialized.fruitbowl[0].name);
        alert(deserialized.fruitbowl[1].seeds[0].density);
    });

这是jsonToObject.js文件:

jsonToObject = {

    deserialize: function(_obj) {
        if (typeof (JSON) === 'object' && typeof (JSON.parse) === 'function') {
            // native JSON parsing is available.
            //return JSON.parse(_obj);
        }
        // otherwise, try non-native methods
        var jsonValue = new Function("return " + _obj)();

        if (!jsonValue instanceof Object) {
            jsonValue = eval("(" + _obj + ")");
        }
        return jsonValue;
    },

    serialize: function(_obj) {
        // Let Gecko browsers do this the easy way - not working
        if (_obj != undefined && typeof _obj.toSource !== 'undefined'
            && typeof _obj.callee === 'undefined') {
            return _obj.toSource();
        }

        // Other browsers must do it the hard way
        switch (typeof _obj) {
            // numbers, booleans, and functions are trivial:                 
            // just return the object itself since its default .toString()                 
            // gives us exactly what we want                 
            case 'number':
            case 'boolean':
            case 'function':
                return _obj;
                break;

            // for JSON format, strings need to be wrapped in quotes                 
            case 'string':
                return '"' + _obj.replace(/"/mg, "'") + '"';
                break;

            case 'object':
                var str;
                if (_obj.constructor === Array || typeof _obj.callee !== 'undefined') {
                    str = '[';
                    var i, len = _obj.length;
                    for (i = 0; i < len - 1; i++) { str += this.serialize(_obj[i]) + ','; }
                    str += this.serialize(_obj[i]) + ']';
                }
                else {
                    str = '{';
                    var key;
                    for (key in _obj) { str += key + ':' + this.serialize(_obj[key]) + ','; }
                    str = str.replace(/\,$/, '') + '}';
                }
                return str;
                break;

            default:
                return '""';
                break;
        }
    }
}

希望这有所帮助...
Jim [编辑] - 当然,你也可以像上面的优秀示例一样为这两个函数提供它们的原型签名,即...
String.prototype.deserialize = function() {...} Object.prototype.serialize = function() {...}

谢谢这个想法。反序列化通常是在字符串上完成的,因此最好将其添加到字符串原型中。 - Anurag
是的,我确实做了一个使用原型签名的本地版本,但考虑到你采用了这种方法,我保留了我的示例 - 这样可以增加一些变化 :)顺便说一下 - 你仍然需要在你的示例中更新签名为字符串 (http://jsfiddle.net/kSATj/) - 以防你忽略它 ;) - jim tollan
多样性总是好的 :) 谢谢你的字符串提示。我刚刚更新了我的示例。 - Anurag

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