第9章  从面向对象到函数式编程

假如本书的写作时间倒退回十年前,书名可能会变成JavaScript面向对象编程思想。自上世纪90年代兴起的面向对象编程思想随Java的繁荣达于顶点,在JavaScript从一门只被用来编写零星的简单的表单验证代码的玩具语言变成日益流行的Web应用不可取代的开发语言的过程中,脚本的作者们也逐渐学习和习惯了被视为软件开发正统的面向对象编程。等到盛极而衰,面向对象编程的缺点开始浮现和被广泛讨论,Java的热度和市场份额不及以往,而对函数式编程的兴趣和关注从学术圈子扩散至商业软件开发领域,Clojure、Erlang、Scala等新的函数式编程语言被发明和获得市场接纳,Lambda表达式成为编程语言中的热词和老牌语言竞相增添的功能,JavaScript程序员也开始认识到函数式编程的优点。幸运的是,函数式编程所需的基本能力JavaScript在诞生之初就具备了,在迄今二十多年的历史中,JavaScript发扬光大的是事件编程和在面向对象编程世界里长久被忽略的基于原型的对象模型,现在它作为最流行和有活力的编程语言之一,正在逐渐发掘和转向自身混合基因中的另一元素。虽然以函数式编程语言的标准来看,JavaScript相比其他专为此设计的语言有不少不足和缺陷,但是它的普及度还是让它成为函数式编程的有力推动者甚至有希望是这种范式应用最广泛的语言。

从现实来看,巨大的惯性、既有的代码、程序员的知识和思维习惯使得面向对象编程依然是JavaScript开发的主流。此外,自ECMAScript 6原来的诸次语言扩展和改进主要是增强面向对象编程能力的。所以提倡JavaScript的函数式编程,不可避免的问题或者说最有力的方法就是将之与面向对象编程做比较,解释前者的优点,说明后者的概念和做法如何能用前者取代。

9.1  面向对象编程的特点
9.1.1  封装性
9.1.2  继承性
9.1.3  多态性
9.2  JavaScript面向对象编程
9.2.1  创建和修改单个对象
9.2.2  克隆和复制属性
9.2.3  原型
9.2.4  建构函数
9.2.5  建构函数和类型继承
9.2.6  原型和类型继承
9.2.7  Proxy和对象继承
9.2.8  Mixin
9.2.9  工厂函数

9.3  函数式编程的视角

在上一节里,我们介绍了JavaScript面向对象编程的种种模式和风格,充分体现了JavaScript的灵活性。程序员可以选择自己喜欢和习惯的编程模式,造就了JavaScript代码五彩纷呈的风格。然而从函数式编程的视角来看,这些编程模式和技巧就有了好坏之分,有些在函数式编程中还有用武之地,大部分则被理念不同的函数式编程的模式替代。

9.3.1  不可变的对象

面向对象编程的出发点是将数据和处理它们的函数封装成对象,以对象为视角和工具来对实际问题进行建模,代码的复用和开发都是以对象为单位。函数式编程以函数为中心,数据和函数保持分离,大量使用部分应用和复合的技术。至于依赖递归与强调纯函数和不变性等,则不是函数式编程与命令式编程的分野,在面向对象编程中也可以采纳。上一节介绍的各种模式中的对象都可以被改造成不可变的对象,以避免6.3.5小节分析的可变类型可能导致的副作用。以工厂函数为例,下面的版本创建的就是不可变的对象。

const immutableCounter = (value = 0) => {const current = () => value;const increment = () => immutableCounter(value + 1);return {current, increment};
};const c1 = immutableCounter();
const c2 = c1.increment();
f.log(c1.increment().current());
//=> 1f.log(c1 === c2);
//=> false

因为reset方法不再修改当前对象,而是返回一个全新的计数器实例,与直接调用工厂函数毫无差别,所以被删除了。对于极简代码的爱好者,上述版本还可以进一步简化。

