Distinguishing between defined and generic JSON objects in JavaScript - javascript

A project I've been working on recently gave me the need to create a function which can return a complete copy of a JSON object, recursively copying any internal objects. After a couple of failed attempts, I came up with this:
function copyObj(obj) {
var copy;
if (obj instanceof Array) {
copy = [];
for (var i in obj) {
copy.push(copyObj(obj[i]));
}
}
else if (obj instanceof Object) {
copy = {};
for (var prop in obj) {
copy[prop] = copyObj(obj[prop]);
}
}
else {
copy = obj;
}
return copy;
}
The function works perfectly for my purposes, which are to copy objects that will only ever contain primitive types, arrays, and nested generic JSON objects. For example, it will return a flawless copy of this: { prop1:0, prop2:'test', prop3:[1, 2, 3], prop4:{ subprop1:['a', 'b', 'c'], subprop2:false } }.
There's one thing about this function that's nagging at me, though - its inability to handle any other types of objects (e.g. the RegExp object). I'd like to improve on it by adding the capability to handle them, but at the same time I'd really rather not just have a huge wall of else if (obj instanceof [insert object type here]). As such, my question is this: Is there a simple way in JavaScript of differentiating between a generic object (i.e. one declared as var obj = { }) and one with a proper prototype/constructor? And if so, is there also a simple generalized way of copying such objects? My expectation for the second part of the question is no, and that I'd still need special handling to call constructors, but I'd still like to know with certainty either way.
P.S. In case anyone was curious about the context, the project requires me to manipulate a large list of items on a server, but in different ways for different connected clients. The easiest way I could think of to handle that was to create one master list and then have the server clone a fresh copy to manipulate without altering the master list for every new client that connects, hence the need for copyObj().
Edit: I probably should have mentioned this in the original question - this is running with node.js as a server, not in a browser, so browser cross-compatibility isn't an issue.
Edit 2: In the interest of not cluttering the comments too much, I'll mention it here: I tried a quick benchmarking of my copyObj() function against the JSON.parse(JSON.stringify(obj)) exploit using the example object above. My version seems to run in about 75% of the time that the JSON method takes (1 million copies took ~3.2 seconds for mine and ~4.4 seconds for JSON). So that makes me feel better about having taken the time to write my own.
Edit 3: Working off of the list of object types in Vitum.us's answer, I threw together an updated version of my copyObj() function. I haven't tested it extensively, and the performance is about 2x worse than the old version, but I think it should actually work for all built-in types (assuming that list was complete).
function copyObjNew(obj) {
var copy;
if (obj.constructor === Object) {
// Generic objects
copy = {};
for (var prop in obj) {
copy[prop] = copyObjNew(obj[prop]);
}
}
else if (obj.constructor === Array) {
// Arrays
copy = [];
for (var i in obj) {
copy.push(copyObjNew(obj[i]));
}
}
else if (obj.constructor === Number || obj.constructor === String || obj.constructor === Boolean) {
// Primitives
copy = obj;
}
else {
// Any other type of object
copy = new obj.constructor(obj);
}
return copy;
}
I'm using the .constructor property now, as Mike suggested, and it seems to be doing the trick. I've tested it so far with RegExp and Date objects, and they both seem to copy correctly. Do any of you see anything blatantly (or subtly) incorrect about this?

One way of detecting plain JS objects (created with {} or new Object) is to use the jQuery method jQuery.isPlainObject.
However, the documentation says that "Host objects have a number of inconsistencies which are difficult to robustly feature detect cross-platform. As a result of this, $.isPlainObject() may evaluate inconsistently across browsers in certain instances." Whether this works reliably with node.js should be tested.
Edit: in response to your comment: you can use jQuery with node.js, see this question: Can I use jQuery with Node.js?
Otherwise it is also possible to just copy the jQuery implementation of the method to your project, as the MIT license (https://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt) seems to permit this. jQuery implementation:
isPlainObject: function( obj ) {
// Not plain objects:
// - Any object or value whose internal [[Class]] property is not "[object Object]"
// - DOM nodes
// - window
if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
return false;
}
// Support: Firefox <20
// The try/catch suppresses exceptions thrown when attempting to access
// the "constructor" property of certain host objects, ie. |window.location|
// https://bugzilla.mozilla.org/show_bug.cgi?id=814622
try {
if ( obj.constructor &&
!core_hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) {
return false;
}
} catch ( e ) {
return false;
}
// If the function hasn't returned already, we're confident that
// |obj| is a plain object, created by {} or constructed with new Object
return true;
}

