For example, say I have a function defined as follows:
function foo() {
return "Hello, serialized world!";
}
I want to be able to serialize that function and store it using localStorage
. How can I go about doing that?
For example, say I have a function defined as follows:
function foo() {
return "Hello, serialized world!";
}
I want to be able to serialize that function and store it using localStorage
. How can I go about doing that?
Most browsers (Chrome, Safari, Firefox, possibly others) return the definition of functions from the .toString()
method:
> function foo() { return 42; }
> foo.toString()
"function foo() { return 42; }"
Just be careful because native functions won't serialize properly. For example:
> alert.toString()
"function alert() { [native code] }"
function foo() {
alert('native function');
return 'Hello, serialised world!';
}
var storedFunction = foo.toString();
var actualFunction = new Function('return ' + foo.toString())()
foo.toString() will be string version of the function foo
"function foo() { ... return 'Hello, serialised world!';}"
But new Function
takes the body of a function and not the function itself.
See MDN: Function
So we can create a function that returns us back this function and assign it to some variable.
"return function foo() { ... return 'Hello, serialised world!';}"
So now when we pass this string to the constructor we get a function and we immediately execute it to get back our original function. :)
I made this answer to address some pretty big flaws with the existing answers: .toString()
/eval()
and new Function()
on their own wont work at all if your function uses this
or named arguments (function (named, arg) {}
), respectively.
Using toJSON()
below, all you need to do is call JSON.stringify()
as usual on the function, and use Function.deserialise
when parse()
ing.
The following wont work for concise functions (hello => 'there'
), but for standard ES5 fat functions it'll return it as it was defined, closures notwithstanding of course. My other answer will work with all that ES6 goodness.
Function.prototype.toJSON = function() {
var parts = this
.toString()
.match(/^\s*function[^(]*\(([^)]*)\)\s*{(.*)}\s*$/)
;
if (parts == null)
throw 'Function form not supported';
return [
'window.Function',
parts[1].trim().split(/\s*,\s*/),
parts[2]
];
};
Function.deserialise = function(key, data) {
return (data instanceof Array && data[0] == 'window.Function') ?
new (Function.bind.apply(Function, [Function].concat(data[1], [data[2]]))) :
data
;
};
Take a look at the DEMO
At it's simplest:
var test = function(where) { return 'hello ' + where; };
test = JSON.parse(JSON.stringify(test), Function.deserialise);
console.log(test('there'));
//prints 'hello there'
More usefully, you can serialise entire objects containing functions and pull them back out:
test = {
a : 2,
run : function(x, y, z) { return this.a + x + y + z; }
};
var serialised = JSON.stringify(test);
console.log(serialised);
console.log(typeof serialised);
var tester = JSON.parse(serialised, Function.deserialise);
console.log(tester.run(3, 4, 5));
Outputs:
{"a":2,"run":["window.Function",["x","y","z"]," return this.a + x + y + z; "]}
string
14
I didn't test older IE's, but it works on IE11, FF, Chrome, Edge.
NB, the name
of the function is lost, if you use that property then there's nothing you can do, really.
You can change it to not use prototype
easily, but that's for you to do if that's what you need.
If you needed a way to serialise Arrow Functions in ES6 I have written a serialiser that makes everything work.
All you need to do is call JSON.stringify()
as usual on the function or object containing the function, and call Function.deserialise
on the other side for the magic to work.
Obviously you shouldn't expect closures to work, it is serialisation after all, but defaults, destructuring, this
, arguments
, class
member functions, it'll all be preserved.
If you're only using ES5 notations please just use my other answer. This one really is above and beyond
Working in Chrome/Firefox/Edge.
Bellow is the output from the demo; a few functions, the serialised string, then calling the new function created after deserialisation.
test = {
//make the function
run : function name(x, y, z) { return this.a + x + y + z; },
a : 2
};
//serialise it, see what it looks like
test = JSON.stringify(test) //{"run":["window.Function",["x","y","z"],"return this.a + x + y + z;"],"a":2}
test = JSON.parse(test, Function.deserialise)
//see if `this` worked, should be 2+3+4+5 : 14
test.run(3, 4, 5) //14
test = () => 7
test = JSON.stringify(test) //["window.Function",[""],"return 7"]
JSON.parse(test, Function.deserialise)() //7
test = material => material.length
test = JSON.stringify(test) //["window.Function",["material"],"return material.length"]
JSON.parse(test, Function.deserialise)([1, 2, 3]) //3
test = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c
test = JSON.stringify(test) //["window.Function",["[a, b] = [1, 2]","{ x: c } = { x: a + b }"],"return a + b + c"]
JSON.parse(test, Function.deserialise)([3, 4]) //14
class Bob {
constructor(bob) { this.bob = bob; }
//a fat function with no `function` keyword!!
test() { return this.bob; }
toJSON() { return {bob:this.bob, test:this.test} }
}
test = new Bob(7);
test.test(); //7
test = JSON.stringify(test); //{"bob":7,"test":["window.Function",[""],"return this.bob;"]}
test = JSON.parse(test, Function.deserialise);
test.test(); //7
And finally, the magic
Function.deserialise = function(key, data) {
return (data instanceof Array && data[0] == 'window.Function') ?
new (Function.bind.apply(Function, [Function].concat(data[1], [data[2]]))) :
data
;
};
Function.prototype.toJSON = function() {
var whitespace = /\s/;
var pair = /\(\)|\[\]|\{\}/;
var args = new Array();
var string = this.toString();
var fat = (new RegExp(
'^\s*(' +
((this.name) ? this.name + '|' : '') +
'function' +
')[^)]*\\('
)).test(string);
var state = 'start';
var depth = new Array();
var tmp;
for (var index = 0; index < string.length; ++index) {
var ch = string[index];
switch (state) {
case 'start':
if (whitespace.test(ch) || (fat && ch != '('))
continue;
if (ch == '(') {
state = 'arg';
tmp = index + 1;
}
else {
state = 'singleArg';
tmp = index;
}
break;
case 'arg':
case 'singleArg':
var escaped = depth.length > 0 && depth[depth.length - 1] == '\\';
if (escaped) {
depth.pop();
continue;
}
if (whitespace.test(ch))
continue;
switch (ch) {
case '\\':
depth.push(ch);
break;
case ']':
case '}':
case ')':
if (depth.length > 0) {
if (pair.test(depth[depth.length - 1] + ch))
depth.pop();
continue;
}
if (state == 'singleArg')
throw '';
args.push(string.substring(tmp, index).trim());
state = (fat) ? 'body' : 'arrow';
break;
case ',':
if (depth.length > 0)
continue;
if (state == 'singleArg')
throw '';
args.push(string.substring(tmp, index).trim());
tmp = index + 1;
break;
case '>':
if (depth.length > 0)
continue;
if (string[index - 1] != '=')
continue;
if (state == 'arg')
throw '';
args.push(string.substring(tmp, index - 1).trim());
state = 'body';
break;
case '{':
case '[':
case '(':
if (
depth.length < 1 ||
!(depth[depth.length - 1] == '"' || depth[depth.length - 1] == '\'')
)
depth.push(ch);
break;
case '"':
if (depth.length < 1)
depth.push(ch);
else if (depth[depth.length - 1] == '"')
depth.pop();
break;
case '\'':
if (depth.length < 1)
depth.push(ch);
else if (depth[depth.length - 1] == '\'')
depth.pop();
break;
}
break;
case 'arrow':
if (whitespace.test(ch))
continue;
if (ch != '=')
throw '';
if (string[++index] != '>')
throw '';
state = 'body';
break;
case 'body':
if (whitespace.test(ch))
continue;
string = string.substring(index);
if (ch == '{')
string = string.replace(/^{\s*(.*)\s*}\s*$/, '$1');
else
string = 'return ' + string.trim();
index = string.length;
break;
default:
throw '';
}
}
return ['window.Function', args, string];
};
Don't serialize the call, instead try serializing the info, allowing to repeat the call, which can include things like class & method names, arguments passed into the call or just a call scenario name.
w = (function(x){
return function(y){
return x+y;
};
});""+w returns "function(x){
return function(y){
return x+y;
};
}" but ""+w(3) returns "function(y){
return x+y;
}"
which is not the same as w(3) which somehow still remembers to add 3.