Heim >Web-Frontend >js-Tutorial >JavaScript 函数式编程实践(来自IBM)_javascript技巧

JavaScript 函数式编程实践(来自IBM)_javascript技巧

PHP中文网
PHP中文网Original
2016-05-16 18:24:181146Durchsuche

说到函数式编程,人们的第一印象往往是其学院派,晦涩难懂,大概只有那些蓬头散发,不修边幅,甚至有些神经质的大学教授们才会用的编程方式。

函数式编程简介
说到函数式编程,人们的第一印象往往是其学院派,晦涩难懂,大概只有那些蓬头散发,不修边幅,甚至有些神经质的大学教授们才会用的编程方式。这可能在历史上的某个阶段的确如此,但是近来函数式编程已经在实际应用中发挥着巨大作用了,而更有越来越多的语言不断的加入诸如 闭包,匿名函数等的支持,从某种程度上来讲,函数式编程正在逐步“同化”命令式编程。
函数式编程思想的源头可以追溯到 20 世纪 30 年代,数学家阿隆左 . 丘奇在进行一项关于问题的可计算性的研究,也就是后来的 lambda 演算。lambda 演算的本质为 一切皆函数,函数可以作为另外一个函数的输出或者 / 和输入,一系列的函数使用最终会形成一个表达式链,这个表达式链可以最终求得一个值,而这个过程,即为计算的本质。
然而,这种思想在当时的硬件基础上很难实现,历史最终选择了同丘奇的 lambda 理论平行的另一种数学理论:图灵机作为计算理论,而采取另一位科学家冯 . 诺依曼的计算机结构,并最终被实现为硬件。由于第一台计算机即为冯 . 诺依曼的程序存储结构,因此运行在此平台的程序也继承了这种基因,程序设计语言如 C/Pascal 等都在一定程度上依赖于此体系。
到了 20 世纪 50 年代,一位 MIT 的教授 John McCarthy 在冯 . 诺依曼体系的机器上成功的实现了 lambda 理论,取名为 LISP(LISt Processor), 至此函数式编程语言便开始活跃于计算机科学领域。
函数式编程语言特性
在函数式编程语言中,函数是第一类的对象,也就是说,函数 不依赖于任何其他的对象而可以独立存在,而在面向对象的语言中,函数 ( 方法 ) 是依附于对象的,属于对象的一部分。这一点 j 决定了函数在函数式语言中的一些特别的性质,比如作为传出 / 传入参数,作为一个普通的变量等。
区别于命令式编程语言,函数式编程语言具有一些专用的概念,我们分别进行讨论:
匿名函数
在函数式编程语言中,函数是可以没有名字的,匿名函数通常表示:“可以完成某件事的一块代码”。这种表达在很多场合是有用的,因为我们有时需要用函数完成某件事,但是这个函数可能只是临时性的,那就没有理由专门为其生成一个顶层的函数对象。比如:

清单 1. map 函数 

function map(array, func){ 
var res = []; 
for ( var i = 0, len = array.length; i < len; i++){ 
res.push(func(array[i])); 
} 
return res; 
} 
var mapped = map([1, 3, 5, 7, 8], function (n){ 
return n = n + 1; 
}); 
print(mapped);

运行这段代码,将会打印:
2,4,6,8,9// 对数组 [1,3,5,7,8] 中每一个元素加 1


注意 map 函数的调用,map 的第二个参数为一个函数,这个函数对 map 的第一个参数 ( 数组 ) 中的每一个都有作用,但是对于 map 之外的代码可能没有任何意义,因此,我们无需为其专门定义一个函数,匿名函数已经足够。
柯里化
柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这句话有点绕口,我们可以通过例子来帮助理解:
清单 2. 柯里化函数 

function adder(num){ 
return 
function (x){ 
return num + x; 
} 
} 
var add5 = adder(5); 
var add6 = adder(6); 
print(add5(1)); 
print(add6(1));

