web-dev-qa-db-ja.com

JavaScriptで関数をシリアル化するにはどうすればよいですか?

たとえば、次のように定義された関数があるとします。

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

その関数をシリアル化し、localStorageを使用して保存できるようにしたいと考えています。どうすればそれを実現できますか?

33
Akash Gupta

ほとんどのブラウザー(Chrome、Safari、Firefoxなど)は、.toString()メソッドから関数の定義を返します。

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

ネイティブ関数は適切にシリアライズされないので注意してください。例えば:

> alert.toString()
"function alert() { [native code] }"
26
David Wolever
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:関数

そのため、この関数を返す関数を作成し、変数に割り当てることができます。

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

したがって、この文字列をコンストラクターに渡すと関数が取得され、すぐに実行して元の関数に戻ります。 :)

7
Harry

既存の回答のかなり大きな欠陥に対処するためにこの回答を作成しました:.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
    ;
};

[〜#〜] demo [〜#〜] を見てください

それは最も簡単です:

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を使用しないように変更できますが、それが必要な場合はそうする必要があります。

2
Hashbrown

ES6で 矢印関数 をシリアル化する方法が必要な場合は、すべてを機能させるシリアライザーを作成しました。

必要なのは、関数または関数を含むオブジェクトに対して通常どおりJSON.stringify()を呼び出し、Function.deserialiseを呼び出すことだけです。 on 向こう側 魔法が働くために。

明らかに、クロージャーが機能することを期待すべきではありません。結局のところシリアライゼーションですが、デフォルト、デストラクタリング、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

そして最後に、魔法

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];
};
0
Hashbrown

JSONの欠点に少し悩まされて、シリアライズを正しく処理する小さなシリアライズ関数を作成しました:関数、null、未定義、NaN、および無限大。コンストラクターを再度呼び出す方法を考えることができなかったので、クラスインスタンスをシリアル化しないことだけです。

let serialize = function(input){
    const escape_sequences = {"\\\\": "\\\\", "`": "\\`", "\\\\b": "\\\\b", '"': '\\"', "\\n": "\\n", "\\\\f": "\\\\f", "\\r": "\\r", "\\\\t": "\\\\\\t", "\\\\v": "\\\\v"};
    if(typeof input === "string"){
        let result = input;
        for(var key in escape_sequences){
          result = result.replace(new RegExp(key, "g"), escape_sequences[key]);
        }
        return '`'+result+'`';
    }else if(typeof input === "number"){
        return input.toString();
    }else if(typeof input === "function"){
        // Handle build in functions
        if((/\{\s*\[native code\]\s*\}/).test('' + input)) return input.name;
        return input.toString().replace(/"/g, '\"');
    }else if(typeof input === "symbol"){
        return input.toString();
    }else if(input === null || input === undefined){
        return input;
    }else if(input instanceof Array){
        let res_list = [];
        for(let i = 0; i < input.length; i++){
            res_list.Push(serialize(input[i]));
        }
        return "["+res_list.join(",")+"]";
    }else if(input.constructor == Object){
        let res_list = [];
        for(let key in input){
            res_list.Push('"'+key.replace(/"/g, '\\"')+'":'+serialize(input[key]));
        }   
        return "{"+res_list.join(",")+"}";
    }else if(typeof input === "object"){
        throw(`You are trying to serialize an instance of `+input.constructor.name+`, we don't serialize class instances for a bunch of reasons.`)
    }else{
        return input;
    }
}

let unserialize = function(input){
    return Function(`
        "use strict";
        return `+input+`;`
    )();
}

それをテストしましょう!

let input = {
    'a': "str normal",
    'b"': 'str "quote"',
    'c': 1,
    'd': -1.3,
    'e': NaN,
    'f': -Infinity,
    'g': ()=>123,
    'h': function(){return "lalala"},
    'i': null,
    'j': undefined,
    'k': true,
    'l': Symbol(123),
    'm': [1,2,3],
    'n': [{"a": "str normal",'b"': 'str "quote"','c': 1,'d': -1.3,'e': NaN,'f': -Infinity,'g': ()=>123,'h': function(){return "lalala"},'i': null,'j': undefined,'k': true,'l': Symbol(123),'m': [1,2,3],}],
};

let output = unserialize(serialize(input));

for(let key in input){
    console.log(input[key], output[key]);
}
0
Stuffe

呼び出しをシリアル化しないでください。代わりに、情報をシリアル化してみてください。呼び出しを繰り返すことができます。これには、クラス名、メソッド名、呼び出しに渡される引数、または呼び出しシナリオ名などを含めることができます。

0
user1514042