前言
虽然大家已经被面向对象编程(Object-oriented programing)洗脑了,但很明显这种编程方式在 JavaScript 里非常笨拙,这种语言里没有类可以用,社区采取的变通方法不下三种,还要应对忘记调用 new
关键字后的怪异行为,真正的私有成员只能通过闭包(closure)才能实现,而多数情况,就像我们在亿书代码里那样,把私有方法放在一个privated变量里,视觉上区分一下而已,本质上并非私有方法。对大多数人来说,函数式编程看起来才更加自然。而且,在Nodejs的世界里,大量的回调函数是数据驱动的,使用函数式编程更加容易理解和处理。
函数式编程远远没有面向对象编程普及,本篇文章借鉴了几篇优秀文档(见参考),结合亿书项目实践和个人体会,汇总了一些平时用得到的函数式编程思路,为更好的优化设计亿书做好准备。本篇内容包括函数式编程基本概念,主要特点和编码方法,其中的一些代码实例主要参考了 《mostly adequate guide》,folktalejs 和 ramdajs 的相关代码,参考里也提供了它们的链接,请认真参考学习。如果想运行文中的代码,请提前安装ramda等相应的第三方组件。
什么是函数式编程?
简单说,“函数式编程”与“面向对象编程”一样,都是一种编写程序的方法论。它属于 “结构化编程” 的一种,主要思想是以数据为思考对象,以功能为基本单元,把程序尽量写成一系列嵌套的函数调用。
下面,我们就从一个简单的例子开始,来体会其中的奥妙和优势。这个例子来自于mostly-adequate-guide,作者说这是一个愚蠢的例子,并不是面向对象的良好实践,它只是强调当前这种变量赋值方式的一些弊端。这是一个海鸥程序,鸟群合并则变成了一个更大的鸟群,繁殖则增加了鸟群的数量,增加的数量就是它们繁殖出来的海鸥的数量。
(1)面向对象编码方式
var Flock = function(n) {
this.seagulls = n;
};
Flock.prototype.conjoin = function(other) {
this.seagulls += other.seagulls;
return this;
};
Flock.prototype.breed = function(other) {
this.seagulls = this.seagulls * other.seagulls;
return this;
};
var flock_a = new Flock(4);
var flock_b = new Flock(2);
var flock_c = new Flock(0);
var result = flock_a.conjoin(flock_c).breed(flock_b).conjoin(flock_a.breed(flock_b)).seagulls;
//=> 32
按照正常的面向对象语言的编码风格,上面的代码好像没有什么错误,但运行结果却是错误的,正确答案是 16
,是因为 flock_a
的状态值seagulls
在运算过程中不断被改变。别的先不说,如果flock_a
的状态保持始终不变,结果就不会错误。
这类代码的内部可变状态非常难以追踪,出现这类看似正常,实则错误的代码,对整个程序是致命的。
(2)函数式编程方式
var conjoin = function(flock_x, flock_y) {
return flock_x + flock_y };
var breed = function(flock_x, flock_y) {
return flock_x * flock_y };
var flock_a = 4;
var flock_b = 2;
var flock_c = 0;
var result = conjoin(breed(flock_b, conjoin(flock_a, flock_c)), breed(flock_a, flock_b));
//=>16
先不用考虑其他场景,至少就这个例子而言,这种写法简洁优雅多了。从数据角度考虑,逻辑简单直接,不过是简单的加(conjoin
)和乘(breed
)运算而已。
(3)函数式编程的延伸
函数名越直白越好,改一下:
var add = function(x, y) {
return x + y };
var multiply = function(x, y) {
return x * y };
var flock_a = 4;
var flock_b = 2;
var flock_c = 0;
var result = add(multiply(flock_b, add(flock_a, flock_c)), multiply(flock_a, flock_b));
//=>16
这么一来,你会发现我们还能运用小学都学过的运算定律:
// 结合律
add(add(x, y), z) == add(x, add(y, z));
// 交换律
add(x, y) == add(y, x);
// 同一律
add(x, 0) == x;
// 分配律
multiply(x, add(y,z)) == add(multiply(x, y), multiply(x, z));
我们来看看运用这些定律如何简化这个海鸥小程序:
// 原有代码
add(multiply(flock_b, add(flock_a, flock_c)), multiply(flock_a, flock_b));
// 应用同一律,去掉多余的加法操作(add(flock_a, flock_c) == flock_a)
add(multiply(flock_b, flock_a), multiply(flock_a, flock_b));
// 再应用分配律
multiply(flock_b, add(flock_a, flock_a));
到这里,程序就变得非常有意思,如果更加复杂的应用,也能够确保结果可以预期,这就是函数式编程。
函数式编程的优势
1.易于开发:代码简洁,大大降低开发成本
从上面的代码,可以体会到这一点。使用函数式编程,可以充分发挥Javascript语言自身的优点,每一个函数都是独立单元,便于调试和测试,也方便模块化组合,代码量少、开发效率高。有人比较过C语言与Lisp语言,同样功能的程序,极端情况下,Lisp代码的长度可能是C代码的二十分之一。
2.易于分享:更接近自然语言,代码即文档
上面使用分配律之后的代码multiply(flock_b, add(flock_a, flock_a))
,完全可以改成下面这样:
add(flock_a, flock_a).multiply(flock_b) // = (flock_a + flock_a) * flock_b
特别是下面这样的句式,如果是用惯了ruby on rails的小伙伴,更加熟悉下面的语句:
User.all().sortBy('name').limit(20)
3.性能更高:能够实现”并发编程”(concurrency)
函数式编程不依赖、也不会改变外界的状态,相同输入获得相同输出,因此不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,实现”并发编程”。目前的计算机基本上都是多核的了,多线程应用将是常态,亿书客户端也会考虑优化线程服务,提高软件性能,改善用户体验,这将非常有帮助。
4.部署简单:热部署和热升级
函数式编程没有副作用,只要接口没变化,改变函数内部代码对外部没有任何影响。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。这对于每秒要处理很多交易的加密货币系统来说,尤为重要。亿书将在未来实现每个节点的热部署和热升级,使节点建立和维护零难度。
函数式编程的基本原则
总的来说,就是“正确地写出正确的函数”。这话有点绕,不过我们写了太多面向对象的代码,可能已被“毒害”太深,不得不下点猛药,说点狠话,不然记不住。函数式编程,当然函数是主角,被称为“一等公民”,特别是对于 JavaScript 语言来说,可以像对待任何其他数据类型一样对待——把它们存在数组里,当作参数传递,赋值给变量…等等。但是,说起来容易,真正做起来,并非每个人都能轻松做到。下面是写出正确函数的几个原则:
(1)直接把函数赋值给变量
记住:凡是使用return
返回函数调用的,都可以去掉这个间接包裹层,最终连参数和括号一起去掉!
以下代码都来自 npm 上的模块包:
// 太傻了
var getServerStuff = function(callback){
return ajaxCall(function(json){
return callback(json);
});
};
// 这才像样
var getServerStuff = ajaxCall;
世界上到处都充斥着这样的垃圾 ajax 代码。以下是上述两种写法等价的原因:
// 这行
return ajaxCall(function(json){
return callback(json);
});
// 等价于这行
return ajaxCall(callback);
// 那么,重构下 getServerStuff
var getServerStuff = function(callback){
return ajaxCall(callback);
};
// ...就等于
var getServerStuff = ajaxCall; // <-- 看,没有括号哦
(2)使用最普适的方式命名
函数属于操作,命名最好简单直白体现功能性,比如add
等。参数是数据,最好不要限定在特定的数据上,比如articles
,就能让写出来的函数更加通用,避免重复造轮子。例如:
// 只针对当前的博客
var validArticles = function(articles) {
return articles.filter(function(article){
return article !== null && article !== undefined;
});
};
// 对未来的项目友好太多
var compact = function(xs) {