1.eval的初体验
eval
这个语句,怎么说呢?十分好用,其实换句话说“用来装逼真是挺不错的。”
它接收一个字符串的实参,然后就可以像在解释器上输入对应字符串的内容那样执行该字符串。不信?看看这个
// 我们可以用它定义一个函数
> eval("function a(m) {return m + 1}")
undefined
> a(10)
11
// 注意如果是匿名函数要这样
> a = eval("(function(m) {return m + 1})")
[Function]
> a(10)
11
// 进行算数运算
> eval("1+2+3+4")
10
// 它还可以利用当前上下文
> x = 1222222
1222222
> eval("x")
1222222
如果eval
里面的值不是字符串的话则返回实参的值!!
> eval(12)
12
// 计算是字符串对象也不行
> eval(new String("console"))
[String: 'console']
这看起来确实很牛逼。有许多动态语言如Python, Ruby都有这个函数。不过我也很少在代码里见到它的身影。
这里传进去的字符串必须是符合语义的,不然的话会报语法错误。
对于这个方法的使用一直都有争议。我看到的下面两种说法。
1. 安全性考虑
eval
比较危险。想想这样一种情形,如果我们不小心把eval
接口开放了一部分出去。其实就相当于给我们的用户执行代码的权利。用户有意或是无意地让我们eval
一些不符合语法规则的字符串内容,有可能会让我们的系统抛出异常。
2.优化不足
现代的JS解释器对大部分JS语法解析都进行了优化,而对那些调用了eval()
的函数优化得并不是很多。
其实大多数情况下我们完全可以不必选择eval
。目前来讲有很多的可替代方案。
Don’t use eval needlessly!
这里我举个例子。
有的时候我们可能想通过迭代来访问一个对象的各种属性,而事先我们不知道对象的属性有哪些是需要的,因此没办法把属性硬编到代码里面。
var dict = {x: 1, y: 2}
// 获取需要访问的键
function getKey() {return 'x'}
var key = getKey();
console.log(eval("dict." + key));
我们如果事先不知道我们要访问的键为 'x'
我们可能会写出上面的代码。其实可以用下面的代码来代替。虽然逼格相对没那么高,但是我觉得这样表达更为清晰。
var dict = {x: 1, y: 2}
// 获取需要访问的键
function getKey() {return 'x'}
var key = getKey();
console.log(dict[key]);
除非不得已否则最好还是不要用eval
因为你还有可能还会写出这样的代码。下面这个是我从资料中拿到的。比较有代表性:
function boo(a) {
eval(a)
};
boo("return;");
这句代码看似没问题,其实它会报这个错误
SyntaxError: Illegal return statement
这是为什么呢?因为eval
实际参数的字符串执行时的上下文和调用eval
函数的上下文环境是一样的,这使得eval("return;")
被理解成单独的语句,而不是放在另一个函数里面执行的语句。(这可能有点难理解)。而return;
如果不放在函数里面就没有任何意义了。所以直接报语法错误。
还不能理解?
在看我举个例子,我稍微改变一下原来的脚本
function boo(a) {
var a = "console.log('mj')";
eval(a)
};
boo("return;");
这个时候函数就会打印出mj
,由于我们赋予变量a
另外一个值。而这个字符串"console.log('mg')"
执行的上下文,其实就是在boo
函数里面。而当a = "return;"
的时候我们可以把解释器看成在一个普通的上下文作用域上面单独执行return;
语句而没有关注它是否在一个函数里面。然而这是不符合语义的。
下面再来看一个比较过瘾的例子
var x = "global";
// 我在node上跑这里需要设置成全局变量才行不能加var
y = "global";
var geval = eval;
// 函数f中eval执行字符串的上下文跟调用它的上下文作用域是相同的
function f() {
var x = "local";
eval("x += 'changed';");
return x;
}
// 函数g中geval执行的上下文是全局对象作为其上下文变量
function g() {
var y = "local";
geval("y += 'changed';");
return y;
}
console.log(f(), x);
console.log(g(), y);
最后运行的结果是
localchanged global
globalchanged local
表明了f
函数是对函数内部x
变量进行了拼接,而g
函数却对全局变量y
进行了拼接。所以这两个函数调用eval
的上下文是不一样的。
2.eval的严格模式
这里也稍微提一下Javascript的严格模式。如果在脚本中开启严格模式(现在很多地方都是这样干的,可以帮助我们写出更加严谨的Javascript代码。)
在正常模式下eval
的语法还是相对比较宽松的。
而在严格模式下,主要有这两点限制。
- 不能在局部作用域中定义新的变量和函数。但是我们可以修改原来局部变量的值。
- 严格模式中
eval
被认为是保留字。我们不能用别的函数覆盖掉原来的eval
函数。不然真的就乱套了。
还是简单举例说明:
function a() {
var x = 2;
eval("var y = 1");
eval("var x = 12");
console.log(x);
console.log(y);
}
a();
这里打印的结果我们应该都能想到 12
和 1
;
我们在严格模式下试试这个脚本。原则上我们只要在脚本的最开始加上'use strict';
就能够开启严格模式。
'use strict';
function a() {
var x = 2;
eval("var y = 1");
eval("var x = 12");
console.log(x);
console.log(y);
}
a();
这个时候在node下再运行脚本就会报错了。
ReferenceError: y is not defined
这里我们用eval
创建局部变量y
失败了,但是我们可以修改已有的局部变量x
。
啊,绕得好累,还有最后一个例子很简单,我们试着在严格模式下覆盖eval
的值。
'use strict'
eval = function() {};
解释器直接报错
SyntaxError: Unexpected eval or arguments in strict mode
我们不能在严格模式下做这种事情。严格模式有助于我们写出解释器比较认可的代码, 或者我们可以理解为Javascript的未来
。有些东西它会直接用报错来提醒我们。比如当我们想通过delete
来删除局部变量的时候,之前说过它会直接返回结果false
,但是如果在严格模式下,这种行为解释器会直接抛出异常。有些大牛还是建议我们写脚本的时候尽可能用严格模式来写,有助于我们写出专业点的Javascript代码,也有助于日后的维护升级。