javascript
因出身寒酸,诞生之日并没有明确的范型(paradigm),但随着ECMAScript
标准的快速发展及babel
的编译器超前支持,javascript
呈现出多种范型并发发展的趋势。比如es6
中规定的class
,让javascript
成为OO
[1]范型成为可能;同样在es6
中定义的=>
(fat arrow)也让javascript
成为FP
[2]范型成为可能。在这些可能的范型中,该如何取舍,这是一个曾经困扰我过的问题。
我曾写过10年的C#
,我对OO
有很深的情结,但是在javascript
的世界里,我并没有走向OO
,原因大概是:
-
this
就像一个大bug,很多时候错误都是因为它,因为隐蔽所以有时花很多时间调试。再加上=>
和function
的细微差别[3],也让我不敢使用this
,如果不用this
,那我觉得已经不像OO
了; -
class
中没有private
概念,所有的属性从本质上讲都可以被外部访问到,当然可以用比如前缀_
来区分,但都很变扭,可以说连OO
最基本的封装也没有做到; - 由于缺乏类型和很多特性的支持,
javascipt
中的OO
很难走得太远,比如:IoC
容器就很难实现; - 我始终觉得
OO
中类的颗粒度很难把握,包含的东西稍微多了,就会变成像一个程序一样,充满了全局变量和函数;如果包含的逻辑少了,比如很多的设计模式(如:Singleton
、Command
、等等),class
只有一个方法一个属性,如果是这样何不就一个函数?
而我最终还是渐渐地走向了FP
,下面是我能想到的原因:
- 尽管
javascript
不是一个100%的FP
语言,但是相对于很多语言,其对于FP
的支持要更多些。比如对于FP
中最基础的curry
和compose
,我觉得要至少比C#
和JAVA
等很多有FP
特性的语言强; - 终于可以摆脱
this
了,也可以在所有场合用=>
了; - 更好的封装性:可以通过
curry
和闭包
实现private
,反而比OO
具有封装性,如下代码所示:
const add = curry((a, b) => a + b)
const inc = add(1) // 这个传入的1其实存在在闭包中,外部无法访问,具有极好的可配置封装性
inc(4) // 5
- 更好地代码重用和重构能力:
FP
优于OO
的主要点是:避免了OO
随时可变的State
[4],因为函数只取决于传入的参数,所以很容易地对代码重构或者重用;
当然,javascript
离完美的FP
语言还有很多距离,比如:
- 对
curry
没有语言基本的支持,而javascript
的可变参数数量简直是和curry
背道而驰。curry
要求参数是固定的,如果定义一个curry函数有3个参数,那么调用小于3个参数的情况都会返回一个函数,而给4个参数,它会把函数的返回结果作为函数去调用第4个参数,从而报错,如下所示
const f3 = curry( (a, b, c) => a + b + c)
f3(1)(2) // 返回函数
f3(1, 2) // 返回函数
f3(1, 2, 3) // 6
f3(1, 2, 3, 4) // 报错,说6不是函数。原因是,这个调用相当于f3(1, 2, 3)(4),即6(4)
-
Immutable
是FP
的重要特点,FP
要求函数都是pure
的,没有side effects
,所以所有的变量都不能改变值。虽然javascript
中有const
,但当定义于址类型来说(比如array),并没有达到immutable
。要真正达到immutable
,有以下办法:- 要不只有引入一些库,比如
immutable-js
,但这这是以破坏程序的可读性为代价的 - 要不自己手工控制,比如
array.push(1)
就用array = [...array, 1]
来取代,对于简单的操作可以,但对于复杂的操作,这些连接代码会增多,也容易引起错误。
- 要不只有引入一些库,比如
const array = []
array.push(1) // 不会报错,但却改变了array的值
- 对于
compose
没有语言层面的支持 - 对于
FP
中的各种基础类型,比如:Task
、Either
、Monad
、Semigroup
等没有语言层面的支持
尽管如此,javascript
的开源社区还是推出了很多库来解决javascript
对于FP
语言层面支持的缺乏,除了连接代码
[5]稍微多了写外,其它都还能接受,经过2年左右的javascript
的FP
实践,我认为是完全可行的。
OO:Object Oriented,即面向对象 ↩
FP:Functional Programming,即函数式编程 ↩
=>
中的this
绑定的是定义时所在的对象,而不是使用时所在的对象,可参考:阮一峰的ECMAScript 6 介绍 ↩OO
的state
:OO
最麻烦的是在某一时刻,你不知道其内部的属性值,这对理解代码和调试代码都带来相当的麻烦,就像全局变量。另外,若要实现并发,只能使用多线程技术,而处理多线程之间的协调又是极其困难 ↩连接代码:指的是没有业务逻辑功能的代码,比如:
const f = curry( (a, b) => a + b )
中curry
就是连接代码,作为一个好的架构,连接代码应该越少越好。 ↩