Recently, in a personal project I'm working on, I came across a need to be able to represent any Javascript object as a string. This isn't a problem since just about every object in Javascript can be represented with JSON (Javascript Object Notation). Every modern browser can parse JSON for you easily enough through eval(..), and Gecko-based browsers even have the ability to reverse the process ("uneval" if you will) and give you back a string representation of an object through a call to .toSource().
If you need this ability in any other browser though, you're gonna have to write it yourself. I needed this ability, so I wrote it (and posted it here for your enjoyment!)
Gecko-based browsers, .toSource():
Gecko-based browsers provide a handy function: .toSource() that you can call on any object in your Javascript code to get back a JSON-like representation of that object.
{
this.Name = name;
this.Age = age;
this.Speak = function() { alert('Meow!'); };
}
var garfield = new Cat('Garfield', 5);
alert(garfield.toSource());
/* garfield.toSource() yields:
({Name:"Garfield", Age:5, Speak:(function () {alert("Meow!");})})
*/
Pretty simple right? You have an object; you want a string. Just invoke the object's .toSource() function.
Serializing objects in other browsers:
Serializing an object manually (as is required by non Gecko-based browsers) requires a bit of recursion. Simple types like integers, booleans, and even functions are trivial to represent as strings. Objects though, are more complicated because they can contain simple types or custom objects (which would need to be serialized themselves). Those "inner" objects could in turn, contain more custom objects, which would also need to be serialized, and this pattern could (theoretically) go on forever.
In practice of course, this pattern will (had better) come to an end. And we can leverage that fact to write a recursive function that will return a string representation (in JSON format) of a given object.
The serialize(..) function:
First the code, then the explanation.
{
// Let Gecko browsers do this the easy way
if (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 + '\'';
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 += serialize(_obj[i]) + ','; }
str += serialize(_obj[i]) + ']';
}
else
{
str = '{';
var key;
for (key in _obj) { str += key + ':' + serialize(_obj[key]) + ','; }
str = str.replace(/\,$/, '') + '}';
}
return str;
break;
default:
return 'UNKNOWN';
break;
}
}
Explaining a recursive function can be difficult, but I'll give it shot:
The function accepts just one parameter: the object (_obj) to be serialized. If you'll remember, I mentioned previously that simple types (string, boolean, number, etc...) were trivial because they all have an obvious string representation already. Complex types though, are more difficult because they can be made up of additional complex types, which in turn could be made up of additional complex types (and so on).
Of course, this pattern will eventually end; ultimately, everything is made up of simple types that have a string representation. The trick is figuring out how to traverse through this maze of "types within types." Recursion (simply stated: a function that calls itself) is perhaps the easiest way to solve this "types within types" problem.
Recursive functions always have a termination case -- something which causes the function to stop calling itself. Otherwise, the function would go into an infinite loop. In our function, there are actually four different cases in which the serialize(..) doesn't need to call itself:
- typeof _obj is a number
- typeof _obj is a boolean
- typeof _obj is a function
- typeof _obj is a string
If any of the above four conditions are met, returning a string representation is trivial, so we simply do it.
_obj when it is of type string: wrap it in quotes. We need to do this, because JSON expects it, and if we ever want to be able to eval(..) the result of a serialize(..) call, we'll need these quotes.
The only other case to deal with is when _obj is of type object. Within this case though, there are two "sub-cases" we need to deal with. The first is when _obj is an Array, or when it has a .callee property (more on that later). The second is well... anything else.
Basic Object Types
Your every-day, run-of-the-mill, object in Javascript can be represented as JSON with:
{ key1: val1, key2: val2, ... }
Where the keys are strings and the vals can be any simple type, or some custom object you've dreamed up. The logic I've used is to simply loop through a given object's keys and build a string of comma delimited, serialized key/value pairs that are wrapped in { and }.
Notice I said serialized key/value pairs. Here, our function is calling itself as it builds the object representation. This ensures that any objects within the object being serialized will also be serialized. If we didn't do this, we'd end up with a lot of strings that looked (something) like this:
{ key1: [object Object], key2: [object Object], etc... }
And that's clearly not what we want. We want those inner objects to be serialized as well, and that's what the recursive nature of our function will take care of for us.
Arrays
When _obj happens to be, not just any object, but more specifically, an Array, we have a better way of representing that as a string:
[ val1, val2, val3, ... ]
The logic I used here is to simply iterate through the array building a comma delimited list of serialized values, wrapped in [ and ]. Arrays in Javascript already have a .toString() function, but we can't use it here; if the Array contains objects, then the result of the Array 's .toString() could end up something like:
[ [object Object], [object Object], etc... ]
Again, not what we want, so we need to make sure we recursively serialize all of the elements in the array.
Arguments (the .callee "gotcha")
There's one bit of code I haven't discussed yet and it deals with the (possible) .callee property of the passed in _obj. It turns out that .toSource() (native function used by Gecko-based browsers) doesn't do anything very useful when called on an arguments object.
The arguments object is an array-like (but not an Array) object that is automatically available within the scope of every function. Unfortunately, no matter what is contained within that arguments object, calling .toSource() on it will always return "({})"
In order to be able to serialize an arguments object then, we need some way to detect it and then treat it like an array. The .callee property is a good choice because arguments objects have it, but other objects (to the best of my knowledge) do not.
In the case of the serialize(..) function, I decided to use the native .toSource() function whenever it was available, unless the object were an arguments object, in which case, I send Gecko-based browsers down the same path as all other browsers for serialization.
Finally, just for good measure, I've added a default case which returns the string UNKNOWN to handle a situation where no other cases applied. Of course, we probably don't want UNKNOWN showing up in the our serialized strings, but it probably won't do much (immediate) harm if it ever does show up, and its presence would be a helpful indicator that there is some case not being met (that probably needs to be).
Conclusion:
What would you ever use a function like this for? Well, In my case, I wanted to use a Javascript object as the key for a hash, but if I tried to do that, Javascript would just represent all of my (different) objects as the same string: [object Object], which wouldn't do me any good. By serializing the object, I can then use that serialized representation as a key in the hash.
It's worked well for my need so far, but I haven't tested it a great deal. If you find any bugs or short-comings, please let me know.
As always, comments are welcome.
29 Responses
Just what I was looking for! I've been looking for a way to serialize functions (as functions are actually objects in Javascript), however my first attempt using Douglas Crockford's json2.js library (http://www.json.org/json2.js) didn't always work properly when serializing functions (though it works fine for serializing and deserializing other Javascript data structures).
Your serialize function has nicely stepped into the breach!
Jason,
Glad it worked for you! Please let me know if you encounter any problems with it.
Hey. I tried your function and it worked well in Firefox, but I ran into problems within Safari. Did you test this browser? Looks like it hangs on the first test, when it tries to employ the .toSource method.
Pongi,
Thanks for pointing this out. I will double check later today and post back. I admit that back when I wrote this post I was not very good about checking in Safari. I have access to a Mac these days though, so I do it much more often lately. I'll give it a go and report back.
Pongi,
I just tested Safari3/Win with a simple Cat object and it worked:
var c = new Cat('Garfield', 5);
alert(serialize(c));
// yields: {Name:'Garfield',Age:5,Speak:function () { alert("Meow!"); }}
I will try Safari2/Mac a little later. Do you have any additional information on what part of the code you think is causing Safari trouble? Which version of Safari by the way?
Ok, I tested this in Safari2 and 3 for Mac, and I had a problem in Safari2. It seems like some sort of an encoding issue because I copied and pasted the code from my blog into TextEdit and Safari kept barking about "parsing errors" (even though there were none).
So I tried creating a brand new file on my Windows machine and uploading it to my server (Linux) and accessing that from Safari2/Mac, and then everything worked.
See if this works for you in Safari: http://blog.stchur.com/blogcode/tmp/serialize.html
The JavaScript function toSource() does not produce double quotes around property names. It is fine in most cases but php's json_decode() function expects the double quotes. So I had to comment out the toSource() section and use the recursive function instead. Also I changed the single quotes you use to double quotes around all property values, and do a string replacement to escape all double quotes in a string.
@fotonics:
I wasn't aware of the issue with PHP's json_decode() function. Thanks for pointing that out — I'm sure it will be helpful for more than one person down the road trying to use this function in concert with some PHP script.
in first not bad adding this lines
if(_obj===null) return 'null';
if(_obj===undefined)return 'undefined';
Regard from Russia
Aky,
Not at all a bad suggestion! Thanks.
-Steve
hi buddy,
thnx a lot… that is what i was looking all throughtout internet.
It was so informative as well as usefull (to be directly used :p )
thnx a lot once again…
Amit
Hi!
Thanks a lot for this nice piece of code!
Hi sstchur
Congratulations, very nice, very tight code.
We are using it for a new tool that we will be releasing very soon.
I can safely promise a serious testing of your code.
If you email me, I can give you a preview.
Hi,
Quite nice piece of code but unfortunatelly I encountered a problem.
The problem with Safari 3.1 (3.1.2 to be precise) on Mac is the following:
var a=[1,2,3];
a.constructor === Array
=> false
unfortunatelly:
typeof a.callee !== 'undefined'
=> false
so, this won't translate an array properly.
One of possible solutions (a bit ugly one):
a.constructor.toString().indexOf('Array') != -1
=> true
so, changing following line:
if (_obj.constructor === Array || typeof _obj.callee !== 'undefined')
to:
if (_obj.constructor === Array || typeof _obj.callee !== 'undefined' || _obj.constructor.toString().indexOf('Array') != -1)
should solve the problem.
Regards
Thanks Jarek! I will make a note of this, as I think this affects me in more than just this piece of code.
Hi Stephen,
very nice and useful code and exactly, what I'm looking for! Thanks a lot for contributing.
As Jarek mentioned before, there are problems with the correct detection of Arrays. My suggestion for solving this is using the property length which does not exist in Object().
So my piece of code looks like:
if (theObj.constructor === Array
|| theObj.length
|| typeof(theObj.callee) !== 'undefined')
Regards from munich,
Oliver
To do the string thing correctly, you need to escape not just any quotes that are in there, but also any /s.
I recommend something like
var stringify_string = function(str) {
str = str.replace(/\\/g, "\\\\");
str = str.replace(/\"/g, "\\\"");
return "\""+str+"\"";
};
The code you provide also, when evaluated, creates an object that is not of the right type. You can get around this in some situations on firefox by setting the __proto__ property – but that's only if you have a name that it should be set to.
Other problems with general usage are loops, or equivalent objects. If an object or its descendant contains a reference to itself, this method will loop forever. Equal objects is a pain as well – if there are two references to the same object, when you deserialise, they will no longer be equal to each other. You can fix using something like this:
{bob: (tmp={key: 1}), alf: tmp}
When that is evaluated, you end up with a bob and alf being === each other. Of course, other object literal notation parsers may not cope with that, and you have set a global there.
Fixing loops is a real pain – you need to keep a list of all objects that you've dealt with so far, and use references to them if they are the same as an earlier one.
@kyb:
Some good points. Take the code for what it's worth… not appropriate for every single possible scenario I guess.
Thanks so much for this function. Works great and compact too!
Serializing arguments:
alert(Array.prototype.slice.apply(arguments).toString());
[...] all I could find online were scripts and jquery plugins for serializing a html form. Then, I found this, a script that takes advantage of the .toSource() method available in Gecko-based browsers and for [...]
That's why the blog is here offering public code snippets
Contribute. Code it up and post it, so everyone can benefit!
Very useful – took all the pain out of throwing an array of objects between pages
+props
How to serialize and deseialize 2d array like
INternalID NAME AGE
100 A 26
101 B 32
102 C 89
[...] 来源:http://blog.stchur.com/2007/04/06/serializing-objects-in-javascript/–Serializing Objects in Javascript [...]
Nice,
It is just what i was looking for
tyvm
Last problem is with recursive structures
I've encountered this when serializing Box2dWeb b2World object. It throws (obviously) Maximum call stack size exceeded Exception.
What is a Box2dWeb b2World object?
Your string serialization isn't properly escaping special characters. For example, quotes inside the string should be prefixed with backslashes; backslashes in the string should be replaced with double backslashes; characters outside the ASCII character range should (probably) be replaced with their hexadeximal unicode equivalent (\xNNNN). The way it is now, all you have to do to break it is to include a single quote within a string anywhere in the object tree. The result will be unparseable JavaScript due to syntax errors. Try this instead, which is straight from the horse's moutn (download links at the bottom of the page): http://www.json.org/js.html