new Function 语法的妙用
基本语法
有一种很少使用但是有时候不得不用的创建函数的方法。
js1let func = new Function([arg1, arg2, ...argN], functionBody)
这个函数是通过传递arg参数以及functionBody来创建的。
示例:
js1let sum = new Function('a', 'b', 'return a + b') 2 3alert(sum(1, 2)) // 3
或者也可以不提供参数,只提供函数体:
js1let sayHi = new Function('alert("Hello")') 2 3sayHi() // Hello
这种方法是在运行时通过参数传递过来的字符串来创建函数。
也就是说,我们能够将任意字符串变成函数而不用自己编写函数代码,所以我们可以拿服务器传过来的字符串来生成新的函数并执行它:
js1let str = ...from service 2let func = new Function(str) 3func()
使用new Function可以从服务器获取代码或者动态地从模板编译成函数。
闭包
函数自身创建时,会有一个特殊的属性[[Environment]]来记录函数创建时的环境。也就是词法环境。
但是如果我们使用new Function创建函数,那么这个函数的[[Environment]]指向全局环境。
因此,此类函数并不能访问外部变量,只能访问全局变量。
js1function 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
如果是常规的我们手动编写的函数,则可以访问外部变量
js1function 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)压缩,压缩程序主要做以下事情:
- 删除多余注释和空格等
- 局部变量重命名
因此,在这种情况下,如果new Function能够获取到局部变量,那么它有可能找不到对应的变量。
例如,函数有一个let userName的局部变量,压缩程序可能将其重命名为let a,而我们用new Function生成函数时不可能知道局部变量被压缩程序修改后的名字。
即使可以在new Function中访问外部词法环境,最终也会因为压缩程序而导致函数运行错误。
如果我们需要向new Function创建出新的函数并且传递数据时,我们必须显式通过参数进行传递。
小结
通过 new Function我们可以动态地从字符串生成函数
js1let func = new Function([arg1, arg2, ...argN], functionBody)
由于历史原因,参数也可以按逗号分隔符的形式给出。
js1new 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 代码执行。
功能是这样的:
tsx1import 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语法中,我们不得不这样修改:
js1return new Function('data', 'environment', statement).apply(null, [data, environment])
此时,在输入框中访问对应的数据是这样的:data.name,environment.env。
随着项目的功能增加,用户希望能使用越来越多的数据,那么我们不得不填充更多的对象数据进去,用户也需要通过obj.xxx这样的用访问对象属性方式去使用数据。
有没有办法让用户像使用变量一样直接引用 name、env这些对象内的属性呢?
结合with就可以做到。
下面的例子里,你可以直接访问到 name、env等,就像使用变量一样。
tsx1import 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}