30天学习计划 js忍者秘籍 第9章 忍者点金术:运行时代码求值

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代码字符串,用于在运行时进行求值操作。

你可能感兴趣的:(30天学习计划 js忍者秘籍 第9章 忍者点金术:运行时代码求值)