zomgistania

Main page | About | Articles | Previous Posts | Archives

Wednesday, August 16, 2006

Serialization of JavaScript objects

In the widget I'm working on I have to save the users preferences and settings. In Opera Widgets, this can be done by using a JavaScript function widget.setPreferenceForKey which can be used to store strings with keys.

I had written a function in my Clock-class which made a string of its data, like the name, GMT offset, GMT minute offset, DST settings etc.

As you can see, there's quite a lot of things already on that list. I started to getting into trouble when I started working on alerts.

I wanted to save the alerts to each clock, so I would've had to write even more code to get a string of the clock, which I just found a bit stupid. In PHP you have a Serialize() function which creates a string of an object or an array, which I thought would be very useful to have in JavaScript as well for purproses like this.


So I coded my own Serialize() and Unserialize() functions for JS!


How it works

The Serialize-function takes an object or an array as its parameter and returns a string representation of it, which can be then given to Unserialize() along with a base-object and you get an object with the same properties as when Serialized.

For a simple array, the serialize output might look like this:

s:0:5:Hello:n:1:2:20:s:2:14:I'm the cell 2

The above data would result in an array as follows:

Array[0] = "Hello";
Array[1] = 20;
Array[2] = "I'm the cell 2";


So how exactly is that converted into an obscure string like that?


The Serialize() -code


//JavaScript object serializer
// Returns a string representation of an object
function Serialize(o)
{

//The serialized data is stored here
var data = '';

//It's an array if o.length is defined
//Arrays must be enumereated differently than
//other objects, so the check has to be made
if(o.length)
{
//Array-mode

for(var i = 0; i < o.length; i++)
{
ParseProp(i);
}
}
else
{
//Object-mode

for(property in o)
{
ParseProp(property);
}
}
//strip : from end
data = data.substr(0,(data.length-1));

return data;

//Internal function which parses the properties
function ParseProp(p)
{
//Functions aren't serialized
if(typeof o[p] != 'function')
{
switch(typeof o[p])
{
//If property is a number
case 'number':
//Convert to string and find length
var len = String(o[p]).length;
data += 'n:'+p+':'+len+':'+o[p];
break;

//if prop is a string
case 'string':
var len = o[p].length;
data += 's:'+p+':'+len+':'+o[p];
break;

//If prop is bool
case 'boolean':
var b = (o[p]) ? 1 : 0;
data += 'b:'+p+':'+b;
break;

//If prop is another object
case 'object':
//Run a serialize on the child-object
var objdata = Serialize(o[p]);
var len = objdata.length; //take data length

//Find the type of the object from its constructor
var type = o[p].constructor.toString();

//Find location of the constructors name
var sindex = type.indexOf(' ') + 1;
var sindex2 = type.indexOf('(');

//Extract the name of the constructor
type = type.substring(sindex,sindex2);


data += 'o:'+type+':'+p+':'+len+':'+objdata;
break;

}
data += ':';
}
}
}


There we go. So firstly, the code checks whether the object is an array. Arrays require a bit different approach to enumerating through its properties as seen in the code.

The ParseProp function inside the Serialize does most of the work. It parses the properties of the object into strings. First, it uses typeof to check if the property is not a function, as we don't want to serialize functions.

Then we continue to a switch statement to parse the different types.

Numbers and strings are parsed in almost the same way: we find its length by converting it into a string using String() and its .length property. Then its saved into the data-block. n is the key for Number type, after which comes the name of the property and after that the length and finally the value.

Strings only differ that we don't need to convert them to find the length. Their key is s.

Boolean variables (true/false) are saved into the datastring, depending on their state, as either 0 or 1 to save space.

