相信看过MDN网站的同学肯定都见过这样的一个功能:
通过点击run即可以看到代码框内的输出结果,是不是很神奇,这种功能要如何实现的呢?
要回答这个问题就不得不提到今天的主角
eval
、new Function
eval
存在的问题
eval相信很多同学第一时间想到eval不安全、不推荐使用,确实它有着诸多问题:
-
有可能会发生xss攻击,因为==输入的字符串直==接被当成代码解析这就非常危险了
-
性能较差,相比普通函数执行性能较差
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
(作用域链)上有所在上下文的作用域链即使没引用外层函数的变量
通过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了
- 尽量不要在业务代码使用,因为通过内容安全策略CSP可以禁用eval
应用场景
- 运算,我们可以通过给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攻击==的风险,同时==性能==也同样存在着问题:
应用场景
- 创建不定参的函数
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.eval
和new 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>