结果为:
6
7
比较有意思的是:函数 adder 接受一个参数,并返回一个函数,这个返回的函数可以被预期的那样被调用。变量 add5 保持着 adder(5) 返回的函数,这个函数可以接受一个参数,并返回参数与 5 的和。
柯里化在 DOM 的回调中非常有用,我们将在下面的小节中看到。
高阶函数
高阶函数即为对函数的进一步抽象,事实上,我们在匿名函数小节提到的 map 函数即为一种高阶函数,在很多的函数式编程语言中均有此函数。map(array, func) 的表达式已经表明,将 func 函数作用于 array 中的每一个元素,最终返回一个新的 array,应该注意的是,map 对 array 和 func 的实现是没有任何预先的假设的,因此称之为“高阶”函数:

清单 3. 高阶函数 

function map(array, func){ 
var res = []; 
for ( var i = 0, len = array.length; i < len; i++){ 
res.push(func(array[i])); 
} 
return res; 
} 
var mapped = map([1, 3, 5, 7, 8], function (n){ 
return n = n + 1; 
}); 
print(mapped); 
var mapped2 = map(["one", "two", "three", "four"], 
function (item){ 
return "("+item+")"; 
}); 
print(mapped2);

将会打印如下结果:
2,4,6,8,9
(one),(two),(three),(four)// 为数组中的每个字符串加上括号

mapped 和 mapped2 均调用了 map,但是得到了截然不同的结果,因为 map 的参数本身已经进行了一次抽象,map 函数做的是第二次抽象,高阶的“阶”可以理解为抽象的层次。
JavaScript 中的函数式编程
JavaScript 是一门被误解甚深的语言,由于早期的 Web 开发中,充满了大量的 copy-paste 代码,因此平时可以见到的 JavaScript 代码质量多半不高,而且 JavaScript 代码总是很飞动的不断闪烁的 gif 广告,限制网页内容的复制等联系在一起的,因此包括 Web 开发者在内的很多人根本不愿意去学习 JavaScript。
这种情形在 Ajax 复兴时得到了彻底的扭转,Google Map,Gmail 等 Ajax 应用的出现使人们惊叹:原来 JavaScript 还可以做这样的事!很快,大量优秀的 JavaScript/Ajax 框架不断出现,比如 Dojo,Prototype,jQuery,ExtJS 等等。这些代码在给页面带来绚丽的效果的同时,也让开发者看到函数式语言代码的优雅。
函数式编程风格
在 JavaScript 中,函数本身为一种特殊对象,属于顶层对象,不依赖于任何其他的对象而存在,因此可以将函数作为传出 / 传入参数,可以存储在变量中,以及一切其他对象可以做的事情 ( 因为函数就是对象 )。
JavaScript 被称为有着 C 语法的 LISP,LISP 代码的一个显著的特点是大量的括号以及前置的函数名,比如:

清单 4. LISP 中的加法
(+ 1 3 4 5 6 7)

加号在 LISP 中为一个函数,这条表达式的意思为将加号后边的所有数字加起来,并将值返回,JavaScript 可以定义同样的求和函数:

清单 5. JavaScript 中的求和 

function sum(){ 
var res = 0; 
for ( var i = 0, len = arguments.length; i < len; i++){ 
res += parseInt(arguments[i]); 
} 
return res; 
} 
print(sum(1,2,3)); 
print(sum(1,2,3,4,6,7,8));

运行此段代码,得到如下结果:
6
31

如果要完全模拟函数式编码的风格,我们可以定义一些诸如:
清单 6. 一些简单的函数抽象 

function add(a, b){ return a+b; } 
function sub(a, b){ return a-b; } 
function mul(a, b){ return a*b; } 
function p(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 a<b; }

这样的小函数以及谓词,那样我们写出的代码就更容易被有函数式编程经验的人所接受:
清单 7. 函数式编程风格 

// 修改之前的代码 
function factorial(n){ 
if (n == 1){ 
return 1; 
} else { 
return factorial(n - 1) * n; 
} 
} 
// 更接近“函数式”编程风格的代码 
function factorial(n){ 
if (equal(n, 1)){ 
return 1; 
} else { 
return mul(n, factorial(dec(n))); 
} 
}

闭包及其使用
闭包是一个很有趣的主题,当在一个函数 outter 内部定义另一个函数 inner,而 inner 又引用了 outter 作用域内的变量,在 outter 之外使用 inner 函数,则形成了闭包。描述起来虽然比较复杂,在实际编程中却经常无意的使用了闭包特性。
清单 8. 一个闭包的例子 

function outter(){ 
var n = 0; 
return 
function (){ 
return n++; 
} 
} 
var o1 = outter(); 
o1();//n == 0 
o1();//n == 1 
o1();//n == 2 
var o2 = outter(); 
o2();//n == 0 
o2();//n == 1

匿名函数 function(){return n++;} 中包含对 outter 的局部变量 n 的引用,因此当 outter 返回时,n 的值被保留 ( 不会被垃圾回收机制回收 ),持续调用 o1(),将会改变 n 的值。而 o2 的值并不会随着 o1() 被调用而改变,第一次调用 o2 会得到 n==0 的结果,用面向对象的术语来说,就是 o1 和 o2 为不同的 实例,互不干涉。
总的来说,闭包很简单,不是吗?但是,闭包可以带来很多好处,比如我们在 Web 开发中经常用到的:
清单 9. jQuery 中的闭包 

var con = $("p#con"); 
setTimeout( function (){ 
con.css({background:"gray"}); 
}, 2000);

上边的代码使用了 jQuery 的选择器,找到 id 为 con 的 p 元素,注册计时器,当两秒中之后,将该 p 的背景色设置为灰色。这个代码片段的神奇之处在于,在调用了 setTimeout 函数之后,con 依旧被保持在函数内部,当两秒钟之后,id 为 con 的 p 元素的背景色确实得到了改变。应该注意的是,setTimeout 在调用之后已经返回了,但是 con 没有被释放,这是因为 con 引用了全局作用域里的变量 con。
使用闭包可以使我们的代码更加简洁,关于闭包的更详细论述可以在参考信息中找到。由于闭包的特殊性,在使用闭包时一定要小心,我们再来看一个容易令人困惑的例子:
清单 10. 错误的使用闭包 

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); 
} 
}

上边的代码片段很简单,将多个这样的 JavaScript 对象存入 outter 数组:

清单 11. 匿名对象 

{ 
no : Number, 
text : String, 
invoke : function (){ 
// 打印自己的 no 字段 
} 
}

我们来运行这段代码:
清单 12. 错误的结果 

clouseTest();// 调用这个函数,向 outter 数组中添加对象 
for ( var i = 0, len = outter.length; i < len; i++){ 
outter[i].invoke(); 
}

出乎意料的是,这段代码将打印:
4
4
4
4
而不是 1,2,3,4 这样的序列。让我们来看看发生了什么事,每一个内部变量 x 都填写了自己的 no,text,invoke 字段,但是 invoke 却总是打印最后一个 i。原来,我们为 invoke 注册的函数为:

清单 13. 错误的原因 

function invoke(){ 
print(i); }

每一个 invoke 均是如此,当调用 outter[i].invoke 时,i 的值才会被去到,由于 i 是闭包中的局部变量,for 循环最后退出时的值为 4,因此调用 outter 中的每个元素都会得到 4。因此,我们需要对这个函数进行一些改造:

清单 14. 正确的使用闭包 

var outter = []; 
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); 
} 
}

通过将函数 柯里化,我们这次为 outter 的每个元素注册的其实是这样的函数: 

//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);}

这样,就可以得到正确的结果了。
实际应用中的例子
好了,理论知识已经够多了,我们下面来看看现实世界中的 JavaScript 函数式编程。有很多人为使 JavaScript 具有面向对象风格而做出了很多努力 (JavaScript 本身具有 可编程性),事实上,面向对象并非必须,使用函数式编程或者两者混合使用可以使代码更加优美,简洁。
jQuery 是一个非常优秀 JavaScript/Ajax 框架,小巧,灵活,具有插件机制,事实上,jQuery 的插件非常丰富,从表达验证,客户端图像处理,UI,动画等等。而 jQuery 最大的特点正如其宣称的那样,改变了人们编写 JavaScript 代码的风格。
优雅的 jQuery
有经验的前端开发工程师会发现,平时做的最多的工作有一定的模式:选择一些 DOM 元素,然后将一些规则作用在这些元素上,比如修改样式表,注册事件处理器等。因此 jQuery 实现了完美的 CSS 选择器,并提供跨浏览器的支持:
清单 15. jQuery 选择器 

var cons = $("p.note");// 找出所有具有 note 类的 p 
var con = $("p#con");// 找出 id 为 con 的 p 元素 
var links = $("a");// 找出页面上所有的链接元素

