Serializing Javascript Closures
In Javascript, the eval function lets you dynamically look up variables in scope, so you can do a terrible hack like this to sort-of serialize a closure if you’re in the same scope as the variables being closed over:
Given StringMap.js and atLeastFreeVarNames.js from SES, one can define the following:
var s = function(f) { // Find free vars in f. (This depends, of course, on // Function.prototype.toString being unchanged.) var code = f.toString(); var free = ses.atLeastFreeVarNames(code); // Construct code that evaluates to an environment object. var env = ["({"]; for (var i = 0, len = free.length; i < len; ++i) { env.push('"'); env.push(free[i]); env.push('":(function(){try{return eval("('); env.push(free[i]); env.push(')")}catch(_){return {}}})()'); env.push(','); } env.pop(); env.push("})"); return "({code:" + JSON.stringify(code) + ",env:" + env.join("") + "})"; }; // See https://gist.github.com/Hoff97/9842228 or // http://jsfiddle.net/7UYd4/1/ // for versions of stringify that handle cycles. var t = function(x) { return '(' + JSON.stringify(x) + ')'; }
Then you can use these definitions to serialize inline definitions that only close over “stringifiable” objects or objects behind cut points (see deserialization below):
var baz = {x: 1}; var serializedClosure = t(eval(s( function bar(foo) { console.log('hi'); return ++(baz.x)+foo /*fiddle*/; } ))); // serializedClosure === '({"code":"function bar(foo) { console.log('hi'); return ++(baz.x)+foo /*fiddle*/; }","env":{"function":{},"bar":{},"foo":{},"console":{},"log":{},"hi":{},"return":{},"baz":{"x":1},"x":{},"fiddle":{}}})'
The string serializedClosure can then be stashed somewhere. When it’s time to deserialize, do the following:
var d = function(closure, localBindings) { localBindings = localBindings || {}; return function() { with(closure.env) { with (localBindings) { return eval("(" + closure.code + ")").apply(this, arguments); } } }; }; var closure = eval(serializedClosure); // Hook up local values if you want them. var deserializedFn = d(closure, {console: console}); deserializedFn("foo"); // Prints "2foo" to the console. deserializedFn("bar"); // Prints "3bar" to the console.
If you want to store the updated state, just re-stringify the closure:
var newSerializedClosure = t(closure); // newSerializedClosure === '({"code":"function bar(foo) { console.log('hi'); return ++(baz.x)+foo /*fiddle*/; }","env":{"function":{},"bar":{},"foo":{},"console":{},"log":{},"hi":{},"return":{},"baz":{"x":3},"x":{},"fiddle":{}}})' // Note that baz.x is now 3.
As I said, a very ugly hack, but still might be useful somewhere.
leave a comment