FP
在 FP 中,函数接受输入并产生输出,保证相同的输入会产生相同的输出。为了做到这一点,函数必须始终为其操作的值具有参数,并且不能依赖于状态。即,如果一个函数依赖于状态,并且该状态发生变化,则函数的输出可能会有所不同。 FP 不惜一切代价避免这种情况。
我们将在 FP 和 OOP 中展示map 的最小实现。在下面这个 FP 示例中,请注意 map 如何仅对局部变量进行操作,而不依赖于状态 -
const _ = {
// ??has two parameters
map: function (arr, fn) {
// ??local
if (arr.length === 0)
return []
else
// ??local
// ??local // ??local // ??local
return [ fn(arr[0]) ].concat(_.map(arr.slice(1), fn))
}
}
const result =
// ??call _.map with two arguments
_.map([1, 2, 3], function(n){ return n * 2; })
console.log(result)
// [ 2, 4, 6 ]
在这种风格中,map 是否存储在 _ 对象中并不重要 - 因为使用了对象,所以不会使其成为“OOP”。我们可以很容易地写出来 -
function map (arr, fn) {
if (arr.length === 0)
return []
else
return [ fn(arr[0]) ].concat(map(arr.slice(1), fn))
}
const result =
map([1, 2, 3], function(n){ return n * 2; })
console.log(result)
// [ 2, 4, 6 ]
这是在 FP 中调用的基本方法 -
// ??function to call
// ??argument(s)
someFunction(arg1, arg2)
这里的 FP 值得注意的是 map 有两 (2) 个参数,arr 和 fn,map 的输出仅取决于这些输入。您将在下面的 OOP 示例中看到这种情况发生了巨大变化。
面向对象
在 OOP 中,对象用于存储状态。当调用对象的方法时,方法(函数)的上下文动态绑定到接收对象,为this。因为this 是一个不断变化的 值,OOP 不能保证任何方法都具有相同的输出,即使给出相同的输入也是如此。
注意map 如何只接受下面的一 (1) 个参数,fn。我们如何只使用fn 来map?我们将map 做什么?如何将目标指定为map? FP 认为这是一场噩梦,因为函数的输出不再仅仅取决于其输入 - 现在map 的输出更难确定,因为它取决于this 的动态 值 -
// ??constructor
function _ (value) {
// ??returns new object
return new OOP(value)
}
function OOP (arr) {
// ??dynamic
this.arr = arr
}
// ??only one parameter
OOP.prototype.map = function (fn) {
// ??dynamic
if (this.arr.length === 0)
return []
else // ??dynamic // ??dynamic
return [ fn(this.arr[0]) ].concat(_(this.arr.slice(1)).map(fn))
}
const result =
// ??create object
// ??call method on created object
// ??with one argument
_([1, 2, 3]).map(function(n){ return n * 2; })
console.log(result)
// [ 2, 4, 6 ]
这是 OOP 中动态调用的基本方法 -
// ??state
// ??bind state to `this` in someAction
// ??argument(s) to action
someObj.someAction(someArg)
重新审视 FP
在第一个 FP 示例中,我们看到 .concat 和 .slice - 这些不是 OOP 动态调用吗?它们是,但特别是这些不会修改输入数组,因此它们可以安全地与 FP 一起使用。
也就是说,调用风格的混合可能有点令人眼花缭乱。 OOP 倾向于“中缀”表示法,其中方法(函数)显示在 函数的参数之间 -
// ??arg1
// ??function
// ??arg2
user .isAuthenticated (password)
这也是 JavaScript 运算符的工作方式 -
// ??arg1
// ??function
// ??arg2
1 + 2
FP 偏爱“前缀”表示法,其中函数始终位于其参数之前。在理想情况下,我们可以在 any 位置调用 OOP 方法和运算符,但不幸的是 JS 不能这样工作 -
// ??invalid use of method
.isAuthenticated(user, password)
// ??invalid use of operator
+(1,2)
通过将.conat和.slice等方法转换为函数,我们可以更自然地编写FP程序。注意前缀符号的一致使用如何更容易想象计算是如何进行的 -
function map (arr, fn) {
if (isEmpty(arr))
return []
else
return concat(
[ fn(first(arr)) ]
, map(rest(arr), fn)
)
}
map([1, 2, 3], function(n){ return n * 2; })
// => [ 2, 4, 6 ]
方法转换如下-
function concat (a, b) {
return a.concat(b)
}
function first (arr) {
return arr[0]
}
function rest (arr) {
return arr.slice(1)
}
function isEmpty (arr) {
return arr.length === 0
}
这开始显示 FP 的其他优势,其中函数保持较小并专注于一项任务。而且因为这些函数只对它们的输入进行操作,所以我们可以在程序的其他区域轻松地重用它们。
您的问题最初是在 2016 年提出的。从那时起,现代 JS 功能允许您以更优雅的方式编写 FP -
const None = Symbol()
function map ([ value = None, ...more ], fn) {
if (value === None)
return []
else
return [ fn(value), ...map(more, fn) ]
}
const result =
map([1, 2, 3], function(n){ return n * 2; })
console.log(result)
// [ 2, 4, 6 ]
使用expressions 代替statements 的进一步改进-
const None = Symbol()
const map = ([ value = None, ...more ], fn) =>
value === None
? []
: [ fn(value), ...map(more, fn) ]
const result =
map([1, 2, 3], n => n * 2)
console.log(result)
// [ 2, 4, 6 ]
语句依赖于副作用,而表达式则直接计算为一个值。表达式在代码中留下的潜在“漏洞”更少,语句可以随时执行任何操作,例如抛出错误或退出函数而不返回值。
FP 与对象
FP 并不意味着“不要使用对象”——它是关于保留轻松推理程序的能力。我们可以编写相同的map 程序,给人一种我们正在使用OOP 的错觉,但实际上它的行为更像FP。它看起来像一个方法调用,但实现只依赖于局部变量,不依赖于动态状态 (this)。
JavaScript 是一种丰富、富有表现力的多范式语言,它允许您编写程序以满足您的需求和偏好 -
function _ (arr) {
function map (fn) {
// ??local
if (arr.length === 0)
return []
else
// ??local
// ??local // ??local // ??local
return [ fn(arr[0]) ].concat(_(arr.slice(1)).map(fn))
}
// ??an object!
return { map: map }
}
const result =
// ??OOP? not quite!
_([1, 2, 3]).map(function(n){ return n * 2; })
console.log(result)
// [ 2, 4, 6 ]