likes
comments
collection
share

高级前端进阶!重学 new Function 语法前言 通过本文你能学到什么? new Function 的基础用法和常

作者站长头像
站长
· 阅读数 28

前言

通过本文你能学到什么?

高级前端进阶!重学 new Function 语法前言 通过本文你能学到什么? new Function 的基础用法和常

  1. new Function 的基础用法和常用demo
  2. 关于 new Function 解析过程anonymous 的结构
  3. new Functionarguments 与常规函数的一些不同之处
  4. new Function 的应用场景有哪些
  5. 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');

参数解释

  1. 'a':这是函数的第一个参数名。
  2. 'b':这是函数的第二个参数名。
  3. 'return a + b':这是函数的主体代码,也就是函数体。

在上面的例子中,Function 构造函数会创建一个函数,其形参为 ab,函数体为 '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');

这里:

  • ab 是函数参数名。
  • '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 引起不时行的检查。而是逐个解析。

  1. 先解析参数列表:如果你传递了一个无效的参数列表(例如非法的字符或注释符号),引擎会立即抛出 SyntaxError,而不会继续解析函数体。

  2. 再解析函数体:只有在参数列表解析成功之后,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.jsv-bindAngular 的模板表达式)中,开发者可能会通过字符串表达式绑定数据或属性。为了提高执行效率和灵活性,框架有时会通过 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 解析器

在数据查询场景中,像 JSONPathXPath这种解析器,用户的查询条件通常是通过字符串输入的。 解析器需要根据用户输入的查询条件动态执行查询操作。这类解析器中,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 Functioneval 类似,可以动态执行字符串代码,但相比于 evalnew 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
评论
请登录