Skip to content

Latest commit

 

History

History
161 lines (114 loc) · 8.44 KB

File metadata and controls

161 lines (114 loc) · 8.44 KB

9.3 Code evaluation in action

在9.1中我们看到代码解析可以多种方式生效,下面我们就通过例子来看看在何时何地该使用这种功能。

9.3.1 Converting JSON

Json是Javascript语言的子集,所以js把Json解析为代码是非常简单的。代码动态解析的常用场景可能就是把字符串形式的Json数据转成对应的Javascript对象。

大部分浏览器的都有JSON对象,有parse()和stringify()方法来处理Json字符串。但是很多原始浏览器并没有这个对象。对于这部分浏览器,如何在没有window.JSON对象时处理json字符串就非常重要了。

其实处理方法非常简单,就是通过eval解析json字符串,只是我们要记得给json字符串加上括号。如下所示:

Listing 9.6 Converting a JSON string into a JavaScript object

<script type="text/javascript">

  var json = '{"name":"Ninja"}';               //#1

  var object = eval("(" + json + ")");         //#2

  assert(object.name === "Ninja",              //#3
         "My name is Ninja!");                 //#3

</script>

非常简单,而且在多数js引擎上都支持。

当然如上一章所说,盲目的执行不可信服务器的代码是有风险的,切记。

最流行的JSON转换器脚本是Douglas Crockford写的,他是Json的创建者。其中他对json字符串做了一些预处理防止非法操作。代码详见JSON-js

其中主要的预处理包括:

  • 防止一些可能导致浏览器出问题的unicode字符的出现
  • 防止非Json模式的出现,比如赋值操作,new操作符等
  • 确保只有合法的json字符才能出现

所以,如果需要处理来自非可信站的json字符串,最好使用Douglas的转换器。对于处理非可信代码的更多更深入的讨论见下面:

好了,下面我们来看另外一个例子。

9.3.2 Importing namespaced code

在chapter3中我们讨论了如何防止局部变量污染当然作用域--通常是全局作用域。这当然是好的,但是如果我们希望把已经放入命名空间的变量引入当前作用域又该怎么办呢?

这是一个具有挑战性的问题,因为在js中没有简单的、支持该功能的方法。大多数时我们会采用如下的简陋方法:

var DOM = base2.DOM;
var JSON = base2.JSON;

Base2库为了在当前作用域引入其他命名空间,提供了一种非常有趣的方案。每增加一个新class或module,base2都会创建一段可执行代码,它可以拿来解析以引入函数或类。如下所示(假设Base2已经被加载进来):

listing9.7 Examining how base2 namespace importing works

<script src="http://base2.googlecode.com/svn/version/1.0.2/base2-p.js"></script>
<script type="text/javascript">
  /*
  base2.namespace ==                                          //#1 需要被引入的名字, base2中自带
    "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 测试eval之前,This是未定义的
         "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." );

</script>

这是一个解决复杂问题的天才方案,尽管这个方案看起来不那么优雅,不过在未来的javascript支持导入之前,我们只能这样。说到天才的解决方案,我们来介绍另外一个天才方案,就是javascript的打包。

9.3.3 JavaScript compression and obfuscation

这一段没太看明白。

代码打包的意义都清楚,因为我们需要把js脚本下载到本地,所以当然希望脚本字节尽量少。为此我们必须缩短变量名,去掉注释等。但是我们又不希望损失可读性。js代码打包的功能就是把一段原始js脚本转换成一段非常精简的字符串,通过eval该字符串,我们可以还原它的作用,同时降低了网络传输量。但此时,代码的加载速度应该等于传输速度 + eval解析速度。如果代码过于复杂,解析很慢也是没达到我们目的的。

不过现在的打包方法应该就是通过src引用那个压缩后的脚本即可。为什么还要用eval呢?

9.3.4 Dynamic code rewriting

因为我们可以反编译函数,可以通过toString()获得函数内容,然后把新内容添加到函数内,从而创造新的函数。

Screw.Unit就是通过这个技术来实现它的测试框架的。Scre.Unit的测试框架如下:

Screw.Unit(function() {
  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, equal都没有在全局空间定义,为了使以上代码正确执行,Screw.Unit临时重写了所有函数,用with(){}把函数包装了一下。大体做法就是:

var contents = fn.toString().match(/^[^{]*{((.*\n*)*)}/m)[1];
var fn = new Function("matchers", "specifications",
  "with (specifications) { with (matchers) { " + contents + " } }"
);
fn.call(this, Screw.Matchers, Screw.Specifications);

通过 with,我们使得在 fn 中调用Matchers对象的方法成为可能,创造了一种更加方便用户的接口,省去把大量变量引入全局空间的麻烦。

9.3.5 Aspect-oriented script tags

AOP是 aspect-oriented programing 的简称,维基百科的定义是:“一种编程范式,旨在通过对不同概念进行划分实现尽量多的模块化”。有点头大是不是?

抽筋剥骨看本质,AOP就是在运行时注入和执行代码的能力,注入的代码有各自不同的功能,如logging,caching,security等等。有了AOP引擎,你就能在运行时增加日志逻辑,而不用在开发时写一堆日志语句。

在运行时注入和解析代码,是不是听起来跟本章重点介绍的功能很像?下面我们就来看看代码解析如何应用到AOP编程上。

Note: 还记得5.5节里的那个memorization例子吗?那个其实就是一种AOP的范例。

我们之前讨论过,给script一个非法的type属性,可以防止浏览器解析这部分数据。现在我们就更进一步,使用它来增强现有的javascript脚本。

Listing 9.8 Creating a script tag type that executes only after the page has loaded

<script>
  window.onload = function(){
    var scripts = document.getElementsByTagName("script"); //#1
    for (var i = 0; i < scripts.length; i++) {             //#2
      if (scripts[i].type == "x/onload") {                 //#2
        globalEval(scripts[i].innerHTML);                  //#2
      }
    }
  };
</script>
<script type="x/onload">
  assert(true,"Executed on page load");                    //#3
</script>

这个例子通过指定type为 x/onload 自定义了一个新脚本块。尽管这个例子非常简单,但是我们可以把它用到更复杂的应用中,比如自定义脚本块被jQuery的 .tmpl() 使用,提供运行时模板。我们可以利用该技术进行用户交互,响应DOM事件等。总之你脑洞多大,他的应用场景就有多大。

9.3.6 Metalanguages and DSLs

主要讲了2个库:Processing.js280 North 公司的OBJECTIVE-J。并没有讲具体细节