为什么说 JavaScript 不擅长函数式编程

JavaScript08

为什么说 JavaScript 不擅长函数式编程,第1张

主要有以下原因:

Functions as first-class values, 函数一等公民, 可以把函数作为参数传递, 从而构造出高阶函数各种用法. 这个用法各种语言都支持了: Lua 支持, Python 似乎也支持, Java 也开始支持了, 我会的语言少都举不出来不支持传函数的流行语言.

Pure functions, 纯函数. 可以写, 但也有很大区别. JavaScript 没限制, 从而不能预判函数纯或者不纯. Clojure 遵循 Lisp 风格的约定, 带副作用的函数一般用 `f!` 这种叹号结尾的写法命名, 而编译器没有约束. Haskell 是严格约束的, 出了名的 IO Monad 就是因为遵循纯函数导致副作用难以直接用数学函数表达出来, 最终精心设计出一个概念.

Referential transparency, 引用透明, 所以表达式可以被其运算结果完全替换掉, 也就是要求控制甚至避免副作用. 

Controlled effects, 受控的副作用, 主要手段是隔离. JavaScript 需要人为地去隔离, 语言层面完全没有限制. Clojure 也需要人为隔离, 就像前面说的 `f!` 那样的约定, 同时规定了数据不可变, 再加上作者有意在语言中强调控制副作用, 实际上副作用少得多. Haskell 通过类型系统限定, 不隔离副作用无法通过编译的.

Everything is an expression, 一切皆是表达式. JavaScript 做不到, 导致设计 DSL 时候很头疼, 倒是 CoffeeScript 做到了. Clojure 继承了 Lisp, 很明显一切皆是表达式. Haskell 代码里都是函数, 除了类型声明和语法糖部分, 也是一切皆是表达式.

No loops, 换句话说, 不能用 for/while, 因为这两个写法当中的 `i++` 依赖可变数据. JavaScript 经常使用 for/while. Clojure 当中的循环基本上用尾递归实现, 同时也提供了 doseq 之类的 Macro 让循环过程很好写. Haskell 就是完全尾递归的写法了.

Immutable values. JavaScript 默认可变, 仅有的手段用 `Object.freeze` 可以强行锁定对象或者 const 锁定变量本身, 另外就是 immutable-js 那样的共享结构的不可变数据作为类库来实现. Clojure 是把不可变数据和结构共享作为语言的基础, 专门设计了 Atom 类型用于模拟共享的可变状态, 也不排除某些场景和宿主语言的互操作还是会有可变数据. Haskell 默认就是不可变数据, 也有 IORef 相关的代码可以模拟可变状态, 但在教程里几乎看不到.

Algebraic Datatypes, 代数类型系统. JavaScript 没有静态类型系统, TypeScript 有类型, 但和代数类型还不一样. Clojure 没有静态类型系统, 就算有而只是很基础的类型检查, 或者用 Specs 做详细运行时检查. Haskell 有强大的代数类型系统, 即便是副作用也被涵盖在类型系统当中.

Product types. Haskell 通过代数类型系统支持.

No Null. JavaScript 当中有 undefined 和 null. Clojure 当中只有 nil. Haskell 里没有 null 也没有 nil, 而是用了 Maybe Monad 这样的概念, 通过类型系统进行了抽象和限制. null 的问题很深, 网上找解释吧, 我还没理解清楚, 只了解到满足了方便却造成了意料之外的复杂度.

A function always returns a value, 函数永远都有返回值, 类似一切皆是表达式那个问题. 比如 Haskell 里会有的叫做 Unit 的 `()` 空的值. 这个有点费解...

Currying, 柯理化. JavaScript 和 Clojure 也能模拟, 而在 Haskell 当中是默认行为.

Lexical scoping, 词法作用域. 三者都支持.

Closures, 闭包, 都支持.

Pattern matching, 模式匹配. 类似解构赋值之类的在 JavaScript 和 Clojure 当中通过语法糖也算有这个功能, 但是跟 Haskell 以及 Elixir 当中的用法对比起来差距很大. 比如说 Haskell 甚至能定义 `let 1 + 1 = 3` 来覆盖 `+` 的行为, 虽然是奇葩的现象, 但这就是一个定义的 pattern, 在 JavaScript 和 Clojure 都没有这种情况.