You can use this to detect if a object is a regular expression
Object.prototype.toString.call( regexpObject ) == "[object RegExp]"
This is the way mentioned in the specification for getting the class of object.
From ECMAScript 5, Section 8.6.2 Object Internal Properties and Methods:
The value of the [[Class]] internal property is defined by this specification for every kind of built-in object. The value of the [[Class]] internal property of a host object may be any String value except one of "Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", and "String". The value of a [[Class]] internal property is used internally to distinguish different kinds of objects. Note that this specification does not provide any means for a program to access that value except through Object.prototype.toString (see 15.2.4.2).
A RegExp is a class of object defined in the spec at Section 15.10 RegExp(RegularExpression)Objects:
A RegExp object contains a regular expression and the associated flags.
Then, you can copy a RegExp object using new RegExp()
var oldObject = /[a-z]+/;
var newObject = new RegExp(oldObject);

Related

Why is there not a built-in method in JavaScript to check if an object is a plain object?

Given the developments of JavaScript since the languages' inception, why is there not a built in method that checks if an object is a plain object?
Or does the method in fact exist?
You can check the type and the instance of an object this way:
var a = new Date();
console.log(typeof a);
console.log(a instanceof Date);
var b = "Hello";
console.log(typeof b);
console.log(b instanceof Date);
Updated according to the comments from the OP:
let arr = [1, 2, true, 4, {
"abc": 123
},
6, 7, {
"def": 456
},
9, [10], {}, "[object Object]"
];
arr.forEach(function(v) {
if (typeof v == "object" && !(v instanceof Array) && v != null)
console.log("Object Found");
else
; // console.log("Na");
});
The above code snippets outputs thrice Object Found.
There doesn't exist any explicit direct way to check if a value is an object, i.e. belongs to Object type, but there are some foolproof ways to do it. I wrote a list in another answer, the most succinct seems
function isObject(value) {
return Object(value) === value;
}
A feature like this has been requested multiple times on esdiscuss. For example,
What is an Object Type(O)?
Juriy Zaytsev "kangax" wonders about a proper way to check if a value is an object.
typeof null
Brendan Eich: "I think we should consider Object.isObject"
Jorge: "Why not .isPrimitive()?"
ES6 doesn't need opt-in
Brendan Eich: "We want sane isObject and isNull predicates"
Axel Rauschmayer: "predicates such as isObject() and isPrimitive()"
In fact, Object.isObject was proposed as strawman, and it appeared in an ES6 early draft.
TC39 bashing: Discussion about Object.isObject in the ES6 draft.
How primitive are Symbols? Bignums? etc: discusses x === Object(x)
Object.isObject strawman was eventually rejected and removed from ES6 draft.
More recently,
ES8 Proposal: Optional Static Typing (Brandon Andrews): Includes Object.isObject
Now there is the is{Type} Methods stage 0 proposal which includes Object.isObject among lots of various other checks.
So there is still hope and eventually we may have something like this.
The above is for testing objects in general. If you don't want that you should define what "plain object" means for you.
For example, you can test the constructor property. But any object can customize it.
You can use Object.prototype.toString to get the legacy ES5 [[Class]]. But any object can customize that via Symbol.toStringTag.
You can check the value returned by [[GetPrototypeOf]]. But even exotic objects might allow their prototype to be changed to whatever arbitrary object or null. And Proxy objects even have full control over that internal method.
So most probably you won't be able to rely on these tests. And adding something to the standard may be hard because different people may want different things.
What I would like is some way to check if an object is an ordinary one. That is, it has the default behaviour for the essential internal methods that must be supported by all objects.
Once you know that an object is ordinary, you can rely on things like [[GetPrototypeOf]] to customize the test to your tastes.
Relying on [object Object] string representation is inaccurate. This behaviour may be changed for any objects with:
let o = { toString: () => '...' };
('' + o) !== '[object Object]'
var a = [];
a.toString = () => '[object Object]';
('' + a) === '[object Object]';
The most solid way to check if a value is a plain object is
let o = {}
Object.getPrototypeOf(o) === Object.prototype
And considering that constructor property wasn't tampered, the most straightforward way to check if a value is a plain object is
let o = {}
o.constructor === Object
This covers all POJOs constructed from Object and doesn't cover Object.create(null, { ... }) or any child classes (including built-ins like RegExp or Array):
Object.create(null).constructor !== Object
[].constructor !== Object
(new class {}).constructor !== Object
One of the possible reasons why there is no dedicated method to check for object plainness is because a restriction to use only {} objects is not practical. This makes very little sense in the context of JS. This prevents the use of any class instances or relatively 'plain' objects (Object.create({}, ...)).
This would require the hack in order for desired non-plain objects to pass the check:
Object.assign({}, (new class {})).constructor === Object
In most cases of object checking 'everything which is not forbidden is allowed' principle pays off (with extra caution regarding infamous null inconsistency).
Applying the above to this case, a safe and concise condition to filter non-array objects is
o && typeof o === 'object' && !Array.isArray(o)
And a condition to filter objects that are not built-ins (functions, Array, RegExp, etc) is
o && (o.constructor === Object || !/\[native code\]/.test(o.constructor))
Just for the sake of further documenting different ways:
One way I can think of:
JSON.stringify(testValue)[0] === '{';
Keep in mind that objects with circular references cannot be stringified. However, if you are certain that all testValues cannot have circular references, you have yourself a way to check against Null, Arrays, and any primitive value to ensure that you have an Object.
I suggest that if you plan on using this throughout your code though, that you define a helper function that actually implements it, in case you find that this does not work as you expect and end up having to change the way you check it.
Every thing JavaScript is an Object , so there is no need to have an isObject api

