聊一聊javascript是应该面向对象式还是函数式

javascript因出身寒酸,诞生之日并没有明确的范型(paradigm),但随着ECMAScript标准的快速发展及babel的编译器超前支持,javascript呈现出多种范型并发发展的趋势。比如es6中规定的class,让javascript成为OO[1]范型成为可能;同样在es6中定义的=>(fat arrow)也让javascript成为FP[2]范型成为可能。在这些可能的范型中,该如何取舍,这是一个曾经困扰我过的问题。

我曾写过10年的C#,我对OO有很深的情结,但是在javascript的世界里,我并没有走向OO,原因大概是:

  1. this就像一个大bug,很多时候错误都是因为它,因为隐蔽所以有时花很多时间调试。再加上=>function的细微差别[3],也让我不敢使用this,如果不用this,那我觉得已经不像OO了;
  2. class中没有private概念,所有的属性从本质上讲都可以被外部访问到,当然可以用比如前缀_来区分,但都很变扭,可以说连OO最基本的封装也没有做到;
  3. 由于缺乏类型和很多特性的支持,javascipt中的OO很难走得太远,比如:IoC容器就很难实现;
  4. 我始终觉得OO中类的颗粒度很难把握,包含的东西稍微多了,就会变成像一个程序一样,充满了全局变量和函数;如果包含的逻辑少了,比如很多的设计模式(如:SingletonCommand、等等),class只有一个方法一个属性,如果是这样何不就一个函数?

而我最终还是渐渐地走向了FP,下面是我能想到的原因:

  1. 尽管javascript不是一个100%的FP语言,但是相对于很多语言,其对于FP的支持要更多些。比如对于FP中最基础的currycompose,我觉得要至少比C#JAVA等很多有FP特性的语言强;
  2. 终于可以摆脱this了,也可以在所有场合用=>了;
  3. 更好的封装性:可以通过curry闭包实现private,反而比OO具有封装性,如下代码所示:
  const add = curry((a, b) => a + b)
  const inc = add(1) // 这个传入的1其实存在在闭包中,外部无法访问,具有极好的可配置封装性
  inc(4) // 5
  1. 更好地代码重用和重构能力:FP优于OO的主要点是:避免了OO随时可变的State[4],因为函数只取决于传入的参数,所以很容易地对代码重构或者重用;

当然,javascript离完美的FP语言还有很多距离,比如:

  1. 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)
  1. ImmutableFP的重要特点,FP要求函数都是pure的,没有side effects,所以所有的变量都不能改变值。虽然javascript中有const,但当定义于址类型来说(比如array),并没有达到immutable。要真正达到immutable,有以下办法:
    • 要不只有引入一些库,比如immutable-js,但这这是以破坏程序的可读性为代价的
    • 要不自己手工控制,比如array.push(1)就用array = [...array, 1]来取代,对于简单的操作可以,但对于复杂的操作,这些连接代码会增多,也容易引起错误。
  const array = []
  array.push(1) // 不会报错,但却改变了array的值
  1. 对于compose没有语言层面的支持
  2. 对于FP中的各种基础类型,比如:TaskEitherMonadSemigroup等没有语言层面的支持

尽管如此,javascript的开源社区还是推出了很多库来解决javascript对于FP语言层面支持的缺乏,除了连接代码[5]稍微多了写外,其它都还能接受,经过2年左右的javascriptFP实践,我认为是完全可行的。

  1. OO:Object Oriented,即面向对象

  2. FP:Functional Programming,即函数式编程

  3. =>中的this绑定的是定义时所在的对象,而不是使用时所在的对象,可参考:阮一峰的ECMAScript 6 介绍

  4. OOstateOO最麻烦的是在某一时刻,你不知道其内部的属性值,这对理解代码和调试代码都带来相当的麻烦,就像全局变量。另外,若要实现并发,只能使用多线程技术,而处理多线程之间的协调又是极其困难

  5. 连接代码:指的是没有业务逻辑功能的代码,比如:const f = curry( (a, b) => a + b )curry就是连接代码,作为一个好的架构,连接代码应该越少越好。

    原文作者:荣观
    原文地址: https://www.jianshu.com/p/4d6c8bcd723a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