默海笑

V1

2022/09/20阅读:23主题:嫩青

万恶的 eval() ?

什么是eval()?

eval()函数就是一个完整的 ECMAScript 解释器,它接收一个参数,即一个要执行的 ECMAScript(JavaScript)字符串。

与很多解释型语言一样,JavaScript有能力解释JavaScript源代码字符串,对它们求值以产生一个值。JavaScript就是通过全局函数eval()来对源代码字符串求值的。

关于eval()是函数还是操作符?

eval()是一个函数,其实应该是个操作符。JavaScript语言最初的版本定义了一个eval()函数,语言设计者和解释器开发者一直对它加以限制,导致它越来越像操作符。

现代JavaScript解释器会执行大量代码分析和优化。一般来说,如果一个函数调用eval(),则解释器将无法再优化该函数。

把eval()定义为函数的问题在于可以给它起不同的名字如果可以这样,那么解释器无法确定哪个函数会调用eval(),也就无法激进优化。假如eval()是个操作符(即保留字),那这个问题就可以避免。

作用域问题

通过 eval()执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。

在执行 eval(..) 之后的代码时,引擎并不“知道”或“在意”前面的代码是以动态形式插入进来,并对词法作用域的环境进行修改的。引擎只会如往常地进行词法作用域查找。

eval()的使用

关于参数

eval()期待一个参数。

1.如果给它传入任何非字符串值,它会简单地返回这个值。

2.如果传入字符串,它会尝试把这个字符串当成JavaScript代码来解析,

①解析失败会抛出SyntaxError。

②如果最后一个表达式或语句没有值则返回undefined。

③如果解析字符串成功,它会求值代码并返回该字符串中最后一个表达式或语句的值。

④如果求值字符串抛出异常,该异常会从调用eval()的地方传播出来。

直接eval()

对于eval()(在像这样调用时),关键在于它会使用调用它的代码的变量环境。它会像本地代码一样查找变量的值、定义新变量和函数。

  1. 定义在包含上下文中的变量可以在 eval()调用内部被引用

如果一个函数定义了一个局部变量x,然后调用了eval("x"),那它会取得这个局部变量的值。

function foo({
    let x = 1 + 1;
    eval("console.log(x)"); // 2
}
foo()
  1. 声明局部变量

如果这个函数调用了eval("var y = 3;"),则会声明一个新局部变量y。

eval("var y = '3';"); 
console.log(y); // 3

eval()声明一个局部函数

eval(`function foo() {
    let x = 1 + 1;
    console.log(x);
}`
)
foo()
  1. 如果被求值的字符串使用了letconst,则声明的变量或常量会被限制在求值的局部作用域内,不会定义到调用环境中。

    通过 eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在 eval()执行的时候才会被创建。

eval("var msg = 'hello world';"); 
console.log(msg); // hello world

eval("let msg = 'hello world';"); 
console.log(msg); // Reference Error: msg is not defined
  1. 传给eval()的代码字符串本身必须从语法上说得通:不能使用它向函数中粘贴代码片段。

eval("return;")是没有意义的,因为return只在函数中是合法的,即使被求值的字符串使用与调用函数相同的变量环境,这个字符串也不会成为函数的一部分。

只要这个字符串本身可以作为独立的脚本运行(即使像x=0这么短),都可以合法地传给eval()。否则,eval()将抛出SyntaxError。

严格模式

一、严格模式对eval()函数增加了更多限制,甚至对标识符“eval”的使用也进行了限制。

当我们在严格模式下调用eval()时,或者当被求值的代码字符串以“use strict”指令开头时,eval(..) 在运行时有其自己的词法作用域,eval()会基于一个私有变量环境进行局部求值。这意味着在严格模式下,被求值的代码可以查询和设置局部变量,但不能在局部作用域中定义新变量或函数也就是说在 eval()内部创建的变量和函数无法被外部访问。

function foo(str{
    "use strict";
    eval(str);
    console.log(a); // ReferenceError: a is not defined
}
foo("var a = 2");

二、严格模式让eval()变得更像操作符,因为“eval”在严格模式下会变成保留字。

不能再使用新值来重写eval()函数。通过名字“eval”来声明变量、函数、函数参数或捕获块参数都是不允许的。

全局eval()

一、

eval()会干扰JavaScript的优化程序,是因为它能够修改局部变量。解释器也不会过多优化调用eval()的函数。

JavaScript规范中说,如果eval()被以“eval”之外的其他名字调用时,它应该把字符串当成顶级全局代码来求值。

被求值的代码可能定义新全局变量或全局函数,可能修改全局变量,但它不会再使用或修改调用函数的局部变量。因此也就不会妨碍局部优化。

使用名字“eval”来调用eval()函数就叫作“直接eval”(这样就有点保留字的感觉了)。直接调用eval()使用的是调用上下文的变量环境。如果在顶级代码中调用eval(),则它操作的一定是全局变量和全局函数。

二、

任何其他调用方式,包括间接调用,都使用全局对象作为变量环境,因而不能读、写或定义局部变量或函数(无论直接调用还是间接调用都只能通过var来定义新变量。在被求值的字符串中使用let和const创建的变量和常量会被限定在求值的局部作用域内,不会修改调用或全局环境)。

let x = 'g';
let y = 'g';
let gEval = eval;
function g({
    let x = 'l';
    gEval("x += 'changed'");
    return x;
}

function l({
    let y = 'l';
    eval("y += 'changed'");
    return y;
}

console.log(g() , x); // l gchanged  g函数外部x改变了
console.log(l() , y); // lchanged g  l函数内部y改变了

这种全局求值的能力不仅仅是为了适应优化程序的需求,同时也是一种极其有用的特性,可以让我们把代码字符串作为独立、顶级的脚本来执行。假如你必须使用eval(),那很可能应该使用它的全局求值而不是局部求值。

为什么不提倡使用eval()?

解释代码字符串的能力是非常强大的,但也非常危险。在使用 eval()的时候必须极为慎重,特别是在解释用户输入的内容时。因为这个方法会对 XSS 利用暴露出很大的攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。

某些Web服务器使用HTTP的"Content-Security-Policy"头部对整个网站禁用eval()

无论在声明时候eval()都可以运行期间修改书写期的词法作用域。除了严格模式。

和eavl函数有类似作用

setTimeout(..) 和 setInterval(..) 的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的函数代码。这些功能已经过时且并不被提倡。不要使用它们!

new Function(..) 函数的行为也很类似,最后一个参数可以接受代码字符串,并将其转化为动态生成的函数(前面的参数是这个新生成的函数的形参)。这种构建函数的语法比 eval(..) 略微安全一些,但也要尽量避免使用。

在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上的损失。

参考

《你不知道的js上》 《JavaScript权威指南第七版》 《JavaScript高级程序设计第四版》

分类:

前端

标签:

JavaScript

作者介绍

默海笑
V1