What are the edge cases when using Object.prototype.toString?

So far I have relied on Object.prototype.toString.call(x) to distinguish between the different native object types in Javascript, arrays in particular.
If you subclass arrays, you get some strange behavior:
function Ctor() {}
Ctor.prototype = Object.create(Array.prototype);
var x = new Ctor();
x.push(1);
Object.prototype.toString.call(x); // [object Object]
Probably this is documented in the ES5 specs (and no longer an issue in ES6), but I consider it a quirk of the current version of the language. I adapted my corresponding functions as follows:
function objTypeOf(deep, type) {
return function _objTypeOf(x) {
do {
if (Object.prototype.toString.call(x).slice(8, -1).toLowerCase() === type) return true;
x = Object.getPrototypeOf(x);
} while(deep && x !== null);
return false;
};
}
var arr = objTypeOf(false, "array"),
arrP = objTypeOf(true, "array"); // array prototype
console.log(arr(x)); // false
console.log(arrP(x)); // true
objTypeOf checks the current object and the entire prototype chain until there is a type match. It accepts an object even if merely one of the prototypes matches the expected type. objTypeOf is not based on prototype identities, but on strings (lacking identity).
I wonder now if there are other edge cases when using Object.prototype.toString, that need special treatment?
Well your problem is not with Object.prototype.toString, but that you tried to subclass arrays. It just doesn't work, and toString correctly tells you that you failed to create an array. It's merely an object that has Array.prototype in its prototype chain (if that was what you cared for, use instanceof Array).
Regardless, to answer your title question:
What are the edge cases when using Object.prototype.toString?
Host objects. Everything that is not a native JS object, despite looking like one, might return any [[Class]] value that you didn't expect. There are even known cases where callable objects do not report Function.

Cloning: what's the fastest alternative to JSON.parse(JSON.stringify(x))?

