如何在JavaScript中序列化一个函数?

61

例如,假设我有一个定义如下的函数:

function foo() {
  return "Hello, serialized world!";
}

我想要将那个函数序列化并使用 localStorage 存储。我该如何实现?


7
为什么要将函数序列化? - Daniel A. White
7
如果被序列化的函数引用了包含作用域中的变量/函数,如果你在错误的位置对其进行反序列化,它将无法正常工作... - nnnnnn
6
我不能评论Akash的用法,但我想将一个函数序列化以将其存储为CouchDB中的验证函数。传入的变量和受限范围都有明确定义。 - ReactiveRaven
57
当这是对某人问题的第一反应时,这确实让我感到恼火! - Michael
21
人们在StackOverflow上经常会忽略他们认为是不好的问题,而不是客观地回答问题,然后再加上一些建议。后者对初学者更有帮助。 - Andy
显示剩余4条评论
6个回答

42

大多数浏览器(Chrome,Safari,Firefox,可能还有其他浏览器)从.toString()方法返回函数的定义:

> function foo() { return 42; }
> foo.toString()
"function foo() { return 42; }"

请注意,原生函数不会正确序列化。例如:

> alert.toString()
"function alert() { [native code] }"

2
你如何从字符串中获取函数? - Akash Gupta
15
@David Wolever - ES3和ES5规范指出,Function.prototype.toString返回函数的“实现相关表示”,该表示“具有FunctionDeclaration的语法”(§15.3.4.2)。可能值得注意的是,并没有说明它必须是函数的文字代码,它也可能只是function foo(){/* returns 42 */} - RobG
5
请注意,如果该函数是一个闭包,这将不容易实现。 - Andy
2
@AkashGupta 使用带有返回语句的函数构造器。 - Harry
1
函数构造器要求显式传递参数。而使用 eval(...) 从 toString 表示中恢复它们并且也适用于 lambda。 当然,这可能不是一个通用的序列化/反序列化解决方案,但它对我来说比其他答案中的示例效果更好。 - Gianluca Romeo
这也适用于es6,喜欢它。 - Steve Moretz

36
function foo() {
  alert('native function');
  return 'Hello, serialised world!';
}

序列化

var storedFunction = foo.toString();

反序列化

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

解释

foo.toString() 将返回函数 foo 的字符串形式。

"function foo() { ... return 'Hello, serialised world!';}"

但是 new Function 接受的是一个函数体而不是函数本身。

参见MDN: Function

因此,我们可以创建一个返回此函数的函数并将其分配给某个变量。

"return function foo() { ... return 'Hello, serialised world!';}"

所以现在当我们将这个字符串传递给构造函数时,我们得到一个函数,然后立即执行它以获取我们的原始函数。 :)


19
我制作了这个答案来解决现有答案中存在的一些严重缺陷:.toString() / eval()new Function()本身都无法正常工作,如果您的函数使用this或命名参数(function(named,arg){} ),则更是如此。使用下面的toJSON()您只需要像往常一样调用JSON.stringify()即可在函数上,并在parse()ing时使用Function.deserialise。以下内容不适用于简明函数(hello => 'there'),但对于标准的ES5 fat函数,它将按照定义返回它,当然闭包除外。我的其他答案将使用所有的ES6好处
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
    ;
};

请查看演示

最简单的情况是:

var test = function(where) { return 'hello ' + where; };
test = JSON.parse(JSON.stringify(test), Function.deserialise);
console.log(test('there'));
//prints 'hello there'

更有用的是,您可以序列化包含函数的整个对象并将其取回:

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));

输出:

{"a":2,"run":["window.Function",["x","y","z"]," return this.a + x + y + z; "]}
string
14

我没有测试旧版本的IE,但它在IE11、FF、Chrome和Edge上都能工作。

注意,如果您使用该属性,则函数的name将丢失,如果您需要使用该属性,则无法做任何事情。
您可以轻松地将其更改为不使用prototype,但如果您需要,请自行处理。


评论不适合进行长时间的讨论;此对话已被移至聊天室 - Ryan M

12
如果你需要在ES6中序列化箭头函数,我编写了一个序列化程序,可以使一切正常运行。
只需像往常一样调用JSON.stringify()来处理包含该函数的函数或对象,并在另一端调用Function.deserialise即可实现魔法。
显然,你不应该期望闭包能够工作,毕竟这是序列化,但默认值、解构、thisargumentsclass成员函数,所有这些都将被保留。
如果你只使用ES5标记,请使用我的其他答案。这个真的是超越了预期。

这是演示

在Chrome/Firefox/Edge中工作。
以下是演示的输出:一些函数,序列化字符串,然后调用反序列化后创建的新函数。

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

最后,神奇的{{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];
};

1
我已经在这里制作了一个简化版本(https://codepen.io/Hashbrown/pen/BPjLmy),但是其中的权衡是每次调用反序列化函数时都会重建您的函数。这个影响*应该*可以忽略不计,所以如果您不喜欢上面描述的庞大参数解析器,请使用这种方法。 - Hashbrown
我正在尝试序列化这个ƒ unsubscribeOne(i) { if (this.observers === undefined || this.observers[i] === undefined) { return; } delete this.observers[i]; this.observerCount -= 1… 这是从调用此方法https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged 返回的函数。但是,我遇到了意外的标识符错误。 - savram
@savram,您正在尝试序列化本机浏览器代码,即非JavaScript,可能是C++代码。unsub函数通过bind进行了清洗,您无法获取它。祝您拥有愉快的一天。 - Hashbrown

-1
不要序列化调用,而是尝试序列化信息,允许重复调用,其中可以包括类和方法名称、传递给调用的参数或仅调用场景名称。

-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; 
}"

这与 w(3) 不同,后者仍然记得要加 3。


1
虽然这段代码可能回答了问题,但提供有关它如何以及为什么解决问题的额外上下文会提高答案的长期价值。 - Alexander

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接