【问题标题】:Beginner JavaScript OOP vs Functional初学者 JavaScript OOP 与函数式
【发布时间】:2016-09-10 22:33:17
【问题描述】:

我刚刚开始研究不同的编程风格(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,我也不明白为什么,即使在对这些编程范式进行了一些研究之后。

【问题讨论】:

标签: javascript oop functional-programming underscore.js


【解决方案1】:

编程范式

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

每个编程范式都由特定的功能组成,但是您最喜欢的语言不必提供所有功能才能归入一个范式。实际上,OOP 可以不用inheritanceencapsulation 也能生存,因此可以说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,但这只是外观问题。因此,引擎盖下没有什么真正令人兴奋的:-)


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

【讨论】:

  • 虽然这个答案对于初学者来说可能是一个很好的起点,但请不要相信我的话,我远非函数式编程专家:-)
  • 随着时间的推移对编程范式的精彩解释:youtu.be/Pg3UeB-5FdA.
【解决方案2】:

函数式:你将一个对象传递给函数并做一些事情

_.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

【讨论】:

    【解决方案3】:

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

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

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

    function compose(f, g) {
      return function(data) {
        return f(g(data));
      }
    }
    
    const flatMap = compose(_.flatten, _.map);
    

    不可能对第二种样式做同样的事情,因为方法实例本质上与用于构造对象的数据相关联。所以我会说第一种形式是更多功能性的。

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

    _.map(func, data);
    

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

    const double = x => x * 2;
    const mapDouble = _.map(double);
    
    mapDouble([1, 2, 3]);
    // => [2, 4, 6]
    

    【讨论】:

      【解决方案4】:

      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) 个参数,arrfnmap 的输出仅取决于这些输入。您将在下面的 OOP 示例中看到这种情况发生了巨大变化。


      面向对象

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

      注意map 如何只接受下面的一 (1) 个参数,fn。我们如何只使用fnmap?我们将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 ]

      【讨论】:

      • 你的回答太棒了!!
      • 我注意到帖子中有一个字符编码错误。我修复了它,现在代码 sn-ps 更容易阅读。感谢您的评论,亡魂:D
      • 你是如何在代码的 cmets 中编写这些手符号的?
      • @ErDeepakGarg 我使用了表情符号键盘。或者如果您无权访问其中之一,则只需复制/粘贴
      【解决方案5】:

      map 都是函数式的,而且都是基于 concpet 的代码,值 => 映射函数的值。

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-11-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-12-06
        相关资源
        最近更新 更多