初学者 JavaScript OOP 与函数式

Beginner JavaScript OOP vs Functional

我刚刚开始研究不同的编程风格(OOP、函数式、过程式)。

我正在学习 JavaScript 并开始学习 underscore.js 并出现在文档中的 this 一小部分。 文档说 underscore.js 可以用在面向对象或函数式风格中,并且这两者会产生相同的结果。

_.map([1, 2, 3], function(n){ return n * 2; });
_([1, 2, 3]).map(function(n){ return n * 2; });

我不明白哪个是函数式的,哪个是 OOP,我也不明白为什么,即使在对这些编程范例进行了一些研究之后也是如此。

函数式: 将对象传递给函数并执行操作

_.map([1, 2, 3], function(n){ return n * 2; });

OOP: 你在对象上调用函数并做一些事情

_([1, 2, 3]).map(function(n){ return n * 2; });

在这两个例子中,[1,2,3] (array) 是一个对象。

示例 OOP 参考:http://underscorejs.org/#times

没有正确定义什么是什么不是"functional",但函数式语言通常强调数据和函数的简单性。

大多数函数式编程语言没有 类 和属于对象的方法的概念。函数在定义明确的数据结构上运行,而不是属于数据结构。

第一个样式_.map_命名空间中的一个函数。它是一个独立的函数,您可以 return 它或将它作为参数传递给另一个函数。

function compose(f, g) {
  return function(data) {
    return f(g(data));
  }
}

const flatMap = compose(_.flatten, _.map);

无法对第二种样式执行相同的操作,因为方法实例本质上与用于构造对象的数据相关联。所以我会说第一种形式是更多功能。

在任何一种情况下,一般的函数式编程风格都是数据应该是函数的最后一个参数,这样可以更容易地柯里化或部分应用前面的参数。 Lodash/fp and ramda 通过为地图添加以下签名来解决此问题。

_.map(func, data);

如果函数是柯里化的,您可以通过只传递第一个参数来创建函数的特定版本。

const double = x => x * 2;
const mapDouble = _.map(double);

mapDouble([1, 2, 3]);
// => [2, 4, 6]

编程范式

面向对象编程 (OOP) 和函数式编程 (FP) 是编程范例。粗略地说,遵循编程范式就是编写符合一组特定规则的代码。例如,将代码组织成单元称为 OOP,避免副作用称为 FP。

每种编程范式都由特定的功能组成,但是您最喜欢的语言不必提供属于一种范式的所有功能。事实上,OOP可以没有inheritance or encapsulation,所以我们可以说JavaScript(JS)是一门继承无封装的OOP语言。

既然您对什么是编程范式有了一点了解(希望如此),让我们快速了解一下 OOP 和 FP 的基础知识。

面向对象编程

在 OOP 中,对象是一个框,其中包含应该引用相同概念的信息和操作。信息通常被称为“属性”,操作通常被称为“方法”。属性允许跟踪对象的状态,方法允许操纵对象的状态。

在 JS 中,您可以向对象发送消息以执行特定方法。下面的代码展示了如何在 JS 中调用一个方法。 “point”对象有两个属性“x”和“y”,以及一个名为“translate”的方法。 “translate”方法根据给定的向量更新“point”的坐标。

point = {
  x: 10, y: 10,
  translate: function (vector) {
    this.x += vector.x;
    this.y += vector.y;
  }
};

point.x; // 10
point.translate({ x: 10, y: 0 });
point.x; // 20

这么简单的案例涉及的功能并不多。在 OOP 中,代码通常被划分为 类,并且通常支持继承和多态性。但我不会详细介绍,因为恐怕我已经超出了你的问题范围。

函数式编程

在FP中,代码本质上是函数的组合。此外,数据是不可变的,这导致编写没有副作用的程序。在函数式代码中,函数无法改变外部世界,输出值仅取决于给定的参数。

实际上,只要你处理好副作用,JS 就可以用作 FP 语言,没有内置机制。以下代码是此类编程风格的示例。 “zipWith”功能来自 Haskell 世界。它使用给定的函数合并两个列表,碰巧 add(point[i], vector[i]).

zipWith = function (f, as, bs) {
  if (as.length == 0) return [];
  if (bs.length == 0) return [];
  return [f(as[0], bs[0])].concat(
    zipWith(f, as.slice(1), bs.slice(1))
  );
};

add = function (a, b) {
  return a + b;
};

translate = function (point, vector) {
  return zipWith(add, point, vector);
};

point = [10, 10];
point[0]; // 10
point = translate(point, [10, 0]);
point[0]; // 20

虽然这个定义非常肤浅。 Haskell 例如,它是一种纯函数式语言,实现了更多的概念,例如函数组合、仿函数、柯里化、monad 等

结论

其实OOP和FP是两个不同的概念,没有共同点,甚至可以说没有可比性。因此,我认为您从 Underscore.js 文档中阅读的内容是对语言的误用。

你不应该在这个库的范围内学习编程范式。确实,您使用 Underscore.js 编写代码的方式使其类似于 OOP 和 FP,但这只是外观问题。因此,引擎盖下没有什么真正令人兴奋的:-)


参考维基百科进行深入阅读。

两者 map 都是函数式的,并且都是基于概念的代码,值 => 映射函数的值。

不过,两者也可以看出OOP,因为object.map风格。

我不建议您通过 Underscore 理解函数式编程。

FP

在 FP 中,函数接受输入并产生输出,并保证相同的输入会产生相同的输出。为了做到这一点,函数必须始终为其操作的值提供参数,并且不能依赖状态。即,如果一个函数依赖于状态,并且该状态发生变化,则该函数的输出可能会有所不同。 FP 不惜一切代价避免这种情况。

我们将展示 map 在 FP 和 OOP 中的最小实现。在下面的这个 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) 个参数,arrfnmap 的输出完全取决于这些输入。您将在下面的 OOP 示例中看到这发生了怎样的巨大变化。


OOP

在 OOP 中,对象用于存储状态。当一个对象的方法被调用时,方法(函数)的上下文动态绑定到接收对象this。因为 this 是一个 变化的 值,OOP 不能保证任何方法都会有相同的输出,即使给出相同的输入。

注意 map 如何只接受下面的一 (1) 个参数,fn。我们如何 map 仅使用 fn?我们会map做什么?如何将目标指定为 map? FP 认为这是一场噩梦,因为函数的输出不再仅取决于其输入 - 现在 map 的输出更难确定,因为它取决于 dynamic 的值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 赞成“前缀”表示法,其中函数始终位于其参数之前。在理想情况下,我们可以在 任何 位置调用 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 instead of 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 ]