JavaScript隐藏技能:让代码字符串动起来

299 阅读3分钟

前言

大家好,我是抹茶。

在日常工作的开发中,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的介绍

image.png
const a = 1,b = 2;
let result = 0;
setTimeout("result = a + b",100);
setTimeout(()=>console.log(result),100); // 3

2.setInterval()

如果你已经为setTimeout震惊过了,那么setInterval你应该能很好的接受了~

MDN的介绍如下:

image.png
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')