60

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?

Akash Gupta
  • 615
  • 1
  • 5
  • 8
  • 7
    why do you want to serialize a function? – Daniel A. White Sep 13 '11 at 00:53
  • 7
    What if the function being serialised references variables/functions in the containing scope? If you deserialise it in the wrong place it won't work... – nnnnnn Sep 13 '11 at 01:07
  • 6
    Can't speak to Akash's use, but I want to serialize a function to store it as a validation function in CouchDB. The variables passed in and restricted scope are well defined. – ReactiveRaven Aug 14 '13 at 16:39
  • 56
    @DanielA.White It really irritates me when this is the first response to somebody's question! – Michael Mar 13 '14 at 05:30
  • 3
    @DanielA.White yeah, saying "you probably don't want to do this" is usually not as helpful as answering "here's how to do it...and here's why it's probably a bad idea" – Andy Jul 13 '16 at 15:30
  • @Andy it could be an x-y problem and im looking for solutions. – Daniel A. White Jul 13 '16 at 15:32
  • 2
    The OP specifically asked how to "serialize that function and store it using localStorage" so trying to speculate about some other solution is off-topic. It would only be on topic if the OP said "I want to do ...I'm trying to accomplish it by storing a function in localStorage" – Andy Jul 13 '16 at 17:31
  • 21
    People blow off questions they consider bad practice all too often on StackOverflow instead of just objectively answering the question and then adding a word of advice afterwards. The latter is much more helpful to beginners. – Andy Jul 13 '16 at 17:34
  • 1
    @Michael you're really fun! – himaru Jan 16 '23 at 17:48

6 Answers6

40

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] }"
David Wolever
  • 148,955
  • 89
  • 346
  • 502
  • 2
    How would you get the function back from the string? – Akash Gupta Sep 13 '11 at 01:31
  • 15
    @David Wolever - the ES 3 and 5 specs say that Function.prototype.toString returns "[an] implementation-dependent representation of the function" that "has the syntax of a FunctionDeclaration" (§15.3.4.2). Probably worth noting that there is nothing to say that it must be the literal code of the function, it could just be `function foo(){/* returns 42 */}`. – RobG Sep 13 '11 at 02:23
  • 5
    You might want to note that this won't easily work if the function is a closure. – Andy Jul 13 '16 at 15:31
  • 2
    @AkashGupta use Function's constructor with return statement – Harry Aug 14 '17 at 13:54
  • 1
    The function constructor requires the arguments to be passed explicitly. Instead using eval(...) restores them from the toString representation and works with lambdas as well. Of course it might not be a bulletproof solution for a generic serializer/deserializer but it worked for me much better than the examples in the other answers. – Gianluca Romeo Oct 08 '20 at 16:50
  • This also works with es6 loving it. – Steve Moretz Jan 23 '22 at 21:40
33
function foo() {
  alert('native function');
  return 'Hello, serialised world!';
}

Serializing

var storedFunction = foo.toString();

Deserializing

var actualFunction = new Function('return ' + foo.toString())()

Explanation

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. :)

Harry
  • 1,572
  • 2
  • 17
  • 31
18

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.

Hashbrown
  • 12,091
  • 8
  • 72
  • 95
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/244123/discussion-on-answer-by-hashbrown-how-can-i-serialize-a-function-in-javascript). – Ryan M Apr 23 '22 at 02:09
11

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


Here's the demonstration

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];
};
Hashbrown
  • 12,091
  • 8
  • 72
  • 95
  • 1
    [I have made a much simpler version here](https://codepen.io/Hashbrown/pen/BPjLmy), but the tradeoff is that your function gets reconstructed for every call to the deserialised one. This *should* be negligible, so use this if you're not fond of the huge arguments parser above – Hashbrown Jul 15 '18 at 07:02
  • I am trying to serialize this ƒ unsubscribeOne(i) { if (this.observers === undefined || this.observers[i] === undefined) { return; } delete this.observers[i]; this.observerCount -= 1… It's a function that is returned from calling this method https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged . However, I get the unexpected identifier error – savram Apr 20 '22 at 16:50
  • @savram You are trying to serialise native browser code, that is, non-javascript, probably C++ code. [The unsub function is laundered through bind, you cannot get it.](https://i.imgur.com/811IU0G.png) Have a nice day – Hashbrown Apr 24 '22 at 09:42
-1

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.

user1514042
  • 1,899
  • 7
  • 31
  • 57
-2
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.

GYaN
  • 2,327
  • 4
  • 19
  • 39
  • 1
    While this code may answer the question, providing additional context regarding **how** and **why** it solves the problem would improve the answer's long-term value. – Alexander Mar 27 '18 at 03:00