《javascript函数式编程思想》——从面向对象到函数式编程

starrow      2022-06-09     638

关键词:

第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());
//=> 1

f.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());
//=> 1

f.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章 名称一般对函数式编程的介绍都会从一等值和纯函数等概念开始,本书却准备在那之前先花些篇章讨论两个通常未得到足够重视的主题:名称和类型系统。前者包括名称绑定、作用域和闭包等内容,后者包括类... 查看详情

《javascript函数式编程思想》

自序伴随着Web技术的普及,JavaScript已成为应用最广泛的编程语言之一。由于其在Web前端编程中的统治地位、语言本身的表现力、灵活性、开源的本质和ECMAScript标准近年来的快速发展,JavaScript向各个领域渗透的势头仍然... 查看详情

用函数式编程对javascript进行断舍离

译者按:当从业20的JavaScript老司机学会函数式编程时,他扔掉了90%的特性,也不用面向对象了,最后发现了真爱啊!!!原文:HowIrediscoveredmyloveforJavaScriptafterthrowing90%ofitinthetrash.译者:Fundebug为了保证可读性,本文采用意译而非直译... 查看详情

《javascript函数式编程思想》

自序伴随着Web技术的普及,JavaScript已成为应用最广泛的编程语言之一。由于其在Web前端编程中的统治地位、语言本身的表现力、灵活性、开源的本质和ECMAScript标准近年来的快速发展,JavaScript向各个领域渗透的势头仍然... 查看详情

函数式编程初探

...如Erlang、clojure、Scala、F#等等。目前最当红的Python、Ruby、Javascript,对函数式编程的支持都很强,就连老牌的面向对象的Java、面向过程的PHP,都忙不迭地加入对匿名函数的支持。越来越多的迹象 查看详情

《javascript函数式编程思想》——列表

第8章 列表函数式编程与列表处理有很深的渊源。列表是最基础,也是使用最普遍的复合数据类型。作为最早出现的函数式编程语言之一,Lisp【注:它的名称就来源于“列表处理器”(LIStProcessor)】用函数参... 查看详情

[转]函数式编程初探

...如Erlang、clojure、Scala、F#等等。目前最当红的Python、Ruby、Javascript,对函数式编程的支持都很强,就连老牌的面向对象的Java、面向过程的PHP,都忙不迭地加入对匿名函数的支持。越来越多的迹象 查看详情

函数式编程思想:耦合和组合,第1部分

...重用的面向对象编程思想的一些影响,并把它们与一些更函数化的可选方法,比如说组合,进行比较。面向对象编程通过封装变动部分把代码变成易懂的,函数式编程则是通过最小化变动部分来把代码变成易懂的。——Mi 查看详情

《javascript函数式编程思想》——部分应用和复合

第5章 部分应用和复合一等值的函数,是函数式编程的基石。部分应用和复合,则是函数式编程的重要特征。采用命令式编程时,每当我们感觉需要抽象出一个新的功能时,就会定义一个函数。在函数式编程中... 查看详情

函数式编程java函数式编程学习(代码片段)

函数式编程-Stream流函数式编程思想概述面向对象思想关注的是用什么对象完成什么事情,而函数式编程思想就类似于数学中的函数,主要关注的是对数据进行了什么操作优点代码简洁,开发快;接近自然语言࿰... 查看详情

函数式编程java函数式编程学习(代码片段)

函数式编程-Stream流函数式编程思想概述面向对象思想关注的是用什么对象完成什么事情,而函数式编程思想就类似于数学中的函数,主要关注的是对数据进行了什么操作优点代码简洁,开发快;接近自然语言࿰... 查看详情

《javascript函数式编程思想》——函数是一等值

第4章 函数是一等值在函数式编程的标准或特点中,“函数是一等值”是最基本和重要的,也是最为人所知的,所有介绍函数式编程的书籍和文章都会优先介绍这一点,以至于“一等值”几乎成为函数的专属头衔&... 查看详情

面向对象编程

在前面的章节中,我们掌握了使用函数式编程和元编程技术来定制函数的行为。也可以用函数创建函数,就是所谓的闭包。还可以像传递其他对象一样,将函数传递给函数,即使用高阶函数。在本章中,我们将走进面向对象编程... 查看详情

javascript系列:函数式编程(开篇)

...数以及函数柯里化等高级函数应用,同时,因为正在学习JavaScript·函数式编程,想整理一下函数式编程中,对于我们日常比较有用的部分。 为什么函数式编程很重要?   学习过C++,java这些面向对象编程语言,我... 查看详情

函数式编程思想:耦合和组合,第2部分

...者可能会对这一方法的缺点及其他的可选做法视而不见,函数式编程使用不同的构建块来实现重用,其基于的是更一般化的概念,比如说列表转换和可移植代码。函数式编程思想的这一部分内容比较了作为重用机制的经由继承的... 查看详情

聚焦javascript面向对象的思想

...复杂系统进行分析、设计与编程,今天我们就来学习一下JavaScript面向对象的思想。面向过程和面向对象编程概述面向过程编程就是分析出解决问题 查看详情

编程语言共性之------什么是函数式编程?

...Erlang、clojure、Scala,、F#等等。目前最当红的Python、Ruby、Javascript,对函数式编程的支持都很强,就连老牌的面向对象的Java、面向过程的PHP,都忙不迭地加入对匿名函数的支持。越来越多 查看详情

面向对象(代码片段)

一.函数式编程和面向对象的对比  面向过程:根据业务逻辑从上到下写垒代码  函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可  面向对象:对函数进行分类和封装,让开发“更快更好更强..... 查看详情