什么是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()(在像这样调用时),关键在于它会使用调用它的代码的变量环境。它会像本地代码一样查找变量的值、定义新变量和函数。
- 定义在包含上下文中的变量可以在 eval()调用内部被引用
如果一个函数定义了一个局部变量x,然后调用了eval("x")
,那它会取得这个局部变量的值。
function foo() { let x = 1 + 1; eval("console.log(x)"); // 2 } foo() 复制代码
- 声明局部变量
如果这个函数调用了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() 复制代码
- 如果被求值的字符串使用了
let
或const
,则声明的变量或常量会被限制在求值的局部作用域内,不会定义到调用环境中。
通过 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 复制代码
- 传给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(..) 略微安全一些,但也要尽量避免使用。
在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上的损失。