What's the fastest alternative to
JSON.parse(JSON.stringify(x))
There must be a nicer/built-in way to perform a deep clone on objects/arrays, but I haven't found it yet.
Any ideas?
No, there is no build in way to deep clone objects.
And deep cloning is a difficult and edgey thing to deal with.
Lets assume that a method deepClone(a) should return a "deep clone" of b.
Now a "deep clone" is an object with the same [[Prototype]] and having all the own properties cloned over.
For each clone property that is cloned over, if that has own properties that can be cloned over then do so, recursively.
Of course were keeping the meta data attached to properties like [[Writable]] and [[Enumerable]] in-tact. And we will just return the thing if it's not an object.
var deepClone = function (obj) {
try {
var names = Object.getOwnPropertyNames(obj);
} catch (e) {
if (e.message.indexOf("not an object") > -1) {
// is not object
return obj;
}
}
var proto = Object.getPrototypeOf(obj);
var clone = Object.create(proto);
names.forEach(function (name) {
var pd = Object.getOwnPropertyDescriptor(obj, name);
if (pd.value) {
pd.value = deepClone(pd.value);
}
Object.defineProperty(clone, name, pd);
});
return clone;
};
This will fail for a lot of edge cases.
Live Example
As you can see you can't deep clone objects generally without breaking their special properties (like .length in array). To fix that you have to treat Array seperately, and then treat every special object seperately.
What do you expect to happen when you do deepClone(document.getElementById("foobar")) ?
As an aside, shallow clones are easy.
Object.getOwnPropertyDescriptors = function (obj) {
var ret = {};
Object.getOwnPropertyNames(obj).forEach(function (name) {
ret[name] = Object.getOwnPropertyDescriptor(obj, name);
});
return ret;
};
var shallowClone = function (obj) {
return Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
};
I was actually comparing it against angular.copy
You can run the JSperf test here:
https://jsperf.com/angular-copy-vs-json-parse-string
I'm comparing:
myCopy = angular.copy(MyObject);
vs
myCopy = JSON.parse(JSON.stringify(MyObject));
This is the fatest of all test I could run on all my computers
The 2022 solution for this is to use structuredClone
See : https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
structuredClone(x)
Cyclic references are not really an issue. I mean they are but that's just a matter of proper record keeping. Anyway quick answer for this one. Check this:
https://github.com/greatfoundry/json-fu
In my mad scientist lab of crazy javascript hackery I've been putting the basic implementation to use in serializing the entirety of the javascript context including the entire DOM from Chromium, sending it over a websocket to Node and reserializing it successfully. The only cyclic issue that is problematic is the retardo navigator.mimeTypes and navigator.plugins circle jerking one another to infinity, but easily solved.
(function(mimeTypes, plugins){
delete navigator.mimeTypes;
delete navigator.plugins;
var theENTIREwindowANDdom = jsonfu.serialize(window);
WebsocketForStealingEverything.send(theENTIREwindowANDdom);
navigator.mimeTypes = mimeTypes;
navigator.plugins = plugins;
})(navigator.mimeTypes, navigator.plugins);
JSONFu uses the tactic of creating Sigils which represent more complex data types. Like a MoreSigil which say that the item is abbreviated and there's X levels deeper which can be requested. It's important to understand that if you're serializing EVERYTHING then it's obviously more complicated to revive it back to its original state. I've been experimenting with various things to see what's possible, what's reasonable, and ultimately what's ideal. For me the goal is a bit more auspicious than most needs in that I'm trying to get as close to merging two disparate and simultaneous javascript contexts into a reasonable approximation of a single context. Or to determine what the best compromise is in terms of exposing the desired capabilities while not causing performance issues. When you start looking to have revivers for functions then you cross the land from data serialization into remote procedure calling.
A neat hacky function I cooked up along the way classifies all the properties on an object you pass to it into specific categories. The purpose for creating it was to be able to pass a window object in Chrome and have it spit out the properties organized by what's required to serialize and then revive them in a remote context. Also to accomplish this without any sort of preset cheatsheet lists, like a completely dumb checker that makes the determinations by prodding the passed value with a stick. This was only designed and ever checked in Chrome and is very much not production code, but it's a cool specimen.
// categorizeEverything takes any object and will sort its properties into high level categories
// based on it's profile in terms of what it can in JavaScript land. It accomplishes this task with a bafflingly
// small amount of actual code by being extraordinarily uncareful, forcing errors, and generally just
// throwing caution to the wind. But it does a really good job (in the one browser I made it for, Chrome,
// and mostly works in webkit, and could work in Firefox with a modicum of effort)
//
// This will work on any object but its primarily useful for sorting the shitstorm that
// is the webkit global context into something sane.
function categorizeEverything(container){
var types = {
// DOMPrototypes are functions that get angry when you dare call them because IDL is dumb.
// There's a few DOM protos that actually have useful constructors and there currently is no check.
// They all end up under Class which isn't a bad place for them depending on your goals.
// [Audio, Image, Option] are the only actual HTML DOM prototypes that sneak by.
DOMPrototypes: {},
// Plain object isn't callable, Object is its [[proto]]
PlainObjects: {},
// Classes have a constructor
Classes: {},
// Methods don't have a "prototype" property and their [[proto]] is named "Empty"
Methods: {},
// Natives also have "Empty" as their [[proto]]. This list has the big boys:
// the various Error constructors, Object, Array, Function, Date, Number, String, etc.
Natives: {},
// Primitives are instances of String, Number, and Boolean plus bonus friends null, undefined, NaN, Infinity
Primitives: {}
};
var str = ({}).toString;
function __class__(obj){ return str.call(obj).slice(8,-1); }
Object.getOwnPropertyNames(container).forEach(function(prop){
var XX = container[prop],
xClass = __class__(XX);
// dumping the various references to window up front and also undefineds for laziness
if(xClass == "Undefined" || xClass == "global") return;
// Easy way to rustle out primitives right off the bat,
// forcing errors for fun and profit.
try {
Object.keys(XX);
} catch(e) {
if(e.type == "obj_ctor_property_non_object")
return types.Primitives[prop] = XX;
}
// I'm making a LOT flagrant assumptions here but process of elimination is key.
var isCtor = "prototype" in XX;
var proto = Object.getPrototypeOf(XX);
// All Natives also fit the Class category, but they have a special place in our heart.
if(isCtor && proto.name == "Empty" ||
XX.name == "ArrayBuffer" ||
XX.name == "DataView" ||
"BYTES_PER_ELEMENT" in XX) {
return types.Natives[prop] = XX;
}
if(xClass == "Function"){
try {
// Calling every single function in the global context without a care in the world?
// There's no way this can end badly.
// TODO: do this nonsense in an iframe or something
XX();
} catch(e){
// Magical functions which you can never call. That's useful.
if(e.message == "Illegal constructor"){
return types.DOMPrototypes[prop] = XX;
}
}
// By process of elimination only regular functions can still be hanging out
if(!isCtor) {
return types.Methods[prop] = XX;
}
}
// Only left with full fledged objects now. Invokability (constructor) splits this group in half
return (isCtor ? types.Classes : types.PlainObjects)[prop] = XX;
// JSON, Math, document, and other stuff gets classified as plain objects
// but they all seem correct going by what their actual profiles and functionality
});
return types;
};

