JSON.stringify()在使用Prototype.js的数组时表现异常

90

我正在试图弄清楚我的JSON序列化出了什么问题,我有当前版本和旧版本的应用程序,并发现JSON.stringify()的工作方式存在一些令人惊讶的差异(使用来自json.org的JSON库)。

在旧版本的应用程序中:

 JSON.stringify({"a":[1,2]})

给我了这个;

"{\"a\":[1,2]}"

在新版本中,

 JSON.stringify({"a":[1,2]})

给我这个:

"{\"a\":\"[1, 2]\"}"

你有什么想法,是什么改变导致同一个库在新版本中给数组括号加上了引号?


4
似乎是与Prototype库发生冲突,这是我们在较新版本中引入的。有什么办法将包含数组的JSON对象字符串化在Prototype下? - morgancodes
27
因此,人们应该避免篡改全局内置对象(如原型框架所做的那样)。 - Gerardo Lima
11个回答

83

自从最近一些浏览器开始使用JSON.stringify,我建议使用它来代替Prototype的toJSON。然后检查window.JSON && window.JSON.stringify,否则只能通过document.createElement('script')加载json.org库...为了解决不兼容问题,请使用:

if(window.Prototype) {
    delete Object.prototype.toJSON;
    delete Array.prototype.toJSON;
    delete Hash.prototype.toJSON;
    delete String.prototype.toJSON;
}

11
实际上,处理这个问题所需的唯一声明是: delete Array.prototype.toJSON - Jean Vincent
1
非常感谢。我现在工作的公司目前仍然在我们的大部分代码中使用原型,而这对于使用更现代的库来说是一个救命稻草,否则一切都会崩溃。 - krob
1
我已经搜索了好几天,还发布了两个不同的SO问题来尝试解决它。在我打第三个问题时,看到了这个相关的问题。非常感谢! - Matthew Herbst
我刚刚在使用Backbone的Collections时遇到了这个问题,它的toJSON方法只返回对象的子集。 - jackocnr
哇,首先是Array.toJson,它将数组转换为字符串,然后在该字符串上调用JSON.stringify。这看起来像一个错误,不是吗?这种行为在定义toJSON的所有地方都相同吗? - Tornike Shavishvili
显示剩余2条评论

82

函数JSON.stringify()在ECMAScript 5及以上版本(第201页 - JSON对象,伪代码第205页)定义,如果对象中存在函数toJSON(),则使用该函数。

由于Prototype.js(或您使用的其他库)定义了Array.prototype.toJSON()函数,因此将数组首先使用Array.prototype.toJSON()转换为字符串,然后再由JSON.stringify()加上双引号,导致数组周围出现不正确的额外引号。

解决方案非常简单(这是Raphael Schweikert答案的简化版本):

delete Array.prototype.toJSON

这当然会对依赖于数组toJSON()函数属性的库产生副作用。但是考虑到与ECMAScript 5的不兼容性,我认为这只是一个小问题。

必须注意的是,在现代浏览器中高效实现了ECMAScript 5中定义的JSON对象,因此最好的解决方案是符合标准并修改现有的库。


5
以下是最简洁的回答,关于为什么会在数组中额外添加引号。 - tmarthal

16

一个不会影响其他原型依赖项的可能解决方案是:

var _json_stringify = JSON.stringify;
JSON.stringify = function(value) {
    var _array_tojson = Array.prototype.toJSON;
    delete Array.prototype.toJSON;
    var r=_json_stringify(value);
    Array.prototype.toJSON = _array_tojson;
    return r;
};

这解决了数组toJSON与JSON.stringify不兼容的问题,并保留了toJSON功能,因为其他原型库可能依赖于它。


我在一个网站中使用了这段代码片段,但它导致了问题。它导致数组的toJSON属性未定义。有什么建议吗? - Sourabh
1
请确保在使用上述代码重新定义JSON.stringify之前,已经定义了Array.prototype.toJSON。在我的测试中它运行良好。 - akkishore
2
我把它包装在 if(typeof Prototype !== 'undefined' && parseFloat(Prototype.Version.substr(0,3)) < 1.7 && typeof Array.prototype.toJSON !== 'undefined') 中。它有效了。 - Sourabh
1
很好。只有在Prototype 1.7之前才会出现这个问题。请点赞 :) - akkishore
1
问题出现在版本小于1.7的情况下。 - Sourabh

9

请编辑以使其更准确:

问题关键代码在来自JSON.org的JSON库中(以及ECMAScript 5的JSON对象的其他实现):

