代码求值机制
javaScript有几种运行机制,每种都有不同的使用上下文,这些不同的代码求值方式包括:
- eval函数
- 函数构造器
- 定时器
-
script标签元素
在查看这些机制时,我们将讨论代码求值的作用域,并学习在运行时运行代码求值的安全实践。
用eval()方法进行求值
eval方法可能是运行时代码求值的最常用的方式了,作为定义在全局作用域内的eval方法,该方法将在当前上下文内,执行所传入字符串形式的代码。执行返回结果则是最后一个表达式的执行结果。
var a = eval("5 + 5");
(function ()
eval('var ninja = 6');
})()
var b = eval('3+4; 5+6');
测试上面代码,感受eval运行。
应该指出的是,任何不是简单变量,原始值,赋值语句的内容都需要在外面包一个括号,以便返回正确的结果。例如,如果我们想使用eval()创建一个简单的对象,可能会编写如下的语句:
var o = eval('{ninja: 1}');
但是结果不是我们期望的,需要加一个括号,如下:
var o = eval('({njnja: 1})');
我们创建函数通常不会使用eval的方式,但是如果我们不知道要创建的函数具体的什么内容,我们可能需要在运行时生成代码或者从别人哪里得到一些代码。
就像我们用普通方式在特定作用域内创建函数一样,eval()创建的函数会继承该作用域的闭包,局部作用域内执行eval()时的衍生结果。
用函数构造器进行求值
javaScript中所有的函数都是Function的实例,可以直接使用Function构造器来实例化函数,如下所示:
var add = new Function('a', 'b', 'return a + b;');
Function构造器可变参数列表的最后一个参数,始终是要创建函数的函数题内容。前面的参数则表示函数的形参名称。所以,上述等价如下:
var add = function (a, b)
return
虽然功能等同,但采用Function构造器方式有一个明显的区别,函数体由运行时字符串所提供。
另一个重要区别是使用Function构造器创建函数的时候,不会创建闭包。在不想承担任何不相关闭包的开销时,这可是一件好事。
用定时器进行求值
定时器可以传递一个内联函数或函数引用,这是推荐使用的方式,但是这些方法也可以接受字符串的传入,从而在定时器触发的时候进行求值。示例如下:
var tick = window.setTimeout('alert("hi !")', 100);
使用这种方式的情形很罕见(大致相当于new Function()的使用方式)
,他的使用会让人失望,除非要求值的代码必须是运行时字符串。
因为断网就写到这里来了。
在讨论eval()方法时候我们强调,求值执行的作用域就是调用eval()时的作用域。
function globalEval(data)
data = data.replace(/^\s*|\s*$/g, '');
if(data) {
var head = document.getElmentsByTagName('head')[0] ||
document.documentElement,
script = document.createElement('script');
script.type = 'text/script';
script.text = data;
head.appendChild(script);
head.removeChild(script);
}
}
window.onload = function ()
(function ()
globalEval('var test = 5;');
})();
}
这段代码很简单,我们定义了一个名为globalEval()的函数,以便在全局作用域内求值任何想要求值的内容。
安全的代码求值
关于代码求值,经常出现的一个问题是,如何安全的执行javaScript代码,换句话说,在不损害网站完整性的情况下,可以安全的执行不可信的javaScript代码码?
然而还是有希望的,一个命名为caja的谷歌项目,尝试创建一个javaScript翻译器,以便将javaScript转换成一种更安全且免受恶意攻击的形式。
函数反编译
大多数javaScript实现,还提供一种将以求值过的javaScript代码进行‘反编译’的功能。
我们称这一过程为序列化,下面将函数反编译成字符串,
反编译听起来很复杂,其实很简单,是由函数的toString()方法来执行,让我们用下面的函数测试一下,
function test(a)
return
需要注意的是,toString()的返回值包含原始声明的所有空格,包括行结束符。
反编译行为有很多潜在的用途,尤其是在宏指令和代码重写的时候,在prototype javaScript库中,有一个比较有趣的应用是,将函数进行反编译从而读取该函数的参数,然后将这些参数名称保存到一个数组中,这是非常有用的,通常用于确定函数想得到什么样的参数。如下代码是Prototype中推断函数参数名称的一段简化代码。
function argumentNames(fn)
var found = /^[\s\(]*function[^(]*\(\s*([^)]*?)\s*\)/.exec(fn.toString());
return found && found[1] ?
found[1].split(/, \s*/) :
[];
}
有的浏览器不支持反编译功能,我们需要使用特性仿真来测试浏览器是否支持反编译功能,其中一种方式如下:
var FUNCTION_DECOMPILATION = /abc(.|\n)*xyz/.test(function (abc)
再一次使用了正则表达式,向test方法传入一个函数,并将结果存储在一个变量中,以供稍后进行使用,截至目前,我们讨论了运行时代码求值的各种方式。现在,让我们把这些知识转化为行动。
代码求值实战
JSON转化
在早些浏览器不提供parse()和stringify()方法,我们自己实现一个,将json字符串转化为javaScript对象
var json = '{"name": "Ninja"}';
var object = eval("(" + json + ")");
非常简单,在大多数javaScript引擎中都表现良好。
但使用eval()做json解析时需要注意的主要是:通常,JSON数据来自于远程服务器,盲目执行远程服务器上的不可信代码,基本是不可取的。
导入有命名空间的代码
如果要将命名空间化的代码引入当前上下文,那又该如何做呢?这个问题很有挑战性,考虑到javaScript语言中没有简单或支持的方法,大多数时候,我们不得不使用类似如下的代码:
var DOM = base2.DOM;
var JSON
对于将命名空间导入到当前上下文,base2库提供了一个非常有趣的方案,因为没有办法将该问题进行自动化操作,因此我们可以利用运行时求值让该实现变得更简单。
面向切面的脚本标签
AOP,或面向方面编程,维基百科将其定义为“一种旨在通过分离横切关注点而增加模块化的编程范式”是的,听起来让我们头晕。
让我们来看一下如何充分利用AOP的思想。
我们之前已经讨论过,在页面的脚本标记中使用无效的类型属性,包含一些不想让浏览器触碰的新数据,我们可以把这个概念更进一步具体化,使用他增强现有的javaScript。
<script type="x/onload">... custom script here ...</script>
window.onload = function () {
var script = document.getElementsByTagName('script');
for (var i = 0; i < script.length; i++) {
if(script[i].type == 'x/onload') {
globalEval(script[i].innerHTML);
}
}
}