Differentiating between arrays and "hashes" in Javascript

In order to make the syntax for one of my functions nicer, I need to be able to tell whether a specific parameter is an array or "hash" (which I know are just objects).
Typeof doesn't work, because they both return the same thing
typeof {foo:"bar"} // Object
typeof ["foo","bar"] // Object
So how would I differentiate between the two?
I know this works, but I'm hoping there's a nicer way
({foo:"bar"}).constructor // Object()
(["foo","bar"]).constructor // [ undefined ]
EDIT
Ah, it seems [ undefined ] in firebug is the same thing as Array. Kind of weird.
You could check the length property as SLaks suggested, but as soon as you pass it a function object you'll be surprised, because it in fact has a length property. Also if the object has a length property defined, you'll get wrong result again.
Your best bet is probably:
function isArray(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
jQuery uses it, and a "couple of" other people... :)
It is more fail proof than the instanceof way. The method is also suggested by the following article:
'instanceof' considered harmful (or how to write a robust 'isArray') (#kagax)
Another thing to add that this function is almost identical to the Array.isArray function in ES 5 spec:
15.4.3.2 Array.isArray ( arg )
If Type(arg) is not Object, return
false.
If the value of the [[Class]]
internal property of arg is "Array",
then return true.
Return false.
something instanceof Array works fine within a single document, but will fail if you start passing arrays between different windows, because the Array from one window is a different object from Array on another. If you have no intention of doing cross-window-scripting (and in general, it's worth avoiding) I would recommend sticking with this.
If you need cross-window support, things are a bit more complex. In the future the story is simple, as ECMAScript Fifth Edition defines a function to do exactly this:
Array.isArray([1]); // -> true
You should use this functionality where available as it's the only reliable and standards-endorsed way. However, many of today's browsers don't support it yet.
Where it isn't available you have to rely on Object#toString serialisation, which is ugly and slightly dodgy. Although it will in general work reliably with today's browsers, there are imaginable cases where it might not (primarily to do with host objects).
You can hack this fallback method into Array on browsers that don't support it, and then use Array.isArray at all times:
if (!('isArray' in Array)) {
Array.isArray= function(o) {
return Object.prototype.toString.call(o)==='[object Array]';
};
}
As for constructor, never use it. It's not available everywhere and it doesn't do what you think. Using it is almost always a mistake.
I think the most elegant way is to simply use the instanceof operator:
if (myVar instanceof Array)
doSomething();
examples:
[] instanceof Array // true
{} instanceof Array // false
{length:100} instanceof Array // false
null instanceof Array // false
Edit:
Be aware that it will fail when testing an object from another iFrame (see answers by #galambalazs and #bobince)
Check for the length property:
"length" in {foo:"bar"} //false
"length" in ["foo","bar"] //true
This typeof function is used to correct the array/object clash for the typeof operator, and the null/object clash. It does not however work across frames or across windows. See the source for a more advanced version that works across these.
function typeOf(value) {
var s = typeof value;
if (s === 'object') {
if (value) {
if (value instanceof Array) {
s = 'array';
}
} else {
s = 'null';
}
}
return s;
}
Source: Douglas Crockford's Javascript site

JSON.stringify() array bizarreness with Prototype.js

I'm trying to figure out what's gone wrong with my json serializing, have the current version of my app with and old one and am finding some surprising differences in the way JSON.stringify() works (Using the JSON library from json.org).
In the old version of my app:
JSON.stringify({"a":[1,2]})
gives me this;
"{\"a\":[1,2]}"
in the new version,
JSON.stringify({"a":[1,2]})
gives me this;
"{\"a\":\"[1, 2]\"}"
any idea what could have changed to make the same library put quotes around the array brackets in the new version?
Since JSON.stringify has been shipping with some browsers lately, I would suggest using it instead of Prototype’s toJSON. You would then check for window.JSON && window.JSON.stringify and only include the json.org library otherwise (via document.createElement('script')…). To resolve the incompatibilities, use:
if(window.Prototype) {
delete Object.prototype.toJSON;
delete Array.prototype.toJSON;
delete Hash.prototype.toJSON;
delete String.prototype.toJSON;
}
The function JSON.stringify() defined in ECMAScript 5 and above (Page 201 - the JSON Object, pseudo-code Page 205), uses the function toJSON() when available on objects.
Because Prototype.js (or another library that you are using) defines an Array.prototype.toJSON() function, arrays are first converted to strings using Array.prototype.toJSON() then string quoted by JSON.stringify(), hence the incorrect extra quotes around the arrays.
The solution is therefore straight-forward and trivial (this is a simplified version of Raphael Schweikert's answer):
delete Array.prototype.toJSON
This produces of course side effects on libraries that rely on a toJSON() function property for arrays. But I find this a minor inconvenience considering the incompatibility with ECMAScript 5.
It must be noted that the JSON Object defined in ECMAScript 5 is efficiently implemented in modern browsers and therefore the best solution is to conform to the standard and modify existing libraries.
A possible solution which will not affect other Prototype dependencies would be:
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;
};
This takes care of the Array toJSON incompatibility with JSON.stringify and also retains toJSON functionality as other Prototype libraries may depend on it.
Edit to make a bit more accurate:
The problem key bit of code is in the JSON library from JSON.org (and other implementations of ECMAScript 5's JSON object):
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
The problem is that the Prototype library extends Array to include a toJSON method, which the JSON object will call in the code above. When the JSON object hits the array value it calls toJSON on the array which is defined in Prototype, and that method returns a string version of the array. Hence, the quotes around the array brackets.
If you delete toJSON from the Array object the JSON library should work properly. Or, just use the JSON library.
I think a better solution would be to include this just after prototype has been loaded
JSON = JSON || {};
JSON.stringify = function(value) { return value.toJSON(); };
JSON.parse = JSON.parse || function(jsonsring) { return jsonsring.evalJSON(true); };
This makes the prototype function available as the standard JSON.stringify() and JSON.parse(), but keeps the native JSON.parse() if it is available, so this makes things more compatible with older browsers.
I'm not that fluent with Prototype, but I saw this in its docs:
Object.toJSON({"a":[1,2]})
I'm not sure if this would have the same problem the current encoding has, though.
There's also a longer tutorial about using JSON with Prototype.
This is the code I used for the same issue:
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)
}
You check if Prototype exists, then you check the version. If old version use Object.toJSON (if is defined) in all other cases fallback to JSON.stringify()
Here's how I'm dealing with it.
var methodCallString = Object.toJSON? Object.toJSON(options.jsonMethodCall) : JSON.stringify(options.jsonMethodCall);
My tolerant solution checks whether Array.prototype.toJSON is harmful for JSON stringify and keeps it when possible to let the surrounding code work as expected:
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
}
}
As people have pointed out, this is due to Prototype.js - specifically versions prior to 1.7. I had a similar situation but had to have code that operated whether Prototype.js was there or not; this means I can't just delete the Array.prototype.toJSON as I'm not sure what relies on it. For that situation this is the best solution I came up with:
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
}
}
Hopefully it will help someone.
If you don't want to kill everything, and have a code that would be okay on most browsers, you could do it this way :
(function (undefined) { // This is just to limit _json_stringify to this scope and to redefine undefined in case it was
if (true ||typeof (Prototype) !== 'undefined') {
// First, ensure we can access the prototype of an object.
// See http://stackoverflow.com/questions/7662147/how-to-access-object-prototype-in-javascript
if(typeof (Object.getPrototypeOf) === 'undefined') {
if(({}).__proto__ === Object.prototype && ([]).__proto__ === Array.prototype) {
Object.getPrototypeOf = function getPrototypeOf (object) {
return object.__proto__;
};
} else {
Object.getPrototypeOf = function getPrototypeOf (object) {
// May break if the constructor has been changed or removed
return object.constructor ? object.constructor.prototype : undefined;
}
}
}
var _json_stringify = JSON.stringify; // We save the actual JSON.stringify
JSON.stringify = function stringify (obj) {
var obj_prototype = Object.getPrototypeOf(obj),
old_json = obj_prototype.toJSON, // We save the toJSON of the object
res = null;
if (old_json) { // If toJSON exists on the object
obj_prototype.toJSON = undefined;
}
res = _json_stringify.apply(this, arguments);
if (old_json)
obj_prototype.toJSON = old_json;
return res;
};
}
}.call(this));
This seems complex, but this is complex only to handle most use cases.
The main idea is overriding JSON.stringify to remove toJSON from the object passed as an argument, then call the old JSON.stringify, and finally restore it.

Categories

Resources