const immutableCounter2 = (value = 0) => ({current() {return value;}, increment() {return immutableCounter2(value + 1);}
});

注意在箭头函数表达式的箭头后面必须加上一对圆括号,以使JavaScript将返回的对象当作一个值来计算,否则对象字面值起始和末尾处的大括号会被解释成包围函数主体的代码。进一步地,共享方法的工厂函数也可以如法炮制。

const immutableCounter3 = (function () {const methods = {current() {return this._val;},increment() {return counter(this._val + 1);}};function counter(value = 0) {return Object.assign({_val: value}, methods);}return counter;
})();const c3 = immutableCounter3();
const c4 = c3.increment();
f.log(c3.increment().current());
//=> 1f.log(c3 === c4);
//=> false

将建构函数和Mixin和其他面向对象编程的模式改造成使用不可变的对象,就留给有兴趣的读者做练习,对于开发智力、开拓思路、辨析JavaScript的特性和掌握其编程技能都大有裨益。

9.3.2  评判面向对象编程

以函数式编程的标准,面向对象编程的第一个问题是其中的函数不是一等值。不少专为面向对象编程设计的语言无法享受到函数是一等值的好处,也就缺乏进行函数式编程的基础。JavaScript没有这个问题,对象的方法可以作为参数传递,可以被返回,可以被赋予变量。但是方法的调用对象通过this关键字传递,而当方法像函数一样作为一等值运用时,this绑定的值往往不正确。面向对象编程的第二个问题是,方法所属的对象作为特殊的参数,不像其他函数参数那样传递,因此方法难以像函数那样采用部分应用和复合的技术。所以函数式编程的做法是保持数据和函数的分离,即使出于方便组织代码和传递数据的需要将数据和函数置于一个容器内,函数也和容器没有任何绑定关系,也就是不依赖this,函数所需的变量数据全部以参数方式传入。函数式编程中所说的对象便是指这样的复合数据或容器。从这一点出发,再加上对纯函数的要求,我们来评判和改造9.2节介绍的各种模式和技术。

用字面值创建对象没有副作用,修改对象的属性则有。所以前者符合函数式编程的精神,后者最好限制于函数内的变量,不要对参数使用。

克隆操作没有副作用,因为JavaScript的内置的对象是可变的,所以克隆在避免函数的副作用时十分有用。将一个对象的属性复制到另一个对象,在最一般的情况下,涉及到源对象、目标对象和待复制的属性名称,最直接的想法是用一个函数完成。函数签名为copy(source, dest, names),目标对象可以是现有对象或缺省(新创建的对象),属性名称可以是字符串数组或缺省(复制所有的属性),因此该函数一共可以处理四种情况。另一种方案是创建两个较简单的函数,pick(source, names)和assign(object, source)。前者摘取一个对象的若干属性,组成一个新对象;后者将一个对象的所有自身属性复制给另一个对象。组合使用这两个函数,可以实现第一个函数的所有功能,而且调用时更方便灵活。新的API还有一个问题,就是会改动参数中的目标对象。更符合函数式编程风格的API应该是用合并两个对象的merge函数来取代assign函数。最后,我们再按照函数式编程的习惯调整pick函数的参数顺序,就得到了从函数式编程的角度解决复制对象属性问题的两个函数。

export function pick(names, source) {let ret = {};for (let n of names) {set(n, get(n, source), ret);}return ret;
}export function merge(o1, o2) {// 在支持对象散布属性的JavaScript引擎中,可以使用下列简洁的语句。// return {...o1, ...o2};let obj = {...o1};Object.assign(obj, o2);return obj;
}

原型是JavaScript的内置的继承机制,它的问题是只能进行单一继承,单一继承的缺点在9.2.8小节已经分析过了,所以即使要采用面向对象编程,9.2.5小节、9.2.7小节和9.2.9小节介绍的多重继承与9.2.8小节介绍的Mixin都是更好的替代方案。

