Welcome, John
也就是说,replace方法不会影响原始字符串,而将新的串作为返回值。如果我们在替换过程中,需要对匹配的 组进行引用(正如之前的\1,\2方式那样),需要怎么做呢?还是上边这个例子,我们要在替换的过程中, 将Welcome和John两个单词调换顺序,编程John, Welcome: var result = str.replace(/(\w+),\s(\w+)/g, "$2, $1"); print(result); 可以得到这样的结果: John, Welcome 因此,我们可以通过$n来对第n个分组进行引用。 var str = "john : tomorrow :remove:file"; var result = str.split(/\s*:\s*/); print(str); print(result); http://abruzzi.javaeye.com 1.6 JavaScript内核系列 第6章 正则表达式 第 90 / 149 页 得到结果: john : tomorrow :remove:file john,tomorrow,remove,file 注意此处split方法的返回值result是一个数组。其中包含了4个元素。 var str = "Tomorrow is another day"; var index = str.search(/another/); print(index);//12 search方法会返回查找到的文本在模式中的位置,如果查找不到,返回-1。 6.3 实例:JSFilter 本小节提供一个实例,用以展示在实际应用中正则表达式的用途,当然,一个例子不可能涵盖所有的内容, 只是一个最常见的场景。 考虑这样一种情况,我们在UI上为用户提供一种快速搜索的能力,使得随着用户的键入,结果集不断的减 少,直到用户找到自己需要的关键字对应的栏目。在这个过程中,用户可以选择是否区分大小写,是否全词匹 配,以及高亮一个记录中的所有匹配。 显然,正则表达式可以满足这个需求,我们在这个例子中忽略掉诸如高亮,刷新结果集等部分,来看看正 则表达式在实际中的应用: http://abruzzi.javaeye.com 1.6 JavaScript内核系列 第6章 正则表达式 第 91 / 149 页 图1 在列表中使用JSFilter(结果集随用户输入而变化) 来看一个代码片段: this.content.each(function(){ var text = $(this).text(); var pattern = new RegExp(keyword, reopts); if(pattern.test(text)){ var item = text.replace(pattern, function(t){ return ""+t+""; }); $(this).html(item).show(); }else{//clear previous search result $(this).find("span."+filterOptions.highlight).each(function(){ $(this).replaceWith($(this).text()); }); } }); http://abruzzi.javaeye.com 1.6 JavaScript内核系列 第6章 正则表达式 第 92 / 149 页 其中,content是结果集,是一个集合,其中的每一个项目都可能包含用户输入的关键字,keyword是用户输入 的关键字序列,而reopts为正则表达式的选项,可能为(i,g,m),each是jQuery中的遍历集合的方式,非常方 便。程序的流程是这样的: l 进入循环,取得结果集中的一个值作为当前值 l 使用正则表达式对象的test方法进行测试 l 如果测试通过,则高亮标注记录中的关键字 l 否则跳过,进行下一条的检测 遍历完所有的结果集,生成了一个新的,高亮标注的结果集,然后将其呈现给用户。而且可以很好的适应用户 的需求,比如是否忽略大小写检查,是否高亮所有,是否全词匹配,如果自行编写程序进行分析,则需要耗费 极大的时间和精力。 图2 在表格中使用JSFilter(不减少结果集) 这个例子来源于一个实际的项目,我对其进行了适度的简化,完整的代码可以参考附件。 附:由于作者本身水平有限,文中难免有纰漏错误等,或者语言本身有不妥当之处,欢迎及时指正,提出建 议,参与讨论,谢谢大家! http://abruzzi.javaeye.com 1.6 JavaScript内核系列 第6章 正则表达式 第 93 / 149 页 附件下载: • scrolling.zip (100.7 KB) • dl.javaeye.com/topics/download/93ffdc7c-f960-3f8e-8b60-b3465a308c5d http://abruzzi.javaeye.com 1.6 JavaScript内核系列 第6章 正则表达式 第 94 / 149 页 1.7 JavaScript内核系列 第7章 闭包 发表时间: 2010-05-04 第七章 闭包 闭包向来给包括JavaScript程序员在内的程序员以神秘,高深的感觉,事实上,闭包的概念在函数式编程语 言中算不上是难以理解的知识。如果对作用域,函数为独立的对象这样的基本概念理解较好的话,理解闭包的 概念并在实际的编程实践中应用则颇有水到渠成之感。 在DOM的事件处理方面,大多数程序员甚至自己已经在使用闭包了而不自知,在这种情况下,对于浏览器 中内嵌的JavaScript引擎的bug可能造成内存泄漏这一问题姑且不论,就是程序员自己调试也常常会一头雾水。 用简单的语句来描述JavaScript中的闭包的概念:由于JavaScript中,函数是对象,对象是属性的集合,而 属性的值又可以是对象,则在函数内定义函数成为理所当然,如果在函数func内部声明函数inner,然后在函数 外部调用inner,这个过程即产生了一个闭包。 7.1闭包的特性 我们先来看一个例子,如果不了解JavaScript的特性,很难找到原因: var outter = []; function clouseTest () { var array = ["one", "two", "three", "four"]; for(var i = 0; i < array.length;i++){ var x = {}; x.no = i; x.text = array[i]; x.invoke = function(){ print(i); } outter.push(x); } } //调用这个函数 clouseTest(); http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 95 / 149 页 print(outter[0].invoke()); print(outter[1].invoke()); print(outter[2].invoke()); print(outter[3].invoke()); 运行的结果如何呢?很多初学者可能会得出这样的答案: 0 1 2 3 然而,运行这个程序,得到的结果为: 4 4 4 4 其实,在每次迭代的时候,这样的语句x.invoke = function(){print(i);}并没有被执行,只是构建了一个函数体 为”print(i);”的函数对象,如此而已。而当i=4时,迭代停止,外部函数返回,当再去调 用outter[0].invoke()时,i的值依旧为4,因此outter数组中的每一个元素的invoke都返回i的值:4。 如何解决这一问题呢?我们可以声明一个匿名函数,并立即执行它: var outter = []; http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 96 / 149 页 function clouseTest2(){ var array = ["one", "two", "three", "four"]; for(var i = 0; i < array.length;i++){ var x = {}; x.no = i; x.text = array[i]; x.invoke = function(no){ return function(){ print(no); } }(i); outter.push(x); } } clouseTest2(); 这个例子中,我们为x.invoke赋值的时候,先运行一个可以返回一个函数的函数,然后立即执行之,这 样,x.invoke的每一次迭代器时相当与执行这样的语句: //x == 0 x.invoke = function(){print(0);} //x == 1 x.invoke = function(){print(1);} //x == 2 x.invoke = function(){print(2);} //x == 3 x.invoke = function(){print(3);} 这样就可以得到正确结果了。闭包允许你引用存在于外部函数中的变量。然而,它并不是使用该变量创建时的值,相反, 它使用外部函数中该变量最后的值。 http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 97 / 149 页 7.2闭包的用途 现在,闭包的概念已经清晰了,我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如 模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。 7.2.1 匿名自执行函数 上一节中的例子,事实上就是闭包的一种用途,根据前面讲到的内容可知,所有的变量,如果不加上var关 键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可 能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。除了每次 使用变量都是用var关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部 变量无需维护,比如UI的初始化,那么我们可以使用闭包: var datamodel = { table : [], tree : {} }; (function(dm){ for(var i = 0; i < dm.table.rows; i++){ var row = dm.table.rows[i]; for(var j = 0; j < row.cells; i++){ drawCell(i, j); } } //build dm.tree })(datamodel); 我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被 释放,关键是这种机制不会污染全局对象。 http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 98 / 149 页 7.2.2缓存 再来看一个例子,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要 将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新 缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引 用,从而函数内部的值可以得以保留。 var CachedSearchBox = (function(){ var cache = {}, count = []; return { attachSearchBox : function(dsid){ if(dsid in cache){//如果结果在缓存中 return cache[dsid];//直接返回缓存中的对象 } var fsb = new uikit.webctrl.SearchBox(dsid);//新建 cache[dsid] = fsb;//更新缓存 if(count.length > 100){//保正缓存的大小<=100 delete cache[count.shift()]; } return fsb; }, clearSearchBox : function(dsid){ if(dsid in cache){ cache[dsid].clearSelection(); } } }; })(); CachedSearchBox.attachSearchBox("input1"); http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 99 / 149 页 这样,当我们第二次调用CachedSearchBox.attachSerachBox(“input1”)的时候,我们就可以从缓存中取道 该对象,而不用再去创建一个新的searchbox对象。 7.2.3 实现封装 可以先来看一个关于封装的例子,在person之外的地方无法访问其内部的变量,而通过提供闭包的形式来访 问: var person = function(){ //变量作用域为函数内部,外部无法访问 var name = "default"; return { getName : function(){ return name; }, setName : function(newName){ name = newName; } } }(); print(person.name);//直接访问,结果为undefined print(person.getName()); person.setName("abruzzi"); print(person.getName()); 得到结果如下: undefined default abruzzi http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 100 / 149 页 闭包的另一个重要用途是实现面向对象中的对象,传统的对象语言都提供类的模板机制,这样不同的对 象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,我 们可以模拟出这样的机制。还是以上边的例子来讲: function Person(){ var name = "default"; return { getName : function(){ return name; }, setName : function(newName){ name = newName; } } }; var john = Person(); print(john.getName()); john.setName("john"); print(john.getName()); var jack = Person(); print(jack.getName()); jack.setName("jack"); print(jack.getName()); 运行结果如下: http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 101 / 149 页 default john default jack 由此代码可知,john和jack都可以称为是Person这个类的实例,因为这两个实例对name这个成员的访问是独 立的,互不影响的。 事实上,在函数式的程序设计中,会大量的用到闭包,我们将在第八章讨论函数式编程,在那里我们会再次 探讨闭包的作用。 7.3应该注意的问题 7.3.1内存泄漏 在不同的JavaScript解释器实现中,由于解释器本身的缺陷,使用闭包可能造成内存泄漏,内存泄漏是比较 严重的问题,会严重影响浏览器的响应速度,降低用户体验,甚至会造成浏览器无响应等现象。 JavaScript的解释器都具备垃圾回收机制,一般采用的是引用计数的形式,如果一个对象的引用计数为零, 则垃圾回收机制会将其回收,这个过程是自动的。但是,有了闭包的概念之后,这个过程就变得复杂起来了, 在闭包中,因为局部的变量可能在将来的某些时刻需要被使用,因此垃圾回收机制不会处理这些被外部引用到 的局部变量,而如果出现循环引用,即对象A引用B,B引用C,而C又引用到A,这样的情况使得垃圾回收机制 得出其引用计数不为零的结论,从而造成内存泄漏。 7.3.2上下文的引用 关于this我们之前已经做过讨论,它表示对调用对象的引用,而在闭包中,最容易出现错误的地方是误用 了this。在前端JavaScript开发中,一个常见的错误是错将this类比为其他的外部局部变量: $(function(){ var con = $("div#panel"); this.id = "content"; con.click(function(){ alert(this.id);//panel }); }); http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 102 / 149 页 此处的alert(this.id)到底引用着什么值呢?很多开发者可能会根据闭包的概念,做出错误的判断: content 理由是,this.id显示的被赋值为content,而在click回调中,形成的闭包会引用到this.id,因此返回值 为content。然而事实上,这个alert会弹出”panel”,究其原因,就是此处的this,虽然闭包可以引用局部变 量,但是涉及到this的时候,情况就有些微妙了,因为调用对象的存在,使得当闭包被调用时(当这 个panel的click事件发生时),此处的this引用的是con这个jQuery对象。而匿名函数中的this.id = “content”是对匿名函数本身做的操作。两个this引用的并非同一个对象。 如果想要在事件处理函数中访问这个值,我们必须做一些改变: $(function(){ var con = $("div#panel"); this.id = "content"; var self = this; con.click(function(){ alert(self.id);//content }); }); 这样,我们在事件处理函数中保存的是外部的一个局部变量self的引用,而并非this。这种技巧在实际应用中多 有应用,我们在后边的章节里进行详细讨论。关于闭包的更多内容,我们将在第九章详细讨论,包括讨论其他 命令式语言中的“闭包”,闭包在实际项目中的应用等等。 http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 103 / 149 页 附:由于作者本身水平有限,文中难免有纰漏错误等,或者语言本身有不妥当之处,欢迎及时指正,提出建 议,参与讨论,谢谢大家! http://abruzzi.javaeye.com 1.7 JavaScript内核系列 第7章 闭包 第 104 / 149 页 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 发表时间: 2010-05-06 第八章 面向对象的 Javascript 面向对象编程思想在提出之后,很快就流行起来了,它将开发人员从冗长,繁复,难以调试的过程式程序中 解放了出来,过程式语 言如 C ,代码的形式往往如此: Component comp; init_component(& comp, props); 而面向对象的语言如 Java ,则会是这种形式: Component comp; comp.init(props); 可以看出,方法是对象的方法,对象是方法的对象,这样的代码形式更接近人的思维方式,因此 OO 大行其道 也并非侥幸。 JavaScript 本身是基于对象 的,而并非基于类。但是, JavaScript 的函数式语言的特性使得它本身是可 编程 的,它可以变成你想要的任何形式。我们在这一章详细讨论如何使用 JavaScript 进行 OO 风格的代码开 发。 8.1 原型继承 JavaScript 中的继承可以通过原型链来实现,调用对象上的一个方法,由于方法在 JavaScript 对象中是对 另一个函数对象的引用,因此解释器会在对象中查找该属性,如果没有找到,则在其内部对象 prototype 对象 上搜索,由于 prototype 对象与对象本身的结构是一样的,因此这个过程会一直回溯到发现该属性,则调用该 属性,否则,报告一个错误。关于原型继承,我们不妨看一个小例 子: http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 105 / 149 页 function Base(){ this .baseFunc = function (){ print ( "base behavior" ); } } function Middle(){ this .middleFunc = function (){ print ( "middle behavior" ); } } Middle. prototype = new Base(); function Final(){ this .finalFunc = function (){ print ( "final behavior" ); } } Final. prototype = new Middle(); function test(){ var obj = new Final(); obj.baseFunc(); obj.middleFunc(); obj.finalFunc(); } http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 106 / 149 页 图 原型链的示意图 在 function test 中,我们 new 了一个 Final 对象,然后依次调用 obj.baseFunc ,由于 obj 对象上并无此方 法,则按照上边提到的规则,进行回溯,在其原型链上搜索,由于 Final 的原型链上包含 Middle ,而 Middle 上 又包含 Base ,因此会执行这个方法,这样就实现了类的继承。 http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 107 / 149 页 base behavior middle behavior final behavior 但是这种继承形式与传统的 OO 语言大相径庭,初学者很难适应,我们后边的章节会涉及到一个比较好的 JavaScript 的面向对象基础包 Base ,使用 Base 包,虽然编码风格上会和传统的 OO 语言不同,但是读者很快 就会发现这种风格的好处。 8.1.1 引用 引用是一个比较有意思的主题,跟其他的语言不同的是, JavaScript 中的引用始终指向最终的对象,而并 非引用本身,我们来看一个例子: var obj = {}; // 空对象 var ref = obj; // 引用 obj. name = "objectA" ; print ( ref . name ); //ref 跟着添加了 name 属性 obj = [ "one" , "two" , "three" ]; //obj 指向了另一个对象 ( 数组对象 ) print ( ref . name ); //ref 还指向原来的对象 print (obj. length ); //3 print ( ref . length ); //undefined 运行结果如下: objectA objectA 3 undefined http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 108 / 149 页 obj 只是对一个匿名对象的引用,所以, ref 并非指向它,当 obj 指向另一个数组对象时 可以看到,引用 ref 并未改变,而始终指向这那个后来添加了 name 属性的 " 空 " 对象 ”{}” 。理解这一点对 后边的内容有很大的帮助。 再看这个例子: var obj = {}; // 新建一个对象,并被 obj 引用 var ref1 = obj; //ref1 引用 obj, 事实上是引用 obj 引用的空对象 var ref2 = obj; obj.func = "function" ; print (ref1.func); print (ref2.func); 声明一个对象,然后用两个引用来引用这个对象,然后修改原始的对象,注意这两步的顺序,运行之: function function 根据运行结果我们可以看出,在定义了引用之后,修改原始的那个对象会影响到其引用上,这一点也应该注 意。 http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 109 / 149 页 8.1.2 new 操作符 有面向对象编程的基础有时会成为一种负担,比如看到 new 的时候, Java 程序员可能会认为这将会调用 一个类的构造器构造一个新的对象出来,我们来看一个例子: function Shape(type){ this .type = type || "rect" ; this .calc = function (){ return "calc, " + this .type; } } var triangle = new Shape( "triangle" ); print (triangle.calc()); var circle = new Shape( "circle" ); print (circle.calc()); 运行结果如下: calc, triangle calc, circle Java 程序员可能会觉得 Shape 就是一个类,然后 triangle , circle 即是 Shape 对应的具体对象,而其实 JavaScript 并非如此工作的,罪魁祸首即为此 new 操作符。在 JavaScript 中,通过 new 操作符来作用与一个函 数,实质上会发生这样的动作: 首先,创建一个空对象,然后用函数的 apply 方法,将这个空对象传入作为 apply 的第一个参数,及上下文参 数。这样函数内部的 this 将会被这个空的对象所替代: http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 110 / 149 页 var triangle = new Shape( "triangle" ); // 上一句相当于下面的代码 var triangle = {}; Shape.apply(triangle, [ "triangle" ]); 8.2 封装 事实上,我们可以通过 JavaScript 的函数实现封装,封装的好处在于未经授权的客户代码无法访问到我们 不公开的数据,我们来看这个例子: function Person(name){ //private variable var address = "The Earth" ; //public method this .getAddress = function (){ return address; } //public variable this .name = name; } //public Person.prototype.getName = function (){ return this .name; } //public http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 111 / 149 页 Person.prototype.setName = function (name){ this .name = name; } 首先声明一个函数,作为模板,用面向对象的术语来讲,就是一个类 。用 var 方式声明的变量仅在类内部可 见,所以 address 为一个私有成员,访问 address 的唯一方法是通过我们向外暴露的 getAddress 方法,而 get/setName ,均为原型链上的方法,因此为公开的。我们可以做个测试: var jack = new Person( "jack" ); print(jack.name);//jack print(jack.getName());//jack print(jack.address);//undefined print(jack.getAddress());//The Earth 直接通过 jack.address 来访问 address 变量会得到 undefined 。我们只能通过 jack.getAddress 来访问。这 样, address 这个成员就被封装起来了。 另外需要注意的一点是,我们可以为类添加静态成员,这个过程也很简单,只需要为函数对象添加一个 属性即 可。比如: function Person(name){ //private variable var address = "The Earth" ; //public method this .getAddress = function (){ return address; } //public variable this .name = name; http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 112 / 149 页 } Person.TAG = "javascript-core" ;// 静态变量 print(Person.TAG); 也就是说,我们在访问 Person.TAG 时, 不需要实例化 Person 类。这与 传统的面向对象语言如 Java 中的静 态变量是一致 的。 8.3 工 具包 Base Base 是由 Dean Edwards 开发的一个 JavaScript 的 面向对象的基础包, Base 本身很小,只有 140 行, 但是这个很小的包对面向对象编程风格有很好的支持,支持类的定义,封装,继承,子类调用 父类的方法等, 代码的质量也很高,而且很多项目都在使用 Base 作为底 层的支持。尽管如此, JavaScript 的面向对象风格依 然非常古 怪,并不可以完全和传统的 OO 语言对等起来。 下面我们来看几个基于 Base 的例子, 假设我们现在在开发一个任务系统,我们需要抽象出一个类来表示 任务,对应的,每个任务都可能会有一个监听器,当任务执行之后,需要通知监听器。我们首先定 义一个事件 监听器的类,然后定义一个任务类: var EventListener = Base.extend({ constructor : function(sense){ this.sense = sense; }, sense : null, handle : function(){ print(this.sense+" occured"); } }); var Task = Base.extend({ constructor : function(name){ this.name = name; http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 113 / 149 页 }, name : null, listener : null, execute : function(){ print(this.name); this.listener.handle(); }, setListener : function(listener){ this.listener = listener; } }); 创建类的方式很简单,需要给 Base.extend 方 法传入一个 JSON 对象,其中可以有成员和方法。方法访问自身 的成员时 需要加 this 关键字。而每一个类都会有一个 constructor 的方法,即构造方法。比如事件监听器类 (EventListener) 的构造器需要传入一个字符串,而任务类 (Task) 也需要传入任务的名字来进行构造。好了,既 然我们已经有了任务类和事件监听器类,我们 来实例化它们: var printing = new Task("printing"); var printEventListener = new EventListener("printing"); printing.setListener(printEventListener); printing.execute(); 首先,创建一个新的 Task , 做打印工作,然后新建一个事件监听器,并将它注册在新建的任务上,这样,当 打印发生时,会通知监听器,监听器会做出相应的判断: printing printing occurred 既然有了基本的框架,我们就来使用这个框架,假设我们要从 HTTP 服务器上下载一个页面,于是我们设计了 一个新的任务类型,叫做 HttpRequester : http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 114 / 149 页 var HttpRequester = Task.extend({ constructor : function(name, host, port){ this.base(name); this.host = host; this.port = port; }, host : "127.0.0.1", port : 9527, execute : function(){ print("["+this.name+"] request send to "+this.host+" of port "+this.port); this.listener.handle(); } }); HttpRequester 类继承了 Task ,并且重载了 Task 类 的 execute 方法, setListener 方法的内容与父类一 致,因此不需要重载。 var requester = new HttpRequester("requester1", "127.0.0.1", 8752); var listener = new EventListener("http_request"); requester.setListener(listener); requester.execute(); 我们新建一个 HttpRequester 任 务,然后注册上事件监听器,并执行之: [requester1] request send to 127.0.0.1 of port 8752 http_request occured 应该注意到 HttpRequester 类 的构造器中,有这样一个语句: http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 115 / 149 页 this.base(name); 表示执行父类的构造器,即将 name 赋 值给父类的成员变量 name ,这样在 HttpRequester 的实例中,我们 就可以通过 this.name 来访问这个成员了。这套机制简直与在其他传统的 OO 语言并无二致。同时, HttpRequester 类 的 execute 方法覆盖了父类的 execute 方法,用面向对象的术语来讲,叫做重载。 在很多应用中,有些对象不会每次都创建新的实例,而是使用一个固有的实例,比如提供数据源的服务,报 表渲染引擎,事件分发 器等,每次都实例化一个会有很大的开销,因此人们设计出了单例模式,整个应用的生 命周期中,始终只有顶多一个实例存在。 Base 同样可以模拟出这样的能力: var ReportEngine = Base.extend({ constructor : null, run : function(){ //render the report } }); 很简单,只需要将构造函数的值赋为 null 即 可。好了,关于 Base 的基本用法我们已经熟悉了,来看看用 Base 还能做点什么: 由于本涨篇幅较长,因此分为两部分 发 布! 附:由于作者本身水平有限,文中难免有纰漏错误等,或者语言本身有不妥当之处,欢迎及时 指正,提出建 议,参与讨论,谢谢大家! http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 116 / 149 页 http://abruzzi.javaeye.com 1.8 JavaScript内核系列 第8章 面向对象的JavaScript(上) 第 117 / 149 页 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 发表时间: 2010-05-06 接上篇:JavaScript内核系列 第8章 面向对象的JavaScript(上) 8.4实例:事件分发器 这一节,我们通过学习一个面向对象的实例来对JavaScript的面向对象进行更深入的理解,这个例子不能太 复杂,涉及到的内容也不能仅仅为继承,多态等概念,如果那样,会失去阅读的乐趣,最好是在实例中穿插一 些讲解,则可以得到最好的效果。 本节要分析的实例为一个事件分发器(Event Dispatcher),本身来自于一个实际项目,但同时又比较小巧, 我对其代码做了部分修改,去掉了一些业务相关的部分。 事件分发器通常是跟UI联系在一起的,UI中有多个组件,它们之间经常需要互相通信,当UI比较复杂,而 页面元素的组织又不够清晰的时候,事件的处理会非常麻烦。在本节的例子中,事件分发器为一个对象,UI组 件发出事件到事件分发器,也可以注册自己到分发器,当自己关心的事件到达时,进行响应。如果你熟悉设计 模式的话,会很快想到观察者模式,例子中的事件分发器正式使用了此模式。 var uikit = uikit || {}; uikit.event = uikit.event || {}; uikit.event.EventTypes = { EVENT_NONE : 0, EVENT_INDEX_CHANGE : 1, EVENT_LIST_DATA_READY : 2, EVENT_GRID_DATA_READY : 3 }; 定义一个名称空间uikit,并声明一个静态的常量:EventTypes,此变量定义了目前系统所支持的事件类型。 http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 118 / 149 页 uikit.event.JSEvent = Base.extend({ constructor : function(obj){ this.type = obj.type || uikit.event.EventTypes.EVENT_NONE; this.object = obj.data || {}; }, getType : function(){ return this.type; }, getObject : function(){ return this.object; } }); 定义事件类,事件包括类型和事件中包含的数据,通常为事件发生的点上的一些信息,比如点击一个表格的某 个单元格,可能需要将该单元格所在的行号和列号包装进事件的数据。 uikit.event.JSEventListener = Base.extend({ constructor : function(listener){ this.sense = listener.sense; this.handle = listener.handle || function(event){}; }, getSense : function(){ return this.sense; http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 119 / 149 页 } }); 定义事件监听器类,事件监听器包含两个属性,及监听器所关心的事件类型sense和当该类型的事件发生后要做 的动作handle。 uikit.event.JSEventDispatcher = function(){ if(uikit.event.JSEventDispatcher.singlton){ return uikit.event.JSEventDispatcher.singlton; } this.listeners = {}; uikit.event.JSEventDispatcher.singlton = this; this.post = function(event){ var handlers = this.listeners[event.getType()]; for(var index in handlers){ if(handlers[index].handle && typeof handlers[index].handle == "function") handlers[index].handle(event); } }; this.addEventListener = function(listener){ var item = listener.getSense(); var listeners = this.listeners[item]; if(listeners){ this.listeners[item].push(listener); }else{ http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 120 / 149 页 var hList = new Array(); hList.push(listener); this.listeners[item] = hList; } }; } uikit.event.JSEventDispatcher.getInstance = function(){ return new uikit.event.JSEventDispatcher(); }; 这里定义了一个单例的事件分发器,同一个系统中的任何组件都可以向此实例注册自己,或者发送事件到此实 例。事件分发器事实上需要为何这样一个数据结构: var listeners = { eventType.foo : [ {sense : "eventType.foo", handle : function(){doSomething();}} {sense : "eventType.foo", handle : function(){doSomething();}} {sense : "eventType.foo", handle : function(){doSomething();}} ], eventType.bar : [ {sense : "eventType.bar", handle : function(){doSomething();}} {sense : "eventType.bar", handle : function(){doSomething();}} {sense : "eventType.bar", handle : function(){doSomething();}} ],.. }; http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 121 / 149 页 当事件发生之后,分发器会找到该事件处理器的数组,然后依次调用监听器的handle方法进行相应。好了,到 此为止,我们已经有了事件分发器的基本框架了,下来,我们开始实现我们的组件(Component)。 组件要通信,则需要加入事件支持,因此可以抽取出一个类: uikit.component = uikit.component || {}; uikit.component.EventSupport = Base.extend({ constructor : function(){ }, raiseEvent : function(eventdef){ var e = new uikit.event.JSEvent(eventdef); uikit.event.JSEventDispatcher.getInstance().post(e); }, addActionListener : function(listenerdef){ var l = new uikit.event.JSEventListener(listenerdef); uikit.event.JSEventDispatcher.getInstance().addEventListener(l); } }); 继承了这个类的类具有事件支持的能力,可以raise事件,也可以注册监听器,这个EventSupport仅仅做了一个代 理,将实际的工作代理到事件分发器上。 uikit.component.ComponentBase = uikit.component.EventSupport.extend({ constructor: function(canvas) { http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 122 / 149 页 this.canvas = canvas; }, render : function(datamodel){} }); 定义所有的组件的基类,一般而言,组件需要有一个画布(canvas)的属性,而且组件需要有展现自己的能力,因 此需要实现render方法来画出自己来。 我们来看一个继承了ComponentBase的类JSList: uikit.component.JSList = uikit.component.ComponentBase.extend({ constructor : function(canvas, datamodel){ this.base(canvas); this.render(datamodel); }, render : function(datamodel){ var jqo = $(this.canvas); var text = ""; for(var p in datamodel.items){ text += datamodel.items[p] + ";"; } var item = $("").addClass("component"); item.text(text); item.click(function(){ jqo.find("div.selected").removeClass("selected"); $(this).addClass("selected"); http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 123 / 149 页 var idx = jqo.find("div").index($(".selected")[0]); var c = new uikit.component.ComponentBase(null); c.raiseEvent({ type : uikit.event.EventTypes.EVENT_INDEX_CHANGE, data : {index : idx} }); }); jqo.append(item); }, update : function(event){ var jqo = $(this.canvas); jqo.empty(); var dm = event.getObject().items; for(var i = 0; i < dm.length();i++){ var entity = dm.get(i).item; jqo.append(this.createItem({items : entity})); } }, createItem : function(datamodel){ var jqo = $(this.canvas); var text = datamodel.items; var item = $("").addClass("component"); item.text(text); item.click(function(){ jqo.find("div.selected").removeClass("selected"); $(this).addClass("selected"); var idx = jqo.find("div").index($(".selected")[0]); var c = new uikit.component.ComponentBase(null); c.raiseEvent({ type : uikit.event.EventTypes.EVENT_INDEX_CHANGE, http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 124 / 149 页 data : {index : idx} }); }); return item; }, getSelectedItemIndex : function(){ var jqo = $(this.canvas); var index = jqo.find("div").index($(".selected")[0]); return index; } }); 首先,我们的画布其实是一个共jQuery选择的选择器,选择到这个画布之后,通过jQuery则可以比较容易的在画 布上绘制组件。 在我们的实现中,数据与视图是分离的,我们通过定义这样的数据结构: {items : ["China", "Canada", "U.S.A", "U.K", "Uruguay"]}; 则可以render出如下图所示的List: http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 125 / 149 页 好,既然组件模型已经有了,事件分发器的框架也有了,相信你已经迫不及待的想要看看这些代码可以干点什 么了吧,再耐心一下,我们还要写一点代码: $(document).ready(function(){ var ldmap = new uikit.component.ArrayLike(dataModel); ldmap.addActionListener({ sense : uikit.event.EventTypes.EVENT_INDEX_CHANGE, handle : function(event){ var idx = event.getObject().index; uikit.component.EventGenerator.raiseEvent({ type : uikit.event.EventTypes.EVENT_GRID_DATA_READY, data : {rows : ldmap.get(idx).grid} }); } }); var list = new uikit.component.JSList("div#componentList", []); var grid = new uikit.component.JSGrid("div#conditionsTable table tbody"); list.addActionListener({ sense : uikit.event.EventTypes.EVENT_LIST_DATA_READY, handle : function(event){ list.update(event); } http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 126 / 149 页 }); grid.addActionListener({ sense : uikit.event.EventTypes.EVENT_GRID_DATA_READY, handle : function(event){ grid.update(event); } }); uikit.component.EventGenerator.raiseEvent({ type : uikit.event.EventTypes.EVENT_LIST_DATA_READY, data : {items : ldmap} }); var colorPanel = new uikit.component.Panel("div#colorPanel"); colorPanel.addActionListener({ sense : uikit.event.EventTypes.EVENT_INDEX_CHANGE, handle : function(event){ var idx = parseInt(10*Math.random()) colorPanel.update(idx); } }); }); 使用jQuery,我们在文档加载完毕之后,新建了两个对象List和Grid,通过点击List上的条目,如果这些条 目在List的模型上索引发生变化,则会发出EVENT_INDEX_CHAGE事件,接收到这个事件的组件或 者DataModel会做出相应的响应。在本例中,ldmap在接收到EVENT_INDEX_CHANGE事件后,会组织数据, 并发出EVENT_GRID_DATA_READY事件,而Grid接收到这个事件后,根据事件对象上绑定的数据模型来更新 自己的UI。 上例中的类继承关系如下图: http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 127 / 149 页 图 事件分发器类层次 应该注意的是,在绑定完监听器之后,我们手动的触发了EVENT_LIST_DATA_READY事件,来通知List可 以绘制自身了: uikit.component.EventGenerator.raiseEvent({ type : uikit.event.EventTypes.EVENT_LIST_DATA_READY, data : {items : ldmap} }); http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 128 / 149 页 在实际的应用中,这个事件可能是用户在页面上点击一个按钮,或者一个Ajax请求的返回,等等,一旦事件 监听器注册完毕,程序就已经就绪,等待异步事件并响应。 点击List中的元素China,Grid中的数据发生变化 点击Canada,Grid中的数据同样发生相应的变化: 由于List和Grid的数据是关联在一起的,他们的数据结构具有下列的结构: var dataModel = [{ item: "China", grid: [ [{ dname: "Beijing", type: "string" }, { dname: "ProductA", type: "string" }, { http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 129 / 149 页 dname: 1000, type: "number" }], [{ dname: "ShangHai", type: "string" }, { dname: "ProductB", type: "string" }, { dname: 23451, type: "number" }], [{ dname: "GuangZhou", type: "string" }, { dname: "ProductB", type: "string" }, { dname: 87652, type: "number" }] ] },... ]; http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 130 / 149 页 一个组件可以发出多种事件,同时也可以监听多种事件,所以我们可以为List的下标改变事件注册另一个监听 器,监听器为一个简单组件Panel,当接收到这个事件后,该Panel会根据一个随机的颜色来重置自身的背景 色(注意在List和Grid下面的灰色Panel): 附:由于作者本身水平有限,文中难免有纰漏错误等,或者语言本身有不妥当之处,欢迎及时 指正,提出建 议,参与讨论,谢谢大家! 附件下载: • jsed.zip (43.7 KB) • dl.javaeye.com/topics/download/de98bf4d-5877-32c9-8b47-32b5fee04285 http://abruzzi.javaeye.com 1.9 JavaScript内核系列 第8章 面向对象的JavaScript(下) 第 131 / 149 页 1.10 JavaScript内核系列 第9章 函数式的Javascript 发表时间: 2010-05-13 第九章 函数式的Javascript 要说JavaScript和其他较为常用的语言最大的不同是什么,那无疑就是JavaScript是函数式的语言,函数式 语言的特点如下: 函数为第一等的元素,即人们常说的一等公民。就是说,在函数式编程中,函数是不依赖于其他对象而独立 存在的(对比与Java,函数必须依赖对象,方法是对象的方法)。 函数可以保持自己内部的数据,函数的运算对外部无副作用(修改了外部的全局变量的状态等),关于函数可 以保持自己内部的数据这一特性,称之为闭包。我们可以来看一个简单的例子: var outter = function(){ var x = 0; return function(){ return x++; } } var a = outter(); print(a()); print(a()); var b = outter(); print(b()); print(b()); 运行结果为: http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 132 / 149 页 0 1 0 1 变量a通过闭包引用outter的一个内部变量,每次调用a()就会改变此内部变量,应该注意的是,当调 用a时,函数outter已经返回了,但是内部变量x的值仍然被保持。而变量b也引用了outter,但是是一个不同的 闭包,所以b开始引用的x值不会随着a()被调用而改变,两者有不同的实例,这就相当于面向对象中的不同实例 拥有不同的私有属性,互不干涉。 由于JavaScript支持函数式编程,我们随后会发现JavaScript许多优美而强大的能力,这些能力得力于以下 主题:匿名函数,高阶函数,闭包及柯里化等。熟悉命令式语言的开发人员可能对此感到陌生,但是使用lisp, scheme等函数式语言的开发人员则觉得非常亲切。 9.1匿名函数 匿名函数在函数式编程语言中,术语成为lambda表达式。顾名思义,匿名函数就是没有名字的函数,这个是与 日常开发中使用的语言有很大不同的,比如在C/Java中,函数和方法必须有名字才可以被调用。 在JavaScript中,函数可以没有名字,而且这一个特点有着非凡的意义: function func(){ //do something } var func = function(){ //do something } 这两个语句的意义是一样的,它们都表示,为全局对象添加一个属性func,属性func的值为一个函数对象,而 这个函数对象是匿名的。匿名函数的用途非常广泛,在JavaScript代码中,我们经常可以看到这样的代码: http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 133 / 149 页 var mapped = [1, 2, 3, 4, 5].map(function(x){return x * 2}); print(mapped); 应该注意的是,map这个函数的参数是一个匿名函数,你不需要显式的声明一个函数,然后将其作为参数传 入,你只需要临时声明一个匿名的函数,这个函数被使用之后就别释放了。在高阶函数这一节中更可以看到这 一点。 9.2高阶函数 通常,以一个或多个函数为参数的函数称之为高阶函数。高阶函数在命令式编程语言中有对应的实现,比如C语 言中的函数指针,Java中的匿名类等,但是这些实现相对于命令式编程语言的其他概念,显得更为复杂。 9.2.1 JavaScript中的高阶函数 Lisp中,对列表有一个map操作,map接受一个函数作为参数,map对列表中的所有元素应用该函数, 最后返回处理后的列表(有的实现则会修改原列表),我们在这一小节中分别用JavaScript/C/Java来对map操作 进行实现,并对这些实现方式进行对比: Array.prototype.map = function(func /*, obj */){ var len = this.length; //check the argument if(typeof func != "function"){ throw new Error("argument should be a function!"); } var res = []; var obj = arguments[1]; for(var i = 0; i < len; i++){ //func.call(), apply the func to this[i] res[i] = func.call(obj, this[i], i, this); } http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 134 / 149 页 return res; } 我们对JavaScript的原生对象Array的原型进行扩展,函数map接受一个函数作为参数,然后对数组的每一个元 素都应用该函数,最后返回一个新的数组,而不影响原数组。由于map函数接受的是一个函数作为参数,因 此map是一个高阶函数。我们进行测试如下: function double(x){ return x * 2; } [1, 2, 3, 4, 5].map(double);//return [2, 4, 6, 8, 10] 应该注意的是double是一个函数。根据上一节中提到的匿名函数,我们可以为map传递一个匿名函数: var mapped = [1, 2, 3, 4, 5].map(function(x){return x * 2}); print(mapped); 这个示例的代码与上例的作用是一样的,不过我们不需要显式的定义一个double函数,只需要为map函数传递 一个“可以将传入参数乘2并返回”的代码块即可。再来看一个例子: [ {id : "item1"}, http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 135 / 149 页 {id : "item2"}, {id : "item3"} ].map(function(current){ print(current.id); }); 将会打印: item1 item2 item3 也就是说,这个map的作用是将传入的参数(处理器)应用在数组中的每个元素上,而不关注数组元素的数据类 型,数组的长度,以及处理函数的具体内容。 9.2.2 C语言中的高阶函数 C语言中的函数指针,很容易实现一个高阶函数。我们还以map为例,说明在C语言中如何实现: //prototype of function void map(int* array, int length, int (*func)(int)); map函数的第三个参数为一个函数指针,接受一个整型的参数,返回一个整型参数,我们来看看其实现: //implement of function map void map(int* array, int length, int (*func)(int)){ int i = 0; http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 136 / 149 页 for(i = 0; i < length; i++){ array[i] = func(array[i]); } } 我们在这里实现两个小函数,分别计算传入参数的乘2的值,和乘3的值,然后进行测试: int twice(int num) { return num * 2; } int triple(int num){ return num * 3; } //function main int main(int argc, char** argv){ int array[5] = {1, 2, 3, 4, 5}; int i = 0; int len = 5; //print the orignal array printArray(array, len); //mapped by twice map(array, len, twice); printArray(array, len); //mapped by twice, then triple map(array, len, triple); printArray(array, len); return 0; } http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 137 / 149 页 运行结果如下: 1 2 3 4 5 2 4 6 8 10 6 12 18 24 30 应该注意的是map的使用方法,如map(array, len, twice)中,最后的参数为twice,而twice为一个函数。因 为C语言中,函数的定义不能嵌套,因此不能采用诸如JavaScript中的匿名函数那样的简洁写法。 虽然在C语言中可以通过函数指针的方式来实现高阶函数,但是随着高阶函数的“阶”的增高,指针层次 势必要跟着变得很复杂,那样会增加代码的复杂度,而且由于C语言是强类型的,因此在数据类型方面必然有很 大的限制。 9.2.3 Java中的高阶函数 Java中的匿名类,事实上可以理解成一个教笨重的闭包(可执行单元),我们可以通过Java的匿名类来实现上述 的map操作,首先,我们需要一个对函数的抽象: interface Function{ int execute(int x); } 我们假设Function接口中有一个方法execute,接受一个整型参数,返回一个整型参数,然后我们在类List中, 实现map操作: private int[] array; public List(int[] array){ this.array = array; http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 138 / 149 页 } public void map(Function func){ for(int i = 0, len = this.array.length; i < len; i++){ this.array[i] = func.execute(this.array[i]); } } map接受一个实现了Function接口的类的实例,并调用这个对象上的execute方法来处理数组中的每一个元 素。我们这里直接修改了私有成员array,而并没有创建一个新的数组。好了,我们来做个测试: public static void main(String[] args){ List list = new List(new int[]{1, 2, 3, 4, 5}); list.print(); list.map(new Function(){ public int execute(int x){ return x * 2; } }); list.print(); list.map(new Function(){ public int execute(int x){ return x * 3; } }); list.print(); } 同前边的两个例子一样,这个程序会打印: http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 139 / 149 页 1 2 3 4 5 2 4 6 8 10 6 12 18 24 30 灰色背景色的部分即为创建一个匿名类,从而实现高阶函数。很明显,我们需要传递给map的是一个可以执 行execute方法的代码。而由于Java是命令式的编程语言,函数并非第一位的,函数必须依赖于对象,附属于对 象,因此我们不得不创建一个匿名类来包装这个execute方法。而在JavaScript中,我们只需要传递函数本身即 可,这样完全合法,而且代码更容易被人理解。 9.3闭包与柯里化 闭包和柯里化都是JavaScript经常用到而且比较高级的技巧,所有的函数式编程语言都支持这两个概念,因此, 我们想要充分发挥出JavaScript中的函数式编程特征,就需要深入的了解这两个概念,我们在第七章中详细的讨 论了闭包及其特征,闭包事实上更是柯里化所不可缺少的基础。 9.3.1柯里化的概念 闭包的我们之前已经接触到,先说说柯里化。柯里化就是预先将函数的某些参数传入,得到一个简单的函数, 但是预先传入的参数被保存在闭包中,因此会有一些奇特的特性。比如: var adder = function(num){ return function(y){ return num + y; } } var inc = adder(1); var dec = adder(-1); http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 140 / 149 页 这里的inc/dec两个变量事实上是两个新的函数,可以通过括号来调用,比如下例中的用法: //inc, dec现在是两个新的函数,作用是将传入的参数值(+/-)1 print(inc(99));//100 print(dec(101));//100 print(adder(100)(2));//102 print(adder(2)(100));//102 9.3.2柯里化的应用 根据柯里化的特性,我们可以写出更有意思的代码,比如在前端开发中经常会遇到这样的情况,当请求从服 务端返回后,我们需要更新一些特定的页面元素,也就是局部刷新的概念。使用局部刷新非常简单,但是代码 很容易写成一团乱麻。而如果使用柯里化,则可以很大程度上美化我们的代码,使之更容易维护。我们来看一 个例子: //update会返回一个函数,这个函数可以设置id属性为item的web元素的内容 function update(item){ return function(text){ $("div#"+item).html(text); } } //Ajax请求,当成功是调用参数callback function refresh(url, callback){ var params = { type : "echo", data : "" }; http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 141 / 149 页 $.ajax({ type:"post", url:url, cache:false, async:true, dataType:"json", data:params, //当异步请求成功时调用 success: function(data, status){ callback(data); }, //当请求出现错误时调用 error: function(err){ alert("error : "+err); } }); } refresh("action.do?target=news", update("newsPanel")); refresh("action.do?target=articles", update("articlePanel")); refresh("action.do?target=pictures", update("picturePanel")); 其中,update函数即为柯里化的一个实例,它会返回一个函数,即: update("newsPanel") = function(text){ $("div#newsPanel").html(text); } 由于update(“newsPanel”)的返回值为一个函数,需要的参数为一个字符串,因此在refresh的Ajax调用中, 当success时,会给callback传入服务器端返回的数据信息,从而实现newsPanel面板的刷新,其他的文章面 板articlePanel,图片面板picturePanel的刷新均采取这种方式,这样,代码的可读性,可维护性均得到了提高。 http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 142 / 149 页 9.4一些例子 9.4.1函数式编程风格 通常来讲,函数式编程的谓词(关系运算符,如大于,小于,等于的判断等),以及运算(如加减乘数等)都会 以函数的形式出现,比如: a > b 通常表示为: gt(a, b)//great than 因此,可以首先对这些常见的操作进行一些包装,以便于我们的代码更具有“函数式”风格: function abs(x){ return x>0?x:-x;} function add(a, b){ return a+b; } function sub(a, b){ return a-b; } function mul(a, b){ return a*b; } function div(a, b){ return a/b; } function rem(a, b){ return a%b; } function inc(x){ return x + 1; } function dec(x){ return x - 1; } function equal(a, b){ return a==b; } function great(a, b){ return a>b; } function less(a, b){ return a0; } function sin(x){ return Math.sin(x); } function cos(x){ return Math.cos(x); } http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 143 / 149 页 如果我们之前的编码风格是这样: // n*(n-1)*(n-2)*...*3*2*1 function factorial(n){ if(n == 1){ return 1; }else{ return n * factorial(n - 1); } } 在函数式风格下,就应该是这样了: function factorial(n){ if(equal(n, 1)){ return 1; }else{ return mul(n, factorial(dec(n))); } } 函数式编程的特点当然不在于编码风格的转变,而是由更深层次的意义。比如,下面是另外一个版本的阶乘实 现: http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 144 / 149 页 /* * product <- counter * product * counter <- counter + 1 * */ function factorial(n){ function fact_iter(product, counter, max){ if(great(counter, max)){ return product; }else{ fact_iter(mul(counter, product), inc(counter), max); } } return fact_iter(1, 1, n); } 虽然代码中已经没有诸如+/-/*//之类的操作符,也没有>,<,==,之类的谓词,但是,这个函数仍然算不上具有 函数式编程风格,我们可以改进一下: function factorial(n){ return (function factiter(product, counter, max){ if(great(counter, max)){ return product; }else{ return factiter(mul(counter, product), inc(counter), max); } })(1, 1, n); } factorial(10); http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 145 / 149 页 通过一个立即运行的函数factiter,将外部的n传递进去,并立即参与计算,最终返回运算结果。 9.4.2 Y-结合子 提到递归,函数式语言中还有一个很有意思的主题,即:如果一个函数是匿名函数,能不能进行递归操作 呢?如何可以,怎么做?我们还是来看阶乘的例子: function factorial(x){ return x == 0 ? 1 : x * factorial(x-1); } factorial函数中,如果x值为0,则返回1,否则递归调用factorial,参数为x减1,最后当x等于0时进行规约,最 终得到函数值(事实上,命令式程序语言中的递归的概念最早即来源于函数式编程中)。现在考虑:将factorial定 义为一个匿名函数,那么在函数内部,在代码x*factorial(x-1)的地方,这个factorial用什么来替代呢? lambda演算的先驱们,天才的发明了一个神奇的函数,成为Y-结合子。使用Y-结合子,可以做到对匿名函 数使用递归。关于Y-结合子的发现及推导过程的讨论已经超出了本部分的范围,有兴趣的读者可以参考附录中 的资料。我们来看看这个神奇的Y-结合子: var Y = function(f) { return (function(g) { return g(g); })(function(h) { return function() { return f(h(h)).apply(null, arguments); }; }); }; http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 146 / 149 页 我们来看看如何运用Y-结合子,依旧是阶乘这个例子: var factorial = Y(function(func){ return function(x){ return x == 0 ? 1 : x * func(x-1); } }); factorial(10); 或者: Y(function(func){ return function(x){ return x == 0 ? 1 : x * func(x-1); } })(10); 不要被上边提到的Y-结合子的表达式吓到,事实上,在JavaScript中,我们有一种简单的方法来实现Y-结合子: var fact = function(x){ return x == 0 : 1 : x * arguments.callee(x-1); } fact(10); http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 147 / 149 页 或者: (function(x){ return x == 0 ? 1 : x * arguments.callee(x-1); })(10);//3628800 其中,arguments.callee表示函数的调用者,因此省去了很多复杂的步骤。 9.4.3其他实例 下面的代码则颇有些“开发智力”之功效: //函数的不动点 function fixedPoint(fx, first){ var tolerance = 0.00001; function closeEnough(x, y){return less( abs( sub(x, y) ), tolerance)}; function Try(guess){//try 是javascript中的关键字,因此这个函数名为大写 var next = fx(guess); //print(next+" "+guess); if(closeEnough(guess, next)){ return next; }else{ return Try(next); } }; return Try(first); } http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 148 / 149 页 // 数层嵌套函数, function sqrt(x){ return fixedPoint( function(y){ return function(a, b){ return div(add(a, b),2);}(y, div(x, y)); }, 1.0); } print(sqrt(100)); fiexedPoint求函数的不动点,而sqrt计算数值的平方根。这些例子来源于《计算机程序的构造和解释》,其中 列举了大量的计算实例,不过该书使用的是scheme语言,在本书中,例子均被翻译为JavaScript。 附:由于作者本身水平有限,文中难免有纰漏错误等,或者语言本身有不妥当之处,欢迎及时 指正,提出建 议,参与讨论,谢谢大家! http://abruzzi.javaeye.com 1.10 JavaScript内核系列 第9章 函数式的Javascript 第 149 / 149 页
贡献于2012-12-09