Make serialized objects smarter using prototype

We can extend objects built from JSON strings by pointing to a different prototype object.

Whenever we send objects to the server, we need to serialize them, for example using JSON encoding. Any safe object encoding only serializes the data, not the code.

1
2
3
4
5
6
var foo = {
bar: 'bar',
getBar: function () { return this.bar; }
};
console.log(JSON.stringify(foo));
// {"bar":"bar"}

So the objects we receive are just data without any methods. It would be very nice to attach methods to the objects. Otherwise all functional logic has to be external, passed around anywhere the objects go:

1
2
3
4
function printName(person) { console.log(person.first, person.last); }
var person = JSON.parse('{"first":"Joe","last":"Smith"}');
printName(person);
// Joe Smith

We can add methods directly to each new object by copying them another object, using _.assign for example

1
2
3
4
5
6
7
var person = JSON.parse('{"first":"Joe","last":"Smith"}');
console.log(person.toString()); // [object Object]
var printer = {
toString: function () { return this.first + ' ' + this.last; }
};
_.assign(person, printer);
console.log(person.toString()); // Joe Smith

This approach takes too much memory: each method reference takes up space.

JavaScript has an interesting alternative: it is prototypical language, meaning an object has a prototype property pointing at another object. If we access a property or a method and it cannot be found on the object itself, the environment tries to find on its prototype, then prototype's prototype, etc. This is why every object (with the exception of bare objects) has method .toString

1
2
3
var person = JSON.parse('{"first":"Joe","last":"Smith"}');
console.log(person.toString()); // [object Object]
console.log(Object.prototype.isPrototypeOf(person)); // true

This points a way to easily add without copying methods / properties to any object parsed from JSON string. In environments where an object's prototype can be changed via __proto__ or ES6 setPrototypeOf we can directly set:

1
2
3
var person = JSON.parse('{"first":"Joe","last":"Smith"}');
person.__proto__ = printer;
console.log(person.toString()); // Joe Smith

We are not copying toString reference, instead we only repointed prototype from Object.prototype to printer. At any point we can change the single method in printer and every object changes its behavior.

1
2
3
person.__proto__ = printer;
printer.toString = function () { return this.last + ', ' + this.first; };
console.log(person.toString()); // Smith, Joe

Changing __proto__ is considered bad for performance (I have not checked myself), and seems to be unavailable in some browsers (IE < 11). In these cases, either assign the object or construct a copy with new prototype using Object.create

1
person = _.assign(Object.create(printer), person);

Finally, let us look at JSON.parse function itself. It accepts a string and a reviver function. The reviver function can transform each property and the final object before returning it. If we want to change the prototype, we could place the logic into JSON.parse call

1
2
3
4
5
6
7
8
9
var withPrinter = function (k, v) {
if (k === '') {
// v is the built object
v.__proto__ = printer;
}
return v;
};
var person = JSON.parse('{"first":"Jim","last":"Chu"}', withPrinter);
console.log(person.toString()); // Chu, Jim

You can even move this reviver function into the printer object itself and even add object validation (I am using check-types here):

1
2
3
4
5
6
7
8
9
10
11
12
var printer = {
toString: function () { return this.first + ' ' + this.last; },
reviver: function (k, v) {
if (k === '') {
check.verify.unemptyString(v.first, 'missing first name');
check.verify.unemptyString(v.last, 'missing last name');
v.__proto__ = printer;
}
return v;
}
};
var person = JSON.parse('{"first":"Jim","last":"Chu"}', printer.reviver);

In conclusion, objects received over the wire do not have to be just data, we can quickly attach methods. In most environments (WebKit, nodejs) we only point the prototype property at a given object. We even have a place to do this in JSON.parse reviver argument.

Update

I found an interesting quirk in V8 JavaScript implementation. If you create a bare object, you cannot later set its prototype. The prototype is shown, but does not work! For example:

1
2
3
4
5
6
var foo = Object.create(null);
foo.__proto__ = {
bar: 'bar'
};
foo; // { [__proto__]: { bar: 'bar' } }
foo.bar; // undefined

Weird, right?