高级前端进阶!重学 new Function 语法前言 通过本文你能学到什么? new Function 的基础用法和常
前言
通过本文你能学到什么?
new Function
的基础用法和常用demo
- 关于
new Function
解析过程和anonymous
的结构 new Function
中arguments
与常规函数的一些不同之处new Function
的应用场景有哪些new Function
不太合适的场景和弊端注意点
用法
基础语法
创建函数语法
let func = new Function ([arg1, arg2, ...argN], functionBody);
函数是通过使用参数 arg1...argN
和给定的 functionBody
创建。
调用
Function
时可以使用或不使用new
,两者都会创建一个新的Function
实例。
举例 1:带有两个参数的函数:
let sum = new Function('a', 'b', 'return a + b');
alert( sum(1, 2) ); // 3
举例 2:一个没有参数的函数,自由函数体
let sayHi = new Function('alert("芝士")');
sayHi(); // Hello
举例 3:没有参数的函数,new Function
被调用后,可以在事后自己调用我们创建的函数了
const sumOfArray = new Function(
"const sumArray = (arr) => arr.reduce((previousValue, currentValue) => previousValue + currentValue); return sumArray",
)();
// 调用函数
sumOfArray([1, 2, 3, 4]);
// 10
-
new Function
实际上返回的是一个匿名函数(就是你传入的函数体)。 -
这个匿名函数执行后,返回的是定义在其中的
sumArray
函数 -
所以
sumOfArray
实际上是sumArray
,当你调用sumOfArray([1, 2, 3, 4])
时,相当于在调用sumArray([1, 2, 3, 4])
。
anonymous
使用 new Function
后,传递的参数和函数体会被动态编译(下文会详细讲)为一个函数表达式,其编译后的代码组装方式如下:
`function anonymous(${args.join(",")}
) {
${functionBody}
}`;
与普通的函数表达式不同,anonymous
这个名字不会被添加到 functionBody
的作用域中,因为 functionBody
只能访问全局作用域。如果 functionBody
不在严格模式中(主体本身需要有 "use strict"
指令,因为它不从上下文中继承严格性),你可以使用 arguments.callee
来指代函数本身。另外,你可以将递归部分定义为一个内部函数:
const recursiveFn = new Function(
"count",
`
(function recursiveFn(count) {
if (count < 0) {
return;
}
console.log(count);
recursiveFn(count - 1);
})(count);
`,
);
anonymous
的这种编译后的组装格式,可以通过 toString()
函数查看。举个例子
const queryCondition = "return obj.age > 30 && obj.country === 'USA';";
const queryFunc = new Function('obj', queryCondition);
console.log(queryFunc.toString());
const result = data.filter(queryFunc);
queryFunc.toString()
的输出内容如下:
function anonymous(obj) { return obj.age > 30 && obj.country === 'USA'; }
最终的有效执行代码如下:
const result = data.filter(
function anonymous(obj) {
return obj.age > 30 && obj.country === 'USA';
}
);
参数传递
由于历史原因,new Function
中的参数也可以按逗号分隔符的形式给出。
以下三种声明的含义相同:
new Function('a', 'b', 'return a + b'); // 基础语法
new Function('a,b', 'return a + b'); // 逗号分隔
new Function('a , b', 'return a + b'); // 逗号和空格分隔
闭包
通常,闭包是指使用一个特殊的属性 [[Environment]]
来记录函数自身的创建时的环境的函数。它具体指向了函数创建时的词法环境。。
但是如果我们使用 new Function
创建一个函数,那么该函数的 [[Environment]]
并不指向当前的词法环境,而是指向全局环境,具有访问本地作用域的 eval
不同,Function
构造函数创建的函数仅在全局作用域中执行。
因此,此类函数无法访问外部(outer
)变量,只能访问全局变量。
举个例子:
function getFunc() {
let value = "芝士";
let func = new Function('alert(value)');
return func;
}
getFunc()(); // error: value is not defined
将其与常规行为进行比较:
function getFunc() {
let value = "芝士";
let func = function() { alert(value); };
return func;
}
getFunc()(); // "芝士" ,从 getFunc 的词法环境中获取的
构造函数中不一样的参数列表
常被忽视的一个知识点,可能会造成错误!
在 JavaScript
中,Function
构造函数确实是一个特殊的情况,其中的最后一个参数是函数体(实际执行的代码),前面所有参数(除了最后一个)都是函数的参数名称。
例如:
let func = new Function('a', 'b', 'return a + b');
参数解释
'a'
:这是函数的第一个参数名。'b'
:这是函数的第二个参数名。'return a + b'
:这是函数的主体代码,也就是函数体。
在上面的例子中,Function
构造函数会创建一个函数,其形参为 a
和 b
,函数体为 'return a + b'
。
arguments
普通函数的 arguments
行为
对于普通的 JavaScript
函数,arguments[0]
始终表示第一个传入的参数,arguments[1]
表示第二个参数,以此类推。参数的顺序与传入顺序一致,没有 "反着来" 的情况。
例子:
function example(a, b, c) {
console.log(arguments[0]); // 输出 a
console.log(arguments[1]); // 输出 b
console.log(arguments[2]); // 输出 c
}
example(10, 20, 30);
输出:
10
20
30
这里,arguments[0]
对应的是第一个传递的参数 10
,而不是函数的最后一个参数。
Function 中的 arguments
行为
当使用 Function
构造函数时,最后一个参数始终被认为是要执行的代码(即函数体)。
例如:
let sum = new Function('a', 'b', 'return a + b');
这里:
a
和b
是函数参数名。'return a + b'
是函数体。arguments[0]
就是'return a + b'
重写Function函数例子
比如我想判断 new Function
执行一段 js
脚本时,代码中包含 debugger
就不执行提示,就可以利用 argument[0]
实现
let _Function = Function;
Function.prototype.constructor = function() {
if (arguments[0].indexOf('debugger') != -1) {
return _Function('');
}
return _Function(arguments[0]);
};
new Function 函数体解析过程
MDN
的中的描述 集合源的两个动态部分——参数列表args.join(",")
和functionBody
将首先被分别解析,以确保它们在语法上都是有效的。这可以防止类似注入的尝试。
不太容易理解,我把它翻译成人话。
当大家使用 Function
构造函数时,JavaScript
引擎会对参数列表和函数体解析。注意 JavaScript
引起不时行的检查。而是逐个解析。
-
先解析参数列表:如果你传递了一个无效的参数列表(例如非法的字符或注释符号),引擎会立即抛出
SyntaxError
,而不会继续解析函数体。 -
再解析函数体:只有在参数列表解析成功之后,
JavaScript
引擎才会继续解析函数体部分。如果函数体包含无效的JavaScript
代码,同样会抛出SyntaxError
。
举个例子
new Function("/*", "return 1;");
例如,传入 /*
作为参数名是非法的,原因是它是注释的开始符号,而参数名必须是有效的 JavaScript
标识符。
这会直接抛出 SyntaxError: Unexpected token '/*'
,因为 /*
不是有效的标识符。
传入的参数无效,整个解析过程第一步就失败了,不会继续检查函数体。
如果参数列表是合法的,才会继续解析函数体。例如:
new Function("a", "return a + 1;"); // 合法,成功创建函数
再回到 MDN
中的这句话
"这可以防止类似注入的尝试。"
基于上面 JavaScript
引擎解析的逻辑,这种解析方式确保了逐步验证的过程,可以防止通过传递非法参数或函数体的方式进行代码注入攻击。比如,不允许通过参数注入注释符号 /*
来破坏整个函数的结构。也就在一定程度上防止代码注入的逻辑。
应用场景
new Function
应用场景其实还蛮多的,主要涉及的方面是动态代码执行、沙箱环境以及性能优化等。简单举几个例子:
1. 动态执行代码
在一些低代码或无代码平台中,用户可能会输入自定义的 JavaScript
代码,平台需要即时执行这些代码。通过 new Function
,可以将用户输入的字符串直接解析并执行。例如:
const userInput = "return a + b;";
const dynamicFunction = new Function('a', 'b', userInput);
console.log(dynamicFunction(2, 3)); // 输出 5
这种场景常见于表单计算器、在线编程环境或数据分析工具中,允许用户定义自定义的计算公式或规则。
2. 模板引擎中的表达式解析
在一些模板引擎(如 Vue.js
的 v-bind
、Angular
的模板表达式)中,开发者可能会通过字符串表达式绑定数据或属性。为了提高执行效率和灵活性,框架有时会通过 new Function
来生成一个能够动态计算表达式的函数。例如:
const expression = "data.a + data.b";
const compute = new Function('data', `return ${expression}`);
console.log(compute({a: 1, b: 2})); // 输出 3
通过 new Function
动态编译模板表达式,可以避免每次都对表达式重新进行解析和计算,提高性能。
3. JSONPath 或 XPath 解析器
在数据查询场景中,像 JSONPath
或 XPath
这种解析器,用户的查询条件通常是通过字符串输入的。 解析器需要根据用户输入的查询条件动态执行查询操作。这类解析器中,new Function
有时用于生成查询逻辑的动态函数,以便在大规模数据上进行快速计算和过滤。
// 用户输入的查询条件
const queryCondition = "return obj.age > 30 && obj.country === 'USA';";
// 动态生成一个查询函数
const queryFunc = new Function('obj', queryCondition);
// 用于查询的JSON数据
const data = [
{ name: 'Alice', age: 35, country: 'USA' },
{ name: 'Bob', age: 28, country: 'UK' },
{ name: 'Charlie', age: 40, country: 'USA' }
];
// 动态过滤数据
const result = data.filter(queryFunc);
console.log(result); // [{ name: 'Alice', age: 35, country: 'USA' }, { name: 'Charlie', age: 40, country: 'USA' }]
4. 性能优化:避免 eval
new Function
与 eval
类似,可以动态执行字符串代码,但相比于 eval
,new Function
的作用域更为严格。它只允许访问全局作用域,不能直接访问当前上下文的局部变量。因此,new Function
在某些需要动态执行代码的场景下比 eval
更安全,也更快,常用于替代 eval
。
5. 表单验证或计算
在复杂的动态表单中,可能需要根据用户的输入动态生成验证规则或者计算结果。new Function
可以根据用户定义的规则生成函数,实时验证表单字段或计算结果:
const rule = "return value > 10;";
const validate = new Function('value', rule);
console.log(validate(15)); // 输出 true
6. JavaScript 沙箱实现
某些框架需要隔离用户输入的代码与主应用逻辑,创建安全的沙箱环境。通过 new Function
,可以限制代码的作用域,防止访问不安全的全局变量。这种方式在在线 IDE
或某些插件系统中较为常见。 沙箱的方式有很多种(比如eval,ast
解析实现等等),new Function
是其中比较简单的一种方式,这里就不细展开说了
new Function 不太合适的场景和弊端注意点
在将 JavaScript
发布到生产环境之前,需要使用 压缩程序(minifier
) 对其进行压缩
-
压缩程序(minifier)工作原理:
- 压缩程序用于优化
JavaScript
代码,将变量名压缩成更短的名称,并删除注释、空格等无关内容。这样做能减少代码体积,提升性能。 - 压缩时,局部变量(如
let userName
)会被替换为较短的变量名(如let a
),所有引用该变量的地方也会同步替换。
- 压缩程序用于优化
-
new Function
访问外部变量的限制:- 使用
new Function
创建的函数不会像普通函数那样能够访问外部的词法作用域(即闭包环境)。它只能访问传递给它的参数和全局变量。 - 在
new Function
中,即使我们尝试引用外部定义的变量(如userName
),由于新函数的词法作用域与外部环境隔离,它是无法直接访问这些变量的。
- 使用
-
压缩程序的影响:
-
假设代码在开发时有一个变量
let userName
,而你在new Function
中尝试访问它。压缩后,userName
可能会变成a
。但new Function
在代码压缩后才被执行,它不会知道压缩后变量名的变化,因此会导致访问出错。 -
例如,如果你的代码中有:
let userName = "John"; const func = new Function("return userName;");
压缩后,
userName
可能被替换为a
,但是new Function
生成的函数依然试图访问userName
,结果会报错,因为压缩程序不会知道new Function
中定义的内容。
-
-
正确方式:显式传递数据:
-
因为
new Function
无法访问外部变量,并且会受到压缩影响,推荐的做法是显式通过函数参数传递需要使用的数据,而不是依赖外部变量。 -
例如:
let userName = "John"; const func = new Function('name', 'return name;'); console.log(func(userName)); // 这样可以正常工作
-
-
架构上不推荐:
- 从架构角度看,依赖
new Function
访问外部变量是错误的做法,因为这不仅使代码在压缩后出错,也会导致代码难以维护。通过显式传递参数的方式更加安全、可控。
- 从架构角度看,依赖
不合适场景总结下:由于 new Function
不能访问外部变量,只能访问传递给它的参数和全局遍历。代码压缩后,局部变量的名称可能会被替换,导致 new Function
中尝试访问外部变量的代码出错,为了避免这些问题,如需使用参数应该通过参数传递数据给 new Function
。
总结
相信学习完本文你对 new Function
有一个彻底的掌握,创作不易欢迎三连支持。
转载自:https://juejin.cn/post/7417843661466042407