new Function 语法的妙用

基本语法

有一种很少使用但是有时候不得不用的创建函数的方法。

js
1let func = new Function([arg1, arg2, ...argN], functionBody)

这个函数是通过传递arg参数以及functionBody来创建的。

示例:

js
1let sum = new Function('a', 'b', 'return a + b')
2
3alert(sum(1, 2)) // 3

或者也可以不提供参数,只提供函数体:

js
1let sayHi = new Function('alert("Hello")')
2
3sayHi() // Hello

这种方法是在运行时通过参数传递过来的字符串来创建函数。

也就是说,我们能够将任意字符串变成函数而不用自己编写函数代码,所以我们可以拿服务器传过来的字符串来生成新的函数并执行它:

js
1let str = ...from service
2let func = new Function(str)
3func()

使用new Function可以从服务器获取代码或者动态地从模板编译成函数。

闭包

函数自身创建时,会有一个特殊的属性[[Environment]]来记录函数创建时的环境。也就是词法环境。

但是如果我们使用new Function创建函数,那么这个函数的[[Environment]]指向全局环境。 因此,此类函数并不能访问外部变量,只能访问全局变量。

js
1function getFunc() {
2  let value = 'test'
3
4  let func = new Function('alert(value)')
5
6  return func
7}
8
9getFunc()() // error: value is not defined

如果是常规的我们手动编写的函数,则可以访问外部变量

js
1function getFunc() {
2  let value = 'test'
3
4  let func = function () {
5    alert(value)
6  }
7
8  return func
9}
10
11getFunc()() // "test",从 getFunc 的词法环境中获取的

这种特性在实际工作中是有用的。

因为我们的 JavaScript 代码在发布到生产环境时,会被压缩程序(minifier)压缩,压缩程序主要做以下事情:

  1. 删除多余注释和空格等
  2. 局部变量重命名

因此,在这种情况下,如果new Function能够获取到局部变量,那么它有可能找不到对应的变量。

例如,函数有一个let userName的局部变量,压缩程序可能将其重命名为let a,而我们用new Function生成函数时不可能知道局部变量被压缩程序修改后的名字。

即使可以在new Function中访问外部词法环境,最终也会因为压缩程序而导致函数运行错误。

如果我们需要向new Function创建出新的函数并且传递数据时,我们必须显式通过参数进行传递。

小结

通过 new Function我们可以动态地从字符串生成函数

js
1let func = new Function([arg1, arg2, ...argN], functionBody)

由于历史原因,参数也可以按逗号分隔符的形式给出。

js
1new Function('a', 'b', 'return a + b') // 基础语法
2new Function('a,b', 'return a + b') // 逗号分隔
3new Function('a , b', 'return a + b') // 逗号和空格分隔

使用new Function生成的函数,它的[[Environment]]指向全局词法环境,而不是函数所在的外部词法环境。因为我们不能在new Function中直接使用外部变量,这有助于我们降低代码出错的可能,并且显式地用参数传值能够避免与压缩程序产生冲突。

简易代码执行器的实现

with语法虽然MDN不建议使用,但是在某些场景下,能发挥一些独到的作用。特别是与 new Function结合。

上面说到,new Function生成的函数需要显式传递参数。下面我们根据这个场景来做一个由用户在输入框中输入 JavaScript 语法的字符串,我们将字符串当成 JavaScript 代码执行。

功能是这样的:

tsx
1import React, { useState } from 'react'
2let data = { name: 'this is data' }
3export default () => {
4  const [text, setText] = useState('alert(data.name)')
5  function runJs(statement) {
6    try {
7      return new Function('data', statement)(data)
8    } catch (error) {
9      console.log(error.message)
10    }
11  }
12  return (
13    <div>
14      请在此输入代码:
15      <input value={text} onChange={(e) => setText(e.target.value)} />
16      <button onClick={() => runJs(text)}>点击我执行代码</button>
17    </div>
18  )
19}

点击按钮,我们能alert出data.name,说明在输入框内,我们已经能够引用 data 对象了。

现在更多的需求来了,假设我除了访问 data外,我还需要能访问到数据为{env:'dev'}的 environment对象,那么在 new Function语法中,我们不得不这样修改:

js
1return new Function('data', 'environment', statement).apply(null, [data, environment])

此时,在输入框中访问对应的数据是这样的:data.name,environment.env。

随着项目的功能增加,用户希望能使用越来越多的数据,那么我们不得不填充更多的对象数据进去,用户也需要通过obj.xxx这样的用访问对象属性方式去使用数据。

有没有办法让用户像使用变量一样直接引用 name、env这些对象内的属性呢?

结合with就可以做到。

下面的例子里,你可以直接访问到 name、env等,就像使用变量一样。

tsx
1import React, { useState } from 'react'
2let data = { name: 'this is data' }
3let environment = { env: 'dev' }
4export default () => {
5  const [text, setText] = useState('alert(`name:${name}`)')
6  function runJs(statement) {
7    try {
8      return new Function('data', `with(data){${statement}}`)({
9        ...data,
10        ...environment,
11      })
12    } catch (error) {
13      console.log(error.message)
14    }
15  }
16  return (
17    <div>
18      请在此输入代码:
19      <input value={text} onChange={(e) => setText(e.target.value)} />
20      <button onClick={() => runJs(text)}>点击我执行代码</button>
21    </div>
22  )
23}