建构函数是JavaScript的为了方便创建对象设立的语法,它必须配合new操作符使用,与普通函数调用方式的差别不仅不方便函数式编程,还有可能引发错误(程序员因为不了解或不小心将建构函数当作普通函数调用,导致全局对象的属性被修改。)。建构函数的自动行为也限制了代码功能的可能性和灵活性,所以函数式编程采用没有以上缺点的工厂函数来创建对象。

多重继承和Mixin同样强大,不过后者的实现更简单,应用时也更为灵活,既可以针对单个对象,也可以结合工厂函数和原型对一个类型的对象生效,所以更为可取。于是我们得出在JavaScript面向对象编程的阵营中,Mixin技术是最为灵活和强大的。再看Mixin的理念,数据和处理它们的函数分开定义,只是在使用时才结合成对象,这距离函数式编程的做法只有一步之遥。如果我们不做最后一步的结合,并且不再通过this来传递函数操作的对象,Mixin就转化成了函数式编程的代码。

//函数式编程中的模块相当于Mixin。
const module1 = {current: (obj) => obj.value,increment: (obj) =>f.set('value', f.inc(obj.value), obj)
};const module2 = {increment: (obj) =>f.set('value', f.add(obj.step, obj.value), obj),set: f.curry((val, obj) =>f.set('value', val, obj))};let o = {name: 'Peter', value: 1, step: 2};
f.log(module1.current(module2.set(4, o)));
//=> 4//结合工厂函数。
function counter(val, step) {return {value: val,step: step};
}let c = counter(1, 2);
const fn = f.pipe(module2.set(3), module2.increment, module1.current);
f.log(fn(c));
//=> 5

回顾本章至此的三节,JavaScript中的面向对象编程不可谓不灵活,但是采用种种或繁琐或精巧的技术所实现的功能,函数式编程都能用更简洁明了的方式做到。反过来函数式编程中对抽象算法和复用代码有巨大帮助的高阶函数和复合等技术,面向对象编程却没有对应的技术。虽然纯函数和不可变的数据对任何一种编程范式都有好处,但函数式编程对此原则性的要求最能鼓励程序员遵循这些思想。从面向对象转换到函数式编程,既是编程技术的转换,更是理念和思考方法的转换,它给程序员的回报是更少的、更易理解和维护的、更优美的代码。

9.4  方法链和复合函数
9.4.1  方法链
9.4.2  延迟的方法链
9.4.3  复合函数
9.4.3  函数式的SQL
9.5  小结

更多内容,请参看拙著:

《JavaScript函数式编程思想》(京东)

《JavaScript函数式编程思想》(当当)

《JavaScript函数式编程思想》(亚马逊)

《JavaScript函数式编程思想》(天猫)