Lazy evaluation, 惰性计算. JavaScript 是严格求值的, 不支持惰性计算. Clojure 支持 Lazy, 然而由于 Clojure 又允许了一些副作用, 实际上某些特殊场景会需要手动 force 代码执行, 也就是说不完美. Haskell 采用惰性计算. 惰性计算就是说代码里的表达式被真正使用来才会真正执行, 否则就像是个 thunk, 继续以表达式存储着. 我印象里 Elm 社区说过, 对于图形界面来说 Lazy 反而是多余的.

    大致做个总结, 就是 Haskell 当中的类型系统, 不可变数据, 控制副作用, 在 Clojure 当中只是做了不可变数据, 同时稍微控制了一下副作用, 而这些概念在 JavaScript 当中很少有支持. 这样的结果, JavaScript 写出来的代码几乎都是不符合函数式编程的限制得.

这是一个真实的例子,展示了三种最常见的编程范式的差异。我将用三种不同的方式解决一个问题。

每个示例将处理表单提交、验证用户输入并将创建的用户打印到控制台。我还添加了保存错误记录器。

案例表单

简单的 HTML 登录表单,它将包含三个js不同范式的有效文件。

过程化编程

过程式编程只是一步一步地解决问题。这是完全有效的编码方式,但是当您希望应用程序扩展时它有许多缺点。

简单一步一步解决问题。但它根本不可重用和可扩展。尽管它对于解决此类问题完全有效,并且您将看到它比其他问题要短得多。

面向对象编程

面向对象编程 ( OOP ) 是最接近现实世界的,因此很容易让您思考。我们查看将其划分为Object的代码,其中每个都只完成它的工作。在OOP 中学习的有用概念是SOLID。

现在你可以明白我将问题划分为Objects 的意思了:

正如你所看到的,有更多的代码,看起来更复杂……那么为什么有人会喜欢这个?

酷的是,现在我们可以将它用于任何类似的形式,只需调用:

因此,它可以在包含此脚本的每个文件中重复使用。而且它很容易扩展,因为一切都被分成只做一件事的块(单一责任原则)。

函数式编程

非常流行,而且非常简单。请注意,这并不意味着它无论如何都更好。尽管某些范例可能对某些问题更好,但使用哪个完全取决于您。

正如您在 函数式编程 中看到的,我们希望使用小的(理想情况下是纯函数)函数来解决问题。这种方法也非常具有可 扩展性 ,并且函数可以重用。

纯函数是一种没有难以追踪的副作用的函数。纯函数应该只依赖于给定的参数。

结论

没有更好和更坏的范式。有经验的开发人员可以看到每个的优点,并为给定的问题选择最好的。

过程式编程并不是说你不能使用函数,函数式编程也不会阻止你使用“类”。这些范式只是帮助以一种随着代码增长而有益的方式来解决问题。

函数式编程 vs 面向对象编程 vs 过程式编程的JS演示比较 - DEV

函数式编程:

知道如何使用纯函数进行声明式编程是一个人应该具备的另一种关键技能,因为它可以完全改变你的编码方式,变得更好。使用函数式编程,您可以纯粹通过函数编写代码,避免共享状态、数据突变和副作用。由于函数式编程的声明性质,生成的代码更简洁、可预测并且更易于测试和更改。此外,由于它简洁,可以更快地执行代码,从而缩短加载时间。学习函数式编程可能既困难又耗时,但它所带来的优势使其值得花时间。

编写跨浏览器代码:

一般来说,互联网用户不会坚持只使用一个网络浏览器;有些人可能更喜欢使用Internet Explorer,而其他人可能更喜欢使用 Google Chrome。在这种情况下,您无法编写在单个 Web 浏览器上运行的代码,因为这会严重限制您的网站受众并阻碍您网站的增长潜力

为了确保您的网站可以在流行的浏览器、移动设备和任何其他网络浏览设备上使用,编写与多个网络浏览器兼容的 JavaScript 代码至关重要。

高效的内存管理:

编写内存高效的 JavaScript 代码的能力是一项随着时间的推移而发展起来的技能,但您仍然应该知道这一技能。任何 Web 应用程序或网站都可能泄露内存,如果代码不适合内存优化,则会导致速度变慢。内存泄漏有时甚至会导致大量性能问题,因为它们最终会消耗大量宝贵的系统资源。尽管 JavaScript 有自己的自动内