9.1.1 用eval()方法进行求值
eval()方法可能是在运行时进行代码求值的最常用方式了。作为定义在全局作用域内的eval()方法,该方法将在当前上下文内,执行所传入字符串形式的代码。执行返回结果则是最后一个表达式的执行结果。
1)基本功能
该方法将执行传入代码的字符串,在调用eval()方法的作用域内进行代码求值。
示例9.1 eval()方法的基本测试
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
assert(eval('5+5') === 10,'5 and 5 is 10');
assert(eval('var ninja = 5;') === undefined,'no value was returned.');
assert(ninja === 5,'The variable ninja was created');
(function(){
eval('var ninja = 6;');
assert(ninja === 6,'evaluated within the current scope.');
})()
assert(window.ninja === 5,'this global scope was unaffected.');
assert(ninja === 5,'the global scope was unaffected.');
2)求值结果
eval()方法将返回传入字符串中最后一个表达式的执行结果。
eval('3+4;5+6') 结果将返回11
任何不是简单变量、原始值、赋值语句的内容都需要在外面包装一个括号,以便返回正确的结果。
var o = eval('({ninja:1})')
示例9.2 测试eval()的返回结果
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var ninja = eval('({name:"ninja"})');
assert(ninja != undefined,'the ninja was created');
assert(ninja.name === 'ninja','and with the expected property');
var fn = eval('(function(){return "ninja";})');
assert(typeof fn === 'function','the function as created');
assert(fn() === 'ninja','and returns expected value');
var ninja2 = eval('{name:"ninja"}');
assert(ninja2 != undefined,'ninja2 was created.');
assert(ninja2.name === 'ninja','and with the expected property');
最后一个测试失败了,因为对象没有按照预期进行创建。
就像我们用普通方式在特定作用域内创建函数一样,eval()创建的函数会继承该作用域的闭包——局部作用域内执行eval()的衍生结果。
9.1.2 用函数构造器进行求值
js中所有的函数都是Function的实例,可以通过像function name(){}这样的语法创建命名函数,或者省略名称创建匿名函数。
也可以直接使用Function构造器来实例化函数。
var add = new Function('a','b','return a+b');
assert(add(3,4)===7,'Function created and working!);
Function构造器可变参数列表的最后一个参数,始终是要创建函数的函数体内容。前面的参数则表示函数的形参名称。
上边代码等价于: var add = function(a,b){return a+b}
虽然这些代码在功能上是等同的,但采用Function构造器方式有一个明显的区别,函数体由运行时的字符串所提供。
另外一个极其重要的实现区别是,使用Function构造器创建函数的时候,不会创建闭包。在不想承担任何不相关的闭包的开销时,这可能是一件好事。
9.1.3 用定时器进行求值
通过定时器可以让代码字符串进行求值,而且是异步的。
我们通常给定时器传递一个内联函数或函数引用。这是setTimeout()和setInterval()方法推荐使用的方式,但是这些方法也可以接受字符串的传入,从而在定时器触发的时候进行求值。
var tick = window.setTimeout('alert("hi")',100)
这种方式使用情况不多,除非要求值的代码必须是运行时字符串。
9.1.4全局作用域内的求值操作
示例9.3 在全局作用域内求值代码
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function globalEval(data){
data = data.replace(/^\s*|\s*$/g,'');
if(data){
var head = document.getElementsByTagName('head')[0]||document.documentElement,
script = document.createElement('script');
script.type = 'text/javascript';
script.text = data;
head.appendChild(script);
head.removeChild(script);
}
}
window.onload = function(){
(function(){
globalEval('var test=5;');
})()
assert(test===5,'The code was evaluated globally.')
}
9.1.5 安全的代码求值
一个命名为Caja的谷歌项目,尝试创建一个js翻译器,以便将js转换成一种更安全且免受恶意攻击的形式。
http://code.google.com/p/google-caja/
9.2 函数反编译
示例9.4 将函数反编译成字符串
function test(a){return a+a;}
assert(test.toString()==='function test(a){return a+a;}','function decompiled')
toString()的返回值包含原始声明的所有空格,包括行结束符。请注意,在反编译函数的时候,需要考虑空格和函数体的格式。
反编译行为有很多潜在的用途,尤其是在宏指令和代码重写的时候。在Prototype js库中,有一个比较有趣的应用是,将函数进行反编译从而读取该函数的参数,然后将这些参数名称保存到一个数组中。通常用于确定函数想得到什么样的参数值。
示例9.5 查找函数参数名称的函数
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function argumentNames(fn){
var found = /^[\s\(]*function[^(]*\(\s*([^)]*?)\s*\)/.exec(fn.toString());
return found && found[1] ? found[1].split(/,\s*/) : [];
}
assert(argumentNames(function(){}).length === 0,'works on zero-arg functions.')
assert(argumentNames(function(x){})[0] === 'x','single argument working.')
var results = argumentNames(function(a,b,c,d,e){});
assert(results[0] == 'a' && results[1] == 'b' && results[2] == 'c' && results[3] == 'd' && results[4] == 'e','multiple arguments working.')
该函数反编译了传入的函数,并使用正则表达式,将这些参数从逗号分隔的参数列表中抽取出来。
9.3 代码求值实战
9.3.1 JSON转化
示例9.6 将JSON字符串转化成js对象
var json = '{"name":"ninja"}';
var object = eval('('+json+')');
assert(object.name === 'ninja','my name is ninja!');
使用eval()做JSON解析时需要注意的主要是:通常,JSON数据来自于远程服务器,盲目执行远程服务器上不可信代码,基本是不可取的。
最受欢迎的JSON转换器脚本是由JSON标记的创造者所编写的,在该转换器中,他做了一些初步的JSON字符串解析,以防止任何恶意信息通过。代码地址:https://github.com/douglascrockford/JSON-js
他写的函数在实际求值之前,执行一些重要的预处理操作。
.防范一些可能在某些浏览器上引起问题的Unicode字符。
.防范恶意显示的非JSON内容,包括赋值运算符和new操作符。
.确保只包含了符合JSON规范的字符。
9.3.2 导入有命名空间的代码
对于将命名空间导入到当前上下文,base2库提供了一个非常有趣的解决方案。因为没有办法将该问题进行自动化操作,因此我们可以利用运行时求值让该实现变得简单。
每当一个新类或模块添加到base2包的时候,构造可执行代码的字符串,对其进行求值,可以将产生的函数引入到当前上下文中,示例如下,假设已经加载了base2。
示例9.7 测试base2的命名空间导入是如何工作的。
base2.namespace == //#1
"var Base=base2.Base;var Package=base2.Package;" +
"var Abstract=base2.Abstract;var Module=base2.Module;" +
"var Enumerable=base2.Enumerable;var Map=base2.Map;" +
"var Collection=base2.Collection;var RegGrp=base2.RegGrp;" +
"var Undefined=base2.Undefined;var Null=base2.Null;" +
"var This=base2.This;var True=base2.True;var False=base2.False;" +
"var assignID=base2.assignID;var detect=base2.detect;" +
"var global=base2.global;var lang=base2.lang;" +
"var JavaScript=base2.JavaScript;var JST=base2.JST;" +
"var JSON=base2.JSON;var IO=base2.IO;var MiniWeb=base2.MiniWeb;" +
"var DOM=base2.DOM;var JSB=base2.JSB;var code=base2.code;" +
"var doc=base2.doc;";
assert(typeof This === "undefined", //#2
"The This object doesn't exist." );
eval(base2.namespace); //#3
assert(typeof This === "function", //#4
"And now the namespace is imported." );
assert(typeof Collection === "function",
"Verifying the namespace import." );
这是一个用于解决复杂问题的非常巧妙的方法。
9.3.3 JS压缩和混淆
最好是将代码写得越清晰越好,然后再进行压缩传输。
压缩js代码的工具 packerhttp://dean.edwards.name/packer/使用eval()进行大规模的重写和解压
下载和求值之间的组合对页面的性能才是最重要的。
加载时间 = 下载时间+求值时间
使用简单压缩性能是最好的,如果要用代码混淆,可以使用packer
9.3.4 动态重写代码
由于我们可以使用函数的toString()方法反编译现有的js函数,可以从现有函数中提取并加工原有函数的内容,从而创建一个 新函数。
单元测试库Screw.Unit(https://github.com/nkallen/screw-unit),就是一个这样的案例。
Screw.Unit使用库中提供的函数,将现有测试函数中的内容进行了动态重写。
describe('Matchers',function(){
it('invokes the provided matcher on a call to expect',function(){
expect(true).to(equal,true);
expect(true).to_not(equal,false);
})
})
describe(),it()以及expect(),这些方法在全局作用域内都不存在。Screw.Unit重写了这段代码,使用多个width(){}语句,将函数内部的内容注入到需要执行的函数中。
var contents = fn.toString().match(/^[^{]*{((.*\n*)*)}/m)[1];
var fn = new Function('matchers','specifications','with(specifications){width(matchers){'+contents+'}}')
fn.call(this.Screw.Matchers,Screw.specifications);
这是一个让测试开发人员在无需将变量引入到全局作用域的情况下,利用代码求值就可以提供简洁用户体验功能的场景。
9.3.5 面向切面的脚本标签
AOP,面向方面编程。
AOP技术可以在运行时将代码进行注入并执行一些“横切”代码,如日志记录、缓存、安全性检查等。AOP引擎将在运行时添加日志代码,而不是在原有代码中添加大量的日志语句,以便让开发人员在开发期间不用关注这些事情。
定义自定义脚本类型是非常简单的,因为浏览器会忽略任何无法识别的脚本类型。通过使用一个不标准的类型值,我们可以强制浏览器完全忽视一个脚本块。
...
注意,我们使用统一约定的“x”表示自定义类型。我们打算用这样的块来包含正常的js代码,以便在页面加载时进行执行,而不是通常的内联执行。
示例9.8 创建一个在页面加载后才执行的脚本标签类型
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function globalEval(data){
data = data.replace(/^\s*|\s*$/g,'');
if(data){
var head = document.getElementsByTagName('head')[0]||document.documentElement,
script = document.createElement('script');
script.type = 'text/javascript';
script.text = data;
head.appendChild(script);
head.removeChild(script);
}
}
window.onload = function(){
var scripts = document.getElementsByTagName('script');
for(var i=0; i
if(scripts[i].type == 'x/onload'){
globalEval(scripts[i].innerHTML)
}
}
}
assert(true,'Executed on page load')
在本例中,我们提供一个浏览器忽略执行的自定义脚本块。在页面的onload处理程序中,查询所有的脚本块,再筛选自定义类型的脚本块,最后用本章前面开发的globalEval()函数,在全局作用域内对脚本块的内容进行求值。
这种技术有更复杂更有意义的用途。例如,将自定义脚本块和jQuery.tmpl()方法一起使用,用于提供运行时模板。利用它可以在用户界面上执行脚本,或者在准备操作DOM的时候,甚至是相邻元素上执行脚本。
9.3.6 元语言和领域特定语言
关于运行时代码求值的一个最重要示例,可以在构建于js之上的其他编程语言实现中看到:元语言。可以将其动态转换成js源代码并求值。通常,这种定制语言非常特定于开发人员的业务需求,并且已经创建了领域特定语言(DSL)这样的名字。
Processing.js
Processing.js是Processing(http://processing.org/)可视化语言的一部分,该可视化语言通常使用java实现。js的实现运行在HTML5的Canvas元素上,由John Resig创建。
这种实现是一种完整的编程语言,可以用来操作绘图区域的视觉显示。
通过使用Processing.js语言,我们获得一些使用js时所没有的直接好处。
.从Processing高级语言特性中获益(如类和继承)
.获取Processing的简单但强大的绘图API
.可以使用Processing现有的文档和示例。
所以这些高级处理代码,都可以通过js语言的代码求值功能来实现。
Objective-J
是Objective-C编程语言的js实现,被用于280Slides产品。
Objective-J解析程序,是由js编写的,并可以在运行阶段转换Objective-J代码,它们使用轻量级表达式进行匹配并处理Objective-C语法代码,而不会干扰现有的js代码。其处理结果是一个js代码字符串,用于在运行时进行求值操作。