如何"递归地"将调用其他作用域函数的JavaScript函数字符串化?

7
由于JavaScript函数不可序列化,为了有时(尽管很少)将它们传递到新上下文中,可以将其字符串化,然后稍后重新评估它们,例如:
const foo = () => { // do something }
const fooText = foo.toString()

// later... in new context & scope
const fooFunc = new Function(' return (' + fooText + ').apply(null, arguments)')
fooFunc() // works!

然而,如果foo引用另一个函数bar,那么作用域将不会被字符串化,因此如果在新的上下文中未定义bar,则调用时评估的foo函数会抛出错误。

我想知道是否有一种递归地字符串化函数的方法?

也就是说,不仅要字符串化父函数,还要字符串化从父函数调用的子函数的内容。

例如:

let bar = () => { alert(1) }
let foo = () => { bar() }

// what toString does
let fooString = foo.toString()
console.log(fooString) // "() => { bar() }"

// what we want
let recursiveFooString = foo.recursiveToString()
console.log(recursiveFooString) // "() => { alert(1) }"

如果你有任何想法可以完成类似于“recursiveToString”这样的操作,请告诉我。


1
进行递归 eval 调用非常低效,同时也相当困难(虽然不是不可能)。我对你需要这样做的原因很感兴趣。也许有另一种方法可以实现你更大的目标,而不需要序列化。 - nick zoum
@Teemu-callmewhateveryouwant 您可以使用解析器或AST转换器获取原始函数中使用的所有参数和函数。然后,您可以使用 eval 将这些函数转换为字符串(需要在相同的作用域)。 - nick zoum
@nickzoum 是的,但我说过你不能复制作用域本身(除非当然是像CertainPerformance的回答中那样复制作用域内的所有内容)。 - Teemu
@nickzoum 嗯...是的,虽然这样嵌套作用域会被压缩成一个单一的作用域,但实际上并没有任何区别。 - Teemu
2
@Teemu-callmewhateveryouwant 噢,是的,我已经要求 OP 回答为什么需要这种方法以及原始问题是什么,因为这听起来像是一个 XY 问题。 - nick zoum
显示剩余4条评论
3个回答

3

唯一的方法是从一个包含所有函数foo引用的父级作用域开始。例如,对于您的foobar,如果您想将foo传递到另一个上下文中,以便bar也可以被调用,请传递一个声明了foobar并返回foo的函数。例如:

const makeFoo = () => {
  let bar = () => { alert(1) }
  let foo = () => { bar() }
  return foo;
};
const makeFooStr = makeFoo.toString();

// ...

const makeFooFunc = new Function(' return (' + makeFooStr + ').apply(null, arguments)');
const foo = makeFooFunc();
foo();

实现这种功能需要像上面那样有预先设计(不幸的是)。当字符串化时,您不能真正包含所有祖先词法环境(给定作用域中变量名称到值的内部映射)。

谢谢您的回复!一个包装函数可以在一个作用域中定义所有被调用者,这肯定可以完成工作。不过我想知道是否有一种方法可以在不同的文件中定义函数(以实现模块化)的同时完成这个任务。寻找某种编程重新定义方法。 - Zachary Denham
我想你可以使用相同的模式来完成这个任务,只需从一开始就将“makeFoo”函数构建为字符串(使用父级作用域将被调用者转换为字符串)。 - Zachary Denham

1
我想知道是否有一种方法可以递归地将函数转化为字符串?
我认为我们可以很容易地证明这在一般情况下是不可能的。
让我们考虑这两个函数。
const greet = (greeting) => (name) => `${greeting} ${name}`
const sayHi = greet ('Hi') 

sayHi ('Jane') //=> "Hi Jane"

虽然以您的foobar为例,我们可能可以想象出一些内容,检查函数体并利用当前范围内可用的所有内容来执行基于解析函数并知道实际使用的本地变量的扩展字符串化函数。 (我猜这也是不可能的,原因与 Rice's Theorem相关,但我们当然可以想象一下。)

但是在这里,请注意

sayHi.toString() //=> "(name) => `${greeting} ${name}`"

sayHi 函数依赖于一个自由变量,该变量未存储在我们当前的作用域中,greeting。除了在 sayHi 的闭包作用域中之外,我们没有在 任何地方 存储用于创建该函数的“Hi”,也没有在任何地方公开它。

因此,即使是这个简单的函数也无法可靠地序列化;对于更复杂的内容似乎几乎没有希望。


1

我最终采用的方案受到了@CertainPerformance答案的启发。

关键是构建一个函数来定义所有子调用函数。然后你就拥有了一切你需要的东西来将父函数字符串化。

注意:为了允许从其他文件导入被调用函数,我决定以编程方式构建一个包含被调用函数定义的字符串,而不是最初在同一作用域中定义它们。

代码:

    // original function definitions (could be in another file)
    let bar = () => { alert(1) }
    let foo = () => { bar() }


    const allCallees = [ bar, foo ] 

    // build string of callee definitions
    const calleeDefinitions = allCallees.reduce(
      (definitionsString, callee) => {
        return `${definitionsString} \n const ${callee.name} = ${callee.toString()};`;
      }, 
      "",
    );

    // wrap the definitions in a function that calls foo
    const fooString = `() => { ${calleeDefinitions} \n return foo(); \n }`;

    console.log(fooString);
    /** 
     * fooString looks like this:
     * `() => {  
     *    const bar = () => { alert(1) }; 
     *    const foo = () => { bar() }; 
     *    return foo();
     *  }`
    **/ 
     

    // in new context & scope
    const evaluatedFoo = new Function(' return (' + fooString + ').apply(null, arguments)');

    // works as expected
    evaluatedFoo();


请注意,尽管我的回答中描述的问题并没有消失。但更重要的是你需要在编写代码时就考虑到这一点。既然如此,为什么不提前准备好序列化字符串呢?为什么要从你当前正在编写的代码中生成它呢? - Scott Sauyet
我可能会保留序列化(和压缩)的字符串,而不是在运行时动态生成它,但是假设我对许多被调用函数(存储在各种文件中)进行了更改,我将希望能够轻松地获取新修改的字符串,这就是我将使用此代码的原因。关于你的观点,我同意这不是一个通用解决方案,也不会考虑到你描述的边缘情况,我将不得不编写这些函数,使它们不依赖于自由变量。 - Zachary Denham

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