The object parser is probably the most complicated here. First, it serializes the child-object using a recursive call to Serialize(). Then it finds the length of the serialized data.
It also has to find the type of the object, which we can find from the objects constructor with a bit of string manipulation: as the constructor looks like "function Name() { some code here }", we just take the first location of a space and the first location of a ( to find the Name part and save it.

The object notation in the data goes like this: o is the key for the object, then comes the type name, property name, data length and the actual data.


All of the data are appended with a : if we later need to add more properties to the string, so after all props have been parsed, we simply remove the last character from the data and return it.


The Unserialize() -code


//JavaScript object unserializer
// Returns an object from the serialized data
function Unserialize(data,o)
{

//Invalid Serialize-data. Should begin with s,b,o or n
if(data[0] != 's' && data[0] != 'b' && data[0] != 'o' && data[0] != 'n')
return o;

var i; //Counter defined here so it's accessible from GetNextString

for(i = 0; i < data.length; i++)
{
var mode = GetNextString(); //Get mode

var d = null; //stores the final prop
var propname; //stores name of the prop
if(mode == 's')
{
//string-mode

propname = GetNextString();
//Find next delimiter

var len = Number(GetNextString());
//Extract data
d = data.substr(i,len);

//i is incremented in the end, otherwise we'd have
//to go forward datalength + 1 here!
i += len; //advance pointer
}
else if(mode == 'n')
{
//Number mode
propname = GetNextString();
var len = Number(GetNextString());
d = Number(data.substr(i,len));
i += len;
}
else if(mode == 'b')
{
propname = GetNextString();

d = (data[i] == 1) ? true : false;

//i is incremented in the end, otherwise we'd have
//to go forward + 2 here!
i++; //pointer forward
}
else if(mode == 'o')
{
var type = GetNextString();
propname = GetNextString();
var len = Number(GetNextString());
var odata = data.substr(i,len);

//We need to use eval to create a new
//instance of "type"
eval('d = new '+type);


d = Unserialize(odata,d);

i += len;
}

//Save
o[propname] = d;

}

return o;

//this is used to get data from the datastring
function GetNextString()
{
var n = data.indexOf(':',i);

//Extract property name
var strdata = data.substring(i,n);

i += strdata.length + 1; //advance pointer

return strdata;
}
}


Now this is a bit more complicated than the Serialize() function. You have to pass the serialized data as a parameter and an object which is used to save the props from the serialized data. So if you had an serialized array, the object has to be an array.

First, there's a check to determine whether the serialized data is ok. This is done by checking whether it starts with one of the keys used for different datatypes. Not absolutely necessary, but I think it's a good thing to have.

The i variable is defined before the for this time, so GetNextString can access it. GetNextString is just something which simplifies a few string operations, so I don't have to write a substring call and an i+=length call every time.


We loop until we have reached the end of the data. First, we get the mode (n for number, s for string, b for bool, o for object) from the data.

Parsing strings is pretty simple. The propertys name and length are taken from the data to be able to use substr to extract the actual data.

Note: we can't use GetNextString to extract the actual data! If there's a : in the data, things will screw up, so that's why we also store the data length.


Parsing numbers is pretty much the same as for strings, we just use Number() to convert the data into a number.

Boolean parsing is even more simple, as it only takes one character after the initial prop name.


Now, the object parser is yet again a bit more complicated as expected.

First we find the type name, prop name, data length and the data from the serialized data. Then we use eval() to create a new instance of the type stored in the variable. After this it's just a matter of a recursive Unserialize call and we're done.

After the if-section, the property is saved into the object. Finally the new object is returned.


Wrap up

Using this method to save data is pretty simple:

var a = new Array('Example',true,'Yes sire',1234);
var data = Serialize(a); //store a to a string

var newArray = new Array();
newArray = Unserialize(data,newArray);
//now newArray contains the same things as a



There are, however, some things to note:

In custom objects, only the variables defined with this are saved! Take this into account when working with this. I'm not sure if there's a method to save private members as well, so if you know a better method of going through the properties, please post a comment.

Labels: , ,

1 Comments:

  • I should come to your house and steal the hyphen key from your keyboard someday.

    By Anonymous Icheb, at 3:32 AM  

Post a Comment

<< Home