new Function 高级总结

307 阅读5分钟
  • 定义:new Function都可以将一段字符串解析成一段JS脚本并执行

  • 语法:new Function(functionBody) or new Function(arg1, arg2, /* …, */ argN, functionBody)

  • 注意:作用域问题,作用域为全局,不能访问当前上下文 【the Function constructor creates functions which execute in the global scope only】

         let foo = "foo";
         function bar() {
              let foo = "bar";
              eval("console.log(foo)"); // 输出bar
              new Function("console.log(foo)")(); // 输出foo,当前上下文foo = "bar"访问不到
         }
         bar();
    
  • 用法梳理:

    • 1、我们使用 new Function('return window')() 或者 Function('return window')() 返回全局的 widow 上下文对下,这在微前端中有一定的使用场景,通过这种方法实现在子应用破局获取到全局顶层的上下文
    • 2、我们同样可以使用 new Function('return (params) => params.a')() 或者 new Function('return function exc(params){return params.a}')() 返回一个函数,去动态执行
    • 3、new Function 拼接字符串: new Function(return + custom)【注意,return 后面有个空格】
       // 1、例一
       const x = new Function(`params`, `return params?.a`);
       x({ a: 222 }); // 222
    
       // 2、例二
       const params = { a: 222 };
       const x = new Function(`return ` + params?.a);
       x(); // 222
    
       // 3、例三
       var name = (params) => {
       return params?.a;
       };
       const x = new Function(`return` + name);
       x()({ a: 222 }); // 222
    
    • 几种传参方式:
       // 1、传统的入参方式单个元素:
       var sum = new Function("a", "b", "return a + b");
       console.log(sum(2, 6));
    
       // 2、传统的入参方式对象:
       var sum = new Function("params", "return params.a + params.b");
       console.log(sum({ a: 2, b: 6 }));
    
       // 3、将顶层变量直接定义好传入:
       var sum = new Function("var a=100; return a + 100");
       console.log(sum());
    
       // 4、使用call的方式调起
       // @1:
       var sum = new Function('return this.a + 100').call({a:10})
       console.log(sum);
       // @2:
       const findNum = new Function(
            "function findLargestNumber (arr) { return Math.max(...arr) }; return findLargestNumber",
       );
       console.log(findNum.call({}).call({}, [2, 4, 1, 8, 5]));
    
  • 需要注意的事:

    • 1、我们尽量使用显示传递参数【代码在执行压缩的过程中,全局的变量名会被替换,而 new Function 中的函数体可能是字符串,不会被同步替换,这样压缩后就有出错的风险】, 这时我们需要使用显式传递数据方式来解决
       // 1、源码
       const userName = "Tom";
       const func = new Function("return userName");
    
       // 2、被压缩后
       const a = "Tom";
       const func = new Function("return userName"); //这个地方没被替换
    
       // 3、分析:上面的代码,在压缩条件下,执行时就可能报错,userName没被替换,全局已经不存在了
    
       // 4、如何破解
       // 显式传递数据: 因为 new Function 无法访问外部变量,并且会受到压缩影响,推荐的做法是显式通过函数参数传递需要使用的数据,而不是依赖外部变量
       // 所以减少直接使用,采用显式传递数据更安全更合理
       const userName = "Tom";
       const func = new Function("name", "return name");
       console.log(func(userName)); //这样可以正常工作
    
  • 使用场景:

    • 1、低代码和泛低代码工程

      • 低代码本质上,是将用户配置的字符串配置,给予转换执行,最终渲染出组件;new Function 在执行字符串方面有天然无与伦比的能力,那么可想而知,new Function 在低代码领域必然有很强的使用性;当然对于需要执行字符串的泛应用场景肯定也是使用满满。下面截取几个知名的低代码工程中的 new Function使用:
       /**
        * lowcode-engine use demo
        **/
      function checkPropTypes(
        value: any,
        name: string,
        rule: any,
        componentName: string
      ): boolean {
        let ruleFunction = rule;
        if (typeof rule === "object") {
          // eslint-disable-next-line no-new-func
          ruleFunction = new Function(
            `"use strict"; const PropTypes = arguments[0]; return ${transformPropTypesRuleToString(
              rule
            )}`
          )(PropTypes2);
        }
        if (typeof rule === "string") {
          // eslint-disable-next-line no-new-func
          ruleFunction = new Function(
            `"use strict"; const PropTypes = arguments[0]; return ${transformPropTypesRuleToString(
              rule
            )}`
          )(PropTypes2);
        }
        if (!ruleFunction || typeof ruleFunction !== "function") {
          logger.warn("checkPropTypes should have a function type rule argument");
          return true;
        }
        const err = ruleFunction(
          {
            [name]: value,
          },
          name,
          componentName,
          "prop",
          null,
          ReactPropTypesSecret
        );
        if (err) {
          logger.warn(err);
        }
        return !err;
      }
      
       /**
        * appsmith use demo
        **/
       function parseConfigurationForCallbackFns(
            chartConfig: Record<string, unknown>,
            _: any,
       ) {
            const config: Record<string, unknown> = _.cloneDeep(chartConfig);
            const fnKeys = (config["__fn_keys__"] as string[]) ?? [];
            for (let i = 0; i < fnKeys.length; i++) {
                 const fnString = _.get(config, fnKeys[i]);
                 const fn = new Function("return " + fnString)();
                 _.set(config, fnKeys[i], fn);
            }
      
            return config;
       }
      
    • 微前端沙箱环境

      • 使用new Function创建沙箱主要是为了隔离变量,防止全局污染
           // 基础型,单new Function型的
           // 创建一个沙箱
           function createSandbox() {
                var sandbox = {
                     run: new Function('print', 'code', 'return eval(code)')
                };
                sandbox.run(sandbox.print = text => console.log(text), 'print("Hello, Sandbox!")');
                return sandbox;
           }
           
           // 使用沙箱
           var sandbox = createSandbox();
      
           // 配合with升级沙箱作用域限定
           const ctx = {
                test(flag){
                     console.log(flag);
                }
           };
           function sandbox(code) {
                const fnbody = "with (ctx) {" + code + "}";
                return new Function("ctx", fnbody);
           }
           const code = `
                const name = 'zhangsan';
                test(name)
           `;
           sandbox(code)(ctx);
      
           // 限定请求作用域为ctx,全局变量有被篡改的风险
      
           // 严格模式的沙箱实现: with + new Function + proxy实现
           // 核心思路是通过 with 块和 Proxy 对象来隔离执行环境,确保执行的代码只能访问到沙盒内的变量。任何在沙盒内声明或者修改的变量都不会影响到全局作用域,同时,全局作用域下的变量在沙盒内也是不可见的
           // 创建一个沙盒对象,这个对象里面的属性和全局作用域不同步,避免沙盒内代码影响外部环境
           const sandboxProxy = new Proxy({}, {
                has: function() {
                     // 拦截属性检查,总是返回 false,迫使 with 块中的查找进入沙盒对象
                     return true;
                },
                get: function(target, key) {
                     if (key === Symbol.unscopables) return undefined;
                     // 返回沙盒对象中的属性,如果不存在则返回 undefined
                     return target[key];
                },
                set: function(target, key, value) {
                     // 设置属性值,影响只限于沙盒内部
                     target[key] = value;
                     return true;
                }
           });
           // 定义执行沙盒代码的函数
           function executeSandboxCode(code) {
                /* 
                     // 通过 new Function 创建一个新的函数,这样保证了函数体内的代码运行在全局作用域之外
                     const sandboxFunction = new Function('sandbox', `with(sandbox) { ${code} }`);
                     // 调用这个新创建的函数,传入沙盒代理对象
                     sandboxFunction(sandboxProxy); 
                */
                // 避免绕过沙盒,通过改变 this 指向的代码示例
                const sandboxFunction = new Function('sandbox', 'with(sandbox) { return function() { "use strict"; ' + code + ' } }');
                sandboxFunction(sandboxProxy).call(null);
           }
      
           // 使用
           executeSandboxCode(`
                // 这些代码运行在沙盒环境中,外部变量对其不可见
                var secret = '我是沙盒中的秘密';
                console.log(secret); // 输出: '我是沙盒中的秘密'
           `);
      
           /**
            * lowcode-engine use demo
            **/
           function parseExpression(a: any, b?: any, c = false) {
                let str;
                let self;
                let thisRequired;
                let logScope;
                if (typeof a === 'object' && b === undefined) {
                     str = a.str;
                     self = a.self;
                     thisRequired = a.thisRequired;
                     logScope = a.logScope;
                } else {
                     str = a;
                     self = b;
                     thisRequired = c;
                }
                try {
                     const contextArr = ['"use strict";', 'var __self = arguments[0];'];
                     contextArr.push('return ');
                     let tarStr: string;
      
                     tarStr = (str.value || '').trim();
      
                     // NOTE: use __self replace 'this' in the original function str
                     // may be wrong in extreme case which contains '__self' already
                     tarStr = tarStr.replace(/this(\W|$)/g, (_a: any, b: any) => `__self${b}`);
                     tarStr = contextArr.join('\n') + tarStr;
      
                     // 默认调用顶层窗口的parseObj, 保障new Function的window对象是顶层的window对象
                     if (inSameDomain() && (window.parent as any).__newFunc) {
                          return (window.parent as any).__newFunc(tarStr)(self);
                     }
                     const code = `with(${thisRequired ? '{}' : '$scope || {}'}) { ${tarStr} }`;
                     return new Function('$scope', code)(self);
                } catch (err) {
                     logger.error(`${logScope || ''} parseExpression.error`, err, str, self?.__self ?? self);
                     return undefined;
                }
           }
      
  • 参考: