関数型プログラミングというと、人々の第一印象は、学術的で曖昧なものであることが多く、おそらく、だらしない、無精な、さらには神経質な大学教授だけが使用していると思われます。
関数型プログラミング入門
関数型プログラミングといえば、学術的でわかりにくいという第一印象を持たれることが多く、髪もボサボサで見た目もボサボサでプログラミングもできる人だけでしょう。一部の神経質な大学教授のみが使用する方法。これは歴史のある段階では真実かもしれませんが、最近では関数型プログラミングが実用的なアプリケーションで大きな役割を果たしており、ますます多くの言語がクロージャや匿名関数などのサポートを追加し続けています。関数型プログラミングは、命令型プログラミングを徐々に「同化」させています。
関数型プログラミングのアイデアの起源は、数学者のアーロン ゾーイ チャーチが後にラムダ計算として知られる問題の計算可能性に関する研究を行っていた 1930 年代に遡ります。ラムダ計算の本質は、関数が別の関数の出力または入力として使用できることです。この式チェーンは最終的に値を取得します。このプロセス、それがコンピューティングの本質です。
しかし、このアイデアは当時のハードウェアに基づいて実現するのは困難でした。歴史は最終的に、チャーチのラムダ理論と並行する別の数学理論、つまりチューリング マシンをコンピューティング理論として選択し、別の科学者フェンのコンピューター アーキテクチャが最終的に実装されました。ハードウェアとして。最初のコンピューターはフォン・ノイマンのプログラム記憶構造であったため、このプラットフォーム上で実行されるプログラムも、C/Pascal などのプログラミング言語はすべてこのシステムにある程度依存しています。
1950年代にMIT教授のジョン・マッカーシーがフォン・ノイマン系マシンにラムダ理論を実装することに成功し、それをLISP(LIStプロセッサ)と名付けて以来、関数型プログラミング言語がコンピュータの分野で活発になってきました。科学。
関数型プログラミング言語の機能
関数型プログラミング言語では、関数は第一級のオブジェクトです。つまり、関数は他のオブジェクトに依存せず、オブジェクト指向では独立して存在できます。言語、関数 (メソッド) はオブジェクトに付属しており、オブジェクトの一部です。この j は、出力/受信パラメーターであること、通常の変数であることなど、関数型言語における関数の特別なプロパティを決定します。
命令型プログラミング言語とは異なり、関数型プログラミング言語にはいくつかの特別な概念があります。これについては別途説明します。
匿名関数
関数型プログラミング言語では、関数が名前によって存在しない場合があります。 、匿名関数は通常、「何かを実行するコードの一部」を意味します。この式は多くの状況で役立ちます。何かを達成するために関数を使用する必要がある場合がありますが、この関数は一時的なものにすぎないため、この関数専用にトップレベルの関数オブジェクトを生成する理由はありません。例:
リスト 1. マップ関数
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/ / Add配列の各要素に 1 [1,3,5,7,8]
map 関数の呼び出しに注意してください。これは、map の 2 番目のパラメーターです。 function マップの最初の各パラメーター (配列) に影響しますが、マップの外のコードには何の意味も持たない可能性があるため、特に関数を定義する必要はありません。匿名関数は次のとおりです。十分。
カリー化
カリー化とは、複数のパラメーターを受け取る関数を、単一のパラメーター (元の関数の最初のパラメーター) を受け取り、残りのパラメーターを返す関数に変換する技術です。結果を返す新しい関数。この文は少し複雑ですが、理解を助けるために例を使用します。
リスト 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
さらに興味深いのは、関数加算器がパラメーターを受け取り、関数を返すことです。返された関数は期待どおりに呼び出すことができます。変数 add5 は、adder(5) によって返される関数を保持します。この関数は 1 つのパラメーターを受け取り、パラメーターと 5 の合計を返します。
次のセクションで説明するように、カリー化は DOM コールバックで非常に役立ちます。
高階関数
高階関数は、関数をさらに抽象化したものです。実際、無名関数のセクションで説明したマップ関数は、多くの場合高階関数です。この関数は関数型プログラミング言語にあります。 map(array, func) の式は、func 関数が配列内の各要素に適用され、最終的に新しい配列を返すことを示しています。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 非常的流行,因此你可以找到很多论述如何使用它的文章。