前言
大家好,我是抹茶。
在日常工作的开发中,90%的场景,我们要运行的代码是提前预知写好的,但依然有一些场景,我们需要动态的拼接生成代码字符串。比如我们之前有个项目需要做国际化翻译,但翻译的key和slot根据后台接口动态得到的,无法事先写好翻译代码,而是拼接好代码字符串后执行得到结果。或者是下面的例子,根据对象的路径拿对象的属性值。
// 请实现get函数:
// function get() {
// 请补全函数参数和实现逻辑
// }
//const obj = {
//selector: { to: { toutiao: 'FE coder' } },
//target: [1, 2, { name: 'byted' }]
//};
//运行代码 get(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name')
//输出结果: ['FE coder', 1, 'byted']
//(不能使用with和eval)
const obj = {
selector: { to: { toutiao: 'FE coder' } },
target: [1, 2, { name: 'byted' }]
};
// 根据路径拿对象属性值
const getValueByPath = (param1, ...rest)=> {
const obj = JSON.stringify(param1);
const result = rest.map(item => {
return new Function(`return ${obj}.${item}`)();
});
return result;
}
getValueByPath(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name')
本文将围绕JavaScript中,让代码字符串跑起来的方案进行展开。
1.setTimeout()
看到setTimeout的时候是不是有点震惊,它的第一个参数不应该是个函数嘛?是的,第一个参数可以是函数,但也可以是代码字符串。下面请看MDN的介绍
const a = 1,b = 2;
let result = 0;
setTimeout("result = a + b",100);
setTimeout(()=>console.log(result),100); // 3
2.setInterval()
如果你已经为setTimeout震惊过了,那么setInterval你应该能很好的接受了~
MDN的介绍如下:
const a = 1,b = 2;
let result = 0;
const id = setInterval(`
result = a + b;
console.log('result',result);
clearInterval(id)`,
100);// result 3
3.eval()
研究过JS底层设计的朋友应该对eval()并不陌生,他的设计初衷就是为了将传入的字符串当做 JavaScript 代码进行执行。
eval是全局对象的一个函数属性,接收一个表示 JavaScript 表达式、语句或一系列语句的字符串。表达式可以包含变量与已存在对象的属性。
conat a = 1,b = 2;
let result = 0;
result = eval("a + b")
setTimeout("result = a + b",100)
4. new Function()
Funciton 是官方推荐的eval平替。
Function()创建一个新的Function对象。直接调用此构造函数可以动态创建函数,但会遇到和eval()类似的安全问题和(相对较小的)性能问题。然而,与eval()不同的是,Function构造函数创建的函数只能在全局作用域中运行。
使用方式如下:
let func = new Function ([arg1, arg2, ...argN], functionBody);
//等价于 let func = Function ([arg1, arg2, ...argN], functionBody);
调用
Function()时可以使用或不使用new。两者都会创建一个新的Function实例。
const a = 1,b = 2;
const sum = new Function("a","b","return a + b");
console.log(sum(a,b));// 3
funciton 和eval的区别
eval中的代码执行时的作用域为当前作用域。它可以访问到函数中的局部变量。
let a = 1
let fn = function(){
let a = 2
let result1 = new Function('console.log(a)');
let result2 = eval('console.log(a)') //2
result1() //1
}
fn()
总结
本文介绍了四种能让代码字符串跑起来的方法。
需要知道的是,这四种方案比传统的方式性能更低(JS引擎无法预知,无法做优化),甚至可能被第三方加以利用进行攻击。
我们在开发中应当避免使用Function()函数与eval()函数,同时切忌在使用setTimeout()函数和setInterval()函数时,第一个参数不要用字符串。
实在一定要运行动态生成的代码字符串时,new Function可能相对来说比较适宜。
上面的获取对象属性值的例子也可以用其他方式实现,如下:
const obj = {
selector: { to: { toutiao: 'FE coder' } },
target: [1, 2, { name: 'byted' }]
};
// 根据路径拿对象属性值
const getValueByPath = (obj, ...rest) => {
function convertString (str) {
// 使用正则表达式匹配形如 [数字] 的子串
return str.replace(/\[(\d+)\]/g, '.$1');
}
const result = rest.map(item => {
const pathList = convertString(item).split('.');
let parent = obj;
while (pathList.length) {
let path = pathList.shift();
parent = parent[path]
}
return parent;
});
console.log(result);//[ 'FE coder', 1, 'byted' ]
return result;
}
getValueByPath(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name')