《JavaScript函数式编程思想》——从面向对象到函数式编程相关推荐

  1. 编程思想:面向对象和面向过程

    何谓面向对象?何谓面向过程?对于这编程界的两大思想,一直贯穿在我们学习和工作当中.我们知道面向过程和面向对象,但要让我们讲出来个所以然,又感觉是不知从何说起,最后可能也只会说出一句就是那样啦,你知道啦 ...

  2. python完全支持面向对象编程思想_面向对象的编程思想和Python的继承和多态,特殊方法,引用计数...

    面向对象的编程思想和Python的类,访问和属性,继承 在上一文中我们了解到了,私有的属性的访问方式:实例名._类名__私有属性名. 一.私有的属性如何对外提供公有的取值和赋值方法呢?提供公有的方法作 ...

  3. python完全支持面向对象编程思想_面向对象的编程思想和Python的类,访问和属性,继承...

    本文将从访问限制,属性,继承,方法重写这几个方面继续介绍面向对象的编程思想和Python类的继承. 复制代码 一.访问权限: Python中在类的内部定义属性和方法,在类的外部是可以直接调用或进行访问 ...

  4. 编程思想:面向对象和面向过程的区别与联系

    前言 何谓面向对象?何谓面向过程?对于这编程界的两大思想,一直贯穿在我们学习和工作当中.我们知道面向过程和面向对象,但要让我们讲出来个所以然,又感觉是不知从何说起.而这种茫然,其实就是对这两大编程思想 ...

  5. java编程思想怎么样_读完java编程思想后的思考?

    谢邀,这本书真的给我带来很多思考. 我的java入门不是java编程思想,学校的教材是一本紫色的书,已经忘了叫什么名字了,里面内容倒挺新还讲了些javafx.但那本书实在是太浅并且结构混乱,以至于我和 ...

  6. 数学对编程思想的帮助_学编程需要什么基础?

    程序员薪酬高.工作环境好,是很多同学向往的职业,让很多非计算机专业的同学羡慕不已.非计算机专业难道就不能成为程序员了吗? 学编程需要什么基础? 1.数学基础 从计算机发展和应用的历史来看计算机的数学模 ...

  7. java面向对象编程思想_Java面向对象编程思想的理解

    1.我们总说java是一门面向对象编程的语言,那什么是面向对象呢? 我是这样理解的,对象是事物存在的实体,如,猪.狗,花早等都是对象,对象由两部分组成.面向对象编程的三大特点:继承,多态,类是封装对象 ...

  8. java面向对象编程思想_Java面向对象编程思想

    面向对象三个特征:封装.继承.多态 封装: 语法:属性私有化(private).提供相对应的get/set 的方法进行访问(public). 在set/get的方法中对属性的数据 做相对应的业务逻辑的 ...

  9. java编程思想 初始化_《java编程思想》_第五章_初始化与清理

    初始化和清理是涉及安全的两个问题,java中采用了构造器,并额外提供了"垃圾回收器",对于不再使用的内存资源,垃圾回收器能自动将其释放. 一.用构造器确保初始化 java中,通过提 ...

  10. 面向对象编程思想 以及类与对象

    一.面向对象编程思想 众所周知,我们常见的编程思想有面向过程和面向对象两种,像我们最基础的c语言,就是一种以过程为中心的编程思想,不关注具体的事件和对象而是针对于解决问题的思路和目标,这种编程思想由于 ...

最新文章

  1. 利用查找替换批处理(附完整源码),进行高效重构
  2. 160个Crackme038之P-Code初窥门径
  3. jsp java语法_JSP基础语法
  4. 我曾经得到的一个最好的编程建议
  5. leetcode 400. Nth Digit | 400. 第 N 位数字(二分法找左侧不大于n的第一个数)
  6. php fopen插入文本_PHP 文件创建/写入
  7. Python档案袋( 面向对象 )
  8. 您在2016年OpenStack峰会上错过的事情
  9. 攀爬者(洛谷P5143题题解,Java语言描述)
  10. android微信打不开怎么办,微信打不开怎么回事
  11. R语言神经网络与深度学习(一)
  12. 值类型与引用类型传递的艺术
  13. mysql存储过程类_mysql存储过程类
  14. Ubuntu中如何打开终端terminal
  15. char *转为pansichar
  16. sqlserver查询语句实例
  17. 实验四——反汇编工具的使用
  18. html文本框怎么加粗,html怎么让字体加粗
  19. 前台传递JSON数据,后台spring mvc如何接收数据
  20. 阿里格灵深瞳计算机视觉岗实习面经

热门文章

  1. lisp 获取横断面数据_CAD中高程点提取横断面数据的方法
  2. English Writing Note
  3. python appium+夜神模拟器 配置 笔记整理
  4. 2020认证杯第二阶段选提建议
  5. 集合论第一章 3 集合论的公式和条件
  6. 高薪物联网职业生涯所需的十大技能(转)
  7. ESXi OEM版本下载地址
  8. zoj3869 Ace of Aces zoj3880 Demacia of the Ancients(水)
  9. MySQL: 为什么使用 innobackupex 备份恢复搭建主从时,必须人为设置 gtid_purged 变量
  10. x5675相当于e5_2020年最新桌面CPU性能排行天梯图(含至强处理器)