当然,jQuery 的选择器规则非常丰富,这里要说的是:用 jQuery 选择器选择出来的 jQuery 对象本质上是一个 List,正如 LISP 语言那样,所有的函数都是基于 List 的。
有了这个 List,我们可以做这样的动作:
清单 16. jQuery 操作 jQuery 对象 (List) 

cons.each( function (index){ 
$( this ).click( function (){ 
//do something with the node 
}); 
});

想当与对 cons 这个 List中的所有元素使用 map( 还记得我们前面提到的 map 吗? ),操作结果仍然为一个 List。我们可以任意的扩大 / 缩小这个列表,比如:
清单 17. 扩大 / 缩小 jQuery 集合 

cons.find("span.title");// 在 p.note 中进行更细的筛选 
cons.add("p.warn");// 将 p.note 和 p.warn 合并起来 
cons.slice(0, 5);// 获取 cons 的一个子集

现在我们来看一个小例子,假设有这样一个页面:
清单 18. 页面的 HTML 结构 

<p class="note"> 
<span class="title">Hello, world</span> 
</p> 
<p class="note"> 
<span class="title">345</span> 
</p> 
<p class="note"> 
<span class="title">Hello, world</span> 
</p> 
<p class="note"> 
<span class="title">67</span> 
</p> 
<p class="note"> 
<span class="title">483</span> 
</p>

效果如下:
图 1. 过滤之前的效果
我们通过 jQuery 对包装集进行一次过滤,jQuery 的过滤函数可以使得选择出来的列表对象只保留符合条件的,在这个例子中,我们保留这样的 p,当且仅当这个 p 中包含一个类名为 title 的 span,并且这个 span 的内容为数字:
清单 19. 过滤集合 

var cons = $("p.note").hide();// 选择 note 类的 p, 并隐藏 
cons.filter( function (){ 
return $( this ).find("span.title").html().match(/^\d+$/); 
}).show();

效果如下图所示:
图 2. 过滤之后的效果
我们再来看看 jQuery 中对数组的操作 ( 本质上来讲,JavaScript 中的数组跟 List 是很类似的 ),比如我们在前面的例子中提到的 map 函数,过滤器等:
清单 20. jQuery 对数组的函数式操作 

var mapped = $.map([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 
function (n){ 
return n + 1; 
}); 
var greped = $.grep([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 
function (n){ 
return n % 2 == 0; 
});

mapped 将被赋值为 :
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
而 greped 则为:
[2, 4, 6, 8, 10]
我们再来看一个更接近实际的例子:
清单 21. 一个页面刷新的例子 

function update(item){ 
return 
function (text){ 
$("p#"+item).html(text); 
} 
} 
function refresh(url, callback){ 
var params = { 
type : "echo", 
data : "" 
}; 
$.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/op=1", update("content1")); 
refresh("action.do/op=2", update("content2")); 
refresh("action.do/op=3", update("content3"));

首先声明一个柯里化的函数 update,这个函数会将传入的参数作为选择器的 id,并更新这个 p 的内容 (innerHTML)。然后声明一个函数 refresh,refresh 接受两个参数,第一个参数为服务器端的 url,第二个参数为一个回调函数,当服务器端成功返回时,调用该函数。
然后我们陆续调用三次 refresh,每次的 url 和 id 都不同,这样可以将 content1,content2,conetent3 的内容通过异步方式更新。这种模式在实际的编程中相当有效,因为关于如何与服务器通信,以及如果选取页面内容的部分被很好的抽象成函数,现在我们需要做的就是将 url 和 id 传递给 refresh,即可完成需要的动作。函数式编程在很大程度上降低了这个过程的复杂性,这正是我们选择使用该思想的最终原因。
结束语
实际的应用中,不会囿于函数式或者面向对象,通常是两者混合使用,事实上,很多主流的面向对象语言都在不断的完善自己,比如加入一些函数式编程语言的特征等,JavaScript 中,这两者得到了良好的结合,代码不但可以非常简单,优美,而且更易于调试。
文中仅仅提到 jQuery 特征的一小部分,如果感兴趣,则可以在参考资料中找到更多的链接,jQuery 非常的流行,因此你可以找到很多论述如何使用它的文章。 

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn