神奇的eval和new Function

725 阅读1分钟

相信看过MDN网站的同学肯定都见过这样的一个功能: gh 通过点击run即可以看到代码框内的输出结果,是不是很神奇,这种功能要如何实现的呢? 要回答这个问题就不得不提到今天的主角evalnew Function


eval

存在的问题

eval相信很多同学第一时间想到eval不安全、不推荐使用,确实它有着诸多问题:

  • 有可能会发生xss攻击,因为==输入的字符串直==接被当成代码解析这就非常危险了 gh

  • 性能较差,相比普通函数执行性能较差

let len = 100000;
function testFn() {
  console.time('testFn');
  const res = [];
  for (let i = 0; i < len; i++) {
    const temp = i * 2;
    res.push(temp);
  }
  console.timeEnd('testFn');
}

function testEval() {
  console.time('testEval');
  const res = [];
  for (let i = 0; i < len; i++) {
    const temp = eval('i * 2');
    res.push(temp);
  }
  console.timeEnd('testEval');
}

testFn();
testEval();
// testFn: 2.828ms
// testEval: 15.17ms
  • 调试困难,需要在代码字符串中使用debbuger

注意事项

  • 通过window==间接调用==和==直接调用==的作用域链会有所不同,window.eval只包含全局作用域,eval包含所在上下文的作用域链

直接调用eval

<body>
  <script>
    function foo() {
      var foo = 'foo';
      function bar() {
        var bar = 'bar';
        eval('debugger;var text = "eval run";console.log(text);')
      }
      bar();
    }
    foo();
  </script>
</body>

可以看到scope chain(作用域链)上有所在上下文的作用域链即使没引用外层函数的变量 gh

通过window调用eval 对代码稍微改动

<body>
  <script>
    function foo() {
      var foo = 'foo';
      function bar() {
        var bar = 'bar';
        window.eval('debugger;var text = "eval run";console.log(text);')
      }
      bar();
    }
    foo();
  </script>
</body>

可以看到scope chain(作用域链)上只有global了 gh

应用场景

  • 运算,我们可以通过给eval传递字符串表达式进行运算
// 通过eval运算
function sum() {
  return eval('1 + 2 + 3 + 4 + 5');
}
console.log(sum()); // 15
  • 通过eval也可以生成不定型参的函数
/**
 * 生成不定参数的函数
 * @param {*} len 型参的长度
 */
function createSumFn(len) {
  let code = 'return ';
  for (let i = 0; i < len; i++) {
    code += i === len - 1 ?  'args[' + i + ']' : 'args[' + i + '] + ';
  }
  code += ';';
  return new Function('args', code);
}

console.log(createSumFn(2)([1, 2])); // 3
  • 通过eval将json字符串转为json对象
/**
 * 实现json.parse
 * @param {string} json 
 * @returns 
 */
function parse(json) {
  return eval('(' + json + ')');
}

console.log(parse(JSON.stringify({'name': 'chen', age: 23}))); // { name: 'chen', age: 23 }
console.log(typeof parse(JSON.stringify({'name': 'chen', age: 23}))); // object
  • 快速获得全局对象(浏览器是window,node环境是global),严格模式下失效
// 获取全局对象
var globalThis = (function(){ return (void 0,eval)("this")})();

console.log("globalThis:", globalThis);

Function

同eval相似功能的还有Funciton,但也有所不同;首先是参数:除了最后一个参数之外所有的参数被视作型参,最后一个参数被当作函数体,像这样:

function test() {
	return new Function('a', 'b', "return a + b");
}
console.log(test()(1, 2)); // 3

存在的问题

同eval一样将字符串解析成代码有==xss攻击==的风险,同时==性能==也同样存在着问题: gh

应用场景

  • 创建不定参的函数
function test() {
  return new Function('a', 'b', "return a + b");
}
console.log(test()(1, 2));

function createSum(len) {
  let code = 'return ';
  for (let i = 0; i < len; i++) {
    code += i === len - 1 ? 'args[' + i + ']' : 'args[' + i + '] + ';
  }
  code += ';'
  return new Function('...args', code);
}

console.log(createSum(4)(1,2,3,4));
  • 模板引擎的实现

eval和Function的不同点

  • eval是一个方法直接将字符串==解析成代码执行==,而Function是构造函数得到的是一个==函数实例==
  • eval直接执行时this和函数具体执行相关,而window.evalnew Funcion都指向的是window
 var name = 'global'
    function foo() {
      var name = 'foo name';
      eval('console.log(this.name)'); // obj name
      window.eval('console.log(this.name)'); // global
      new Function('console.log(this.name)')(); // global
    }
    const obj = {
      name: 'obj name',
      foo
    }
    obj.foo();
  • eval会受CSP策略的影响,而Function不会

承接开头让我们使用eval来实现一个简陋的代码执行器

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    textarea {
      display: block;
      width: 80vw;
      margin-bottom: 20px;
    }
  </style>
</head>

<body>
  <h2>
    请输入你的代码
  </h2>
  <textarea name="" id="input-code" cols="100" rows="20"></textarea>
  <button id="btn">
    点我运行代码
  </button>
  <h2>
    输出
  </h2>
  <div id="output"></div>
  <script>
    const input = document.querySelector('#input-code');
    const output = document.querySelector('#output');
    const btn = document.querySelector('#btn');
    let code = '';

    const runCodeWithEval = function(code) {
      let console = {
        ...window.console,
        logData: '',
        log(...data) {
          this.logData += data.toString() + '\n';
        }
      }
      eval(code);
      return console.logData;
    }

    const emit = function () {
      const logData = runCodeWithEval(code);
      console.log(logData);
      output.innerText = logData;
    }

    btn.addEventListener('click', emit, false);
    input.addEventListener('change', function (e) {
      code = e.target.value;
    });
  </script>
</body>

</html>

reference