22.12.07

Serializing JavaScript objects

I once have written a JavaScript function that dumps a JavaScript object in a human readable format. It was something like JSON, but it also handled objects containing circular references shared object references. I must admit that it was written badly.

After reading this, I thought that maybe it's a good idea to change the previous code for serializing JavaScript objects. I also refactored the code. You can find it below. I didn't test it thoroughly, so be careful.

An example of what it can do:
var x = {};

x.foo  = new Object();
x.self = x;
x.arr  = [x.self];
x.bar  = x.foo;

var s = serialize(x);
var y = deserialize(s);
alert(y == y.self);    // true
alert(y == y.arr[0]);  // true
alert(y.foo == y.bar); // true

alert(s);
// {
//     'foo': {},
//     'self': { _root_: [  ] },
//     'arr': [
//         { _root_: [  ] }
//     ],
//     'bar': { _root_: [ "foo" ] }
// }
Addition: I think that I have to express that better: don't expect it to serialize functions, regular expressions, images (or any other browser object), etc. It's just something like JSON, but it also handles shared object references. Code:
//============================================================================//
//  JavaScript object de/serialization with circular references.              //
//                                                                            //
//  author: Mehmet Yavuz Selim Soyturk                                        //
//  e-mail: Mehmet dot Yavuz dot Selim at gmail dot com                       //
//============================================================================//

Array.prototype.findIf = function(predicate) {
    for(var i in this) {
        if (predicate(this[i]))
            return i;
    }
    return -1;
};
Array.prototype.map = function(func) {
    var len = this.length;
    var result = [];
    for (var i=0; i<len; i++) {
        result[i] = func(this[i]);
    }
    return result;
};
Array.prototype.appended = function(value) {
    var copy = this.slice(0);
    copy[copy.length] = value;
    return copy;
};

//============================================================================//
//  SERIALIZATION                                                             //
//============================================================================//

function indented(n) {
    var s = '';
    for(var i=0; i<n; i++) s += '    ';
    return s;
}

function serializePrimitive(value) {
    if (typeof value == 'string')
        return '"' + value + '"';
    else
        return '' + value;
}

function serializePrimitiveArray(arr) {
    var s = "[ ";
    s += arr.map(serializePrimitive).join(', ');
    s += " ]";
    return s;
}

function serializeArray(arr, seen, indices, depth) {
    if (arr.length == 0)
        return '[]';

    seen[seen.length] = {obj: arr, indices: indices};
    
    var result = '[\n';
    for (var i=0; i<arr.length; i++) {
        result += indented(depth + 1);
        result += serializeAny(arr[i], seen, indices.appended(i), depth + 1);
        result += (i == arr.length - 1) ? '' : ', ';
        result += '\n';
    }
    result += indented(depth) + ']';

    return result;
}

function serializeObject(obj, seen, indices, depth) {
    seen[seen.length] = {obj: obj, indices: indices};
    
    var result = '{\n';
    var count = 0;
    for (var i in obj) {
        if (typeof obj[i] != 'function') {
            count++;
            result += indented(depth+1);
            result += "'" + i + "': ";
            result += serializeAny(obj[i], seen, indices.appended(i), depth + 1);
            result += ", \n"; // bad hack, see next
        }
    }
    if (count > 0) {
        result = result.substring(0, result.length-3) + '\n'; // bad hack
        return result + indented(depth) + '}';
    }
    else {
        return '{}';
    }
}


function serializeAny(value, seen, indices, depth) {
    var t = typeof(value);
    var prevIndex;
    
    if (t == 'function') {
        throw new Error("Cannot serialize function. Keys from root: " + 
            serializePrimitiveArray(indices));
    }
    else if (t != 'object') {
        return serializePrimitive(value);
    }
    else if ( (prevIndex = seen.findIf
                    ( function(obj) { return obj.obj == value } )
              ) >= 0) {
        return '{ _root_: ' + serializePrimitiveArray(seen[prevIndex].indices) + ' }';
    }
    else if (value.constructor == Array) {
        return serializeArray(value, seen, indices, depth);
    }
    else {
        return serializeObject(value, seen, indices, depth);
    }
};

function serialize(obj) {
   return serializeAny(obj, [], [], 0);
}


//============================================================================//
//  DESERIALIZATION                                                           //
//============================================================================//

function followIndices(obj, indices) {
    for (var i=0; i<indices.length; i++)
        obj = obj[indices[i]];
        
    return obj;
}

function replaceRootRefs(obj, root) {
    if (typeof obj != 'object' || obj == null)
        return;
    
    for (var i in obj) {
        var prop = obj[i];
        if (typeof prop == 'object' && prop != null) {
            if ('_root_' in prop)
                obj[i] = followIndices(root, prop._root_);
            replaceRootRefs(prop, root);
        }
    }
}

function deserialize(str) {
    var obj = eval( '(' + str + ')' );
    if (str.indexOf('_root_') < 0)
        return obj;
    
    replaceRootRefs(obj, obj);
    return obj;
}

3 comments:

Anonymous said...

I use to load interface with Javascript with the toSource function, and i try your serialize library.
you say it can't run with a function, but i replace the throw error in your serializeAny function by "return value.toString();" and it works very well.

Chris Dew said...

The link 'here' is broken. Could you fix it please?

herhangibiri said...

@Chris, thanks. Added code in the blog post.