if (value && typeof value === 'object' &&
  typeof value.toJSON === 'function') {
  value = value.toJSON(key);
}

问题在于Prototype库扩展了Array以包含一个toJSON方法,该方法将在上述代码中被JSON对象调用。当JSON对象命中数组值时,它会调用Prototype中定义的toJSON方法,并返回该方法数组的字符串版本。因此,在数组括号周围有引号。
如果从Array对象中删除toJSON,则JSON库应该正常工作。或者,只需使用JSON库即可。

2
这不是库中的错误,因为这正是在ECMAScript 5中定义JSON.stringify()的确切方式。问题出在prototype.js上,解决方法是: 删除Array.prototype.toJSON这将对原型toJSON序列化产生一些副作用,但我发现这些副作用相对于原型与ECMAScript 5的不兼容性来说是微不足道的。 - Jean Vincent
Prototype库不是扩展Object.prototype,而是Array.prototype。虽然JavaScript中typeof array也返回“object”,但它们没有相同的“constructor”和原型。要解决这个问题,您需要执行以下操作:“删除Array.prototype.toJSON;” - Jean Vincent
@Jean 说实话,Prototype 扩展了所有基本的原生对象,包括 Object。但好吧,我再次理解了你的观点 :) 感谢帮助我回答更好。 - Bob
原型已经很久没有再扩展“Object.prototype”了(我不记得是哪个版本),以避免出现for .. in的问题。 它现在只扩展Object的静态属性(作为命名空间)(更安全):http://api.prototypejs.org/language/Object/ - Jean Vincent
Jean,实际上这是库中确切的一个bug。如果一个对象有toJSON,则必须调用它并使用其结果,但不应将其引用。 - grr
嗯,实际上,这似乎是规范中的“错误”或者说其中一个没有完全考虑周到的方面。 - grr

4
我认为更好的解决方案是在原型加载后立即包含这个。
JSON = JSON || {};

JSON.stringify = function(value) { return value.toJSON(); };

JSON.parse = JSON.parse || function(jsonsring) { return jsonsring.evalJSON(true); };

这使原型函数可用于标准的JSON.stringify()和JSON.parse(),但如果本地的JSON.parse()可用,则保留本地的JSON.parse(),因此这使得与旧版浏览器更兼容。

如果传入的'value'是一个对象,则JSON.stringify版本无法工作。你应该这样做:JSON.stringify = function(value) { return Object.toJSON(value); }; - akkishore

2
这是我用于同一问题的代码:
function stringify(object){
      var Prototype = window.Prototype
      if (Prototype && Prototype.Version < '1.7' &&
          Array.prototype.toJSON && Object.toJSON){
              return Object.toJSON(object)
      }
      return JSON.stringify(object)
}

您需要检查Prototype是否存在,然后检查版本。如果是旧版本,请在所有其他情况下回退到JSON.stringify()之前使用Object.toJSON(如果已定义)。


2

我对Prototype并不是非常熟悉,但我在它的文档中看到了这个:docs

Object.toJSON({"a":[1,2]})

我不确定这个是否会有当前编码存在的问题。

还有一篇更长的教程介绍如何在Prototype中使用JSON。


1
我的宽容解决方案检查Array.prototype.toJSON是否对JSON stringify有害,并在可能的情况下保留它,以便让周围的代码按预期工作。
var dummy = { data: [{hello: 'world'}] }, test = {};

if(Array.prototype.toJSON) {
    try {
        test = JSON.parse(JSON.stringify(dummy));
        if(!test || dummy.data !== test.data) {
            delete Array.prototype.toJSON;
        }
    } catch(e) {
        // there only hope
    }
}

1

正如人们所指出的那样,这是由于Prototype.js - 特别是1.7之前的版本。我遇到了类似的情况,但必须编写能够在有或没有Prototype.js的情况下运行的代码;这意味着我不能只删除Array.prototype.toJSON,因为我不确定它依赖于什么。对于这种情况,我想出了最好的解决方案:

function safeToJSON(item){ 
    if ([1,2,3] === JSON.parse(JSON.stringify([1,2,3]))){
        return JSON.stringify(item); //sane behavior
    } else { 
        return item.toJSON(); // Prototype.js nonsense
    }
}

希望它能帮助到某人。


1

这是我处理它的方式。

var methodCallString =  Object.toJSON? Object.toJSON(options.jsonMethodCall) :  JSON.stringify(options.jsonMethodCall);

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