Skip to content

Latest commit

 

History

History
497 lines (383 loc) · 19.5 KB

File metadata and controls

497 lines (383 loc) · 19.5 KB

七、JavaScript 中的函数式和面向对象编程

你会经常听到 JavaScript 是一种空白语言,其中空白要么是面向对象的,要么是函数式的,要么是通用的。这本书把重点放在了作为函数式语言的 JavaScript 上,并竭尽全力证明了这一点。但事实是,JavaScript 是一种通用语言,这意味着它完全能够支持多种编程风格。像 Python 和 F#一样,JavaScript 是多范式的。但与这些语言不同,JavaScript 的 OOP 端是基于原型的,而大多数其他通用语言是基于类的。

在这最后一章中,我们将把函数式和面向对象编程都与 JavaScript 联系起来,并看看这两种范式如何互补并共存。本章将涵盖以下主题:

  • JavaScript 如何做到既有功能又有 OOP?
  • JavaScript 的面向对象程序——使用原型
  • 如何在 JavaScript 中混合函数式和 OOP
  • 功能继承
  • 功能混合物

更好的代码是目标。函数式和面向对象编程只是达到这一目的的手段。

JavaScript——多范式语言

如果面向对象编程意味着把所有变量都当作对象,函数编程意味着把所有函数都当作变量,那么函数就不能当作对象吗?在 JavaScript 中,他们可以。

但是说函数式编程意味着把函数当作变量,这有点不准确。一个更好的说法是:函数式编程意味着将一切都视为一个值,尤其是函数。

更好的描述函数式编程的方法可能是称之为声明式的。独立于编程风格的命令分支,声明式编程表达了解决问题所需的计算逻辑。电脑被告知问题是什么,而不是解决问题的程序。

同时,面向对象编程是从命令式编程风格衍生而来的:计算机被一步一步地给出如何解决问题的指令。OOP 要求将计算指令(方法)和它们处理的数据(成员变量)组织成称为对象的单元。访问这些数据的唯一方法是通过对象的方法。

那么这两种风格如何融合在一起呢?

  • 对象方法中的代码通常以命令式的方式编写。但是如果它是功能型的呢?毕竟,OOP 不排除不可变数据和高阶函数。
  • 也许混合这两者的更纯粹的方法是将对象同时视为函数和传统的基于类的对象。
  • 也许我们可以简单地将函数式编程的几个想法——比如承诺和递归——包含到我们的面向对象应用中。
  • OOP 涵盖了封装、多态和抽象等主题。函数式编程也是如此,只是方式不同。因此,也许我们可以在面向函数的应用中包含几个来自面向对象编程的想法。

重点是:OOP 和 FP 可以混合在一起,有几种方法可以做到。他们并不互相排斥。

JavaScript 的面向对象实现——使用原型

JavaScript 是一种无类语言。这并不是说比其他计算机语言不那么时髦或者更蓝领;无类意味着它没有像面向对象语言那样的类结构。相反,它使用原型进行继承。

尽管这可能会让有 C++和 Java 背景的程序员感到困惑,但基于原型的继承比传统继承更具表现力。以下是对 C++和 JavaScript 的区别的简要对比:

|

C++

|

Java Script 语言

| | --- | --- | | 强类型 | 松散类型 | | 静态 | 动态的 | | 基于类 | 基于原型的 | | 班级 | 功能 | | 构造器 | 功能 | | 方法 | 功能 |

遗传

在更进一步之前,让我们确保完全理解面向对象编程中的继承概念。下面的伪代码演示了基于类的继承:

class Polygon {
  int numSides;
  function init(n) {
    numSides = n;
  }
}
class Rectangle inherits Polygon {
  int width;
  int length;
  function init(w, l) {
    numSides = 4;
    width = w;
    length = l;
  }
  function getArea() {
    return w * l;
  }
}
class Square inherits Rectangle {
  function init(s) {
    numSides = 4;
    width = s;
    length = s;
  }
}

Polygon类是其他类继承的父类。它只定义了一个成员变量,边的数量,这是在init()函数中设置的。Rectangle子类继承了Polygon类,并增加了两个成员变量lengthwidth以及一个方法getArea()。它不需要定义numSides变量,因为它已经被它继承的类定义了,并且它还覆盖了init()函数。Square类通过从Rectangle类继承其getArea()方法,进一步延续了这一继承链。通过简单地再次覆盖init()函数,使得长度和宽度相同,getArea()函数可以保持不变,并且需要编写更少的代码。

在传统的 OOP 语言中,这就是继承的意义所在。如果我们想给所有对象添加一个颜色属性,我们所要做的就是把它添加到Polygon对象中,而不需要修改从它继承的任何对象。

JavaScript 的原型链

JavaScript 中的继承归结为原型。每个对象都有一个内部属性,称为其原型,是另一个对象的链接。那个物体有自己的原型。这个模式可以重复,直到到达一个以undefined为原型的物体。这就是所谓的原型链,这就是继承在 JavaScript 中的工作方式。下图解释了 JavaScirpt 中的继承:

JavaScript's prototype chain

当运行对对象函数定义的搜索时,JavaScript“遍历”原型链,直到找到具有正确名称的函数的第一个定义。因此,覆盖它就像在子类的原型上提供一个新的定义一样简单。

JavaScript 中的继承和 Object.create()方法

正如在 JavaScript 中有很多创建对象的方法一样,也有很多复制基于类的经典继承的方法。但是最好的方法是Object.create()方法。

var Polygon = function(n) {
  this.numSides = n;
}

var Rectangle = function(w, l) {
  this.width = w;
  this.length = l;
}

// the Rectangle's prototype is redefined with Object.create
Rectangle.prototype = Object.create(Polygon.prototype);

// it's important to now restore the constructor attribute
// otherwise it stays linked to the Polygon
Rectangle.prototype.constructor = Rectangle;

// now we can continue to define the Rectangle class
Rectangle.prototype.numSides = 4;
Rectangle.prototype.getArea = function() {
  return this.width * this.length;
}

var Square = function(w) {
  this.width = w;
  this.length = w;
}
Square.prototype = Object.create(Rectangle.prototype);
Square.prototype.constructor = Square;

var s = new Square(5);
console.log( s.getArea() ); // 25

这个语法对很多人来说似乎不太寻常,但是只要稍加练习,它就会变得熟悉。必须使用prototype关键字来访问所有对象都拥有的内部属性[[Prototype]]Object.create()方法声明了一个新的对象,该对象带有一个指定的对象供原型继承。通过这种方式,可以在 JavaScript 中实现经典继承。

Object.create()方法于 2011 年在 ECMAScript 5.1 中引入,并被宣传为创建对象的新的首选方式。这只是将继承集成到 JavaScript 中的众多尝试之一。谢天谢地,这种方法非常有效。

我们在第五章范畴论中构建Maybe类时看到了这个继承结构。这里有MaybeNoneJust类,它们像前面的例子一样相互继承。

var Maybe = function(){}; 

var None = function(){}; 
None.prototype = Object.create(Maybe.prototype);
None.prototype.constructor = None;
None.prototype.toString = function(){return 'None';};

var Just = function(x){this.x = x;};
Just.prototype = Object.create(Maybe.prototype);
Just.prototype.constructor = Just;
Just.prototype.toString = function(){return "Just "+this.x;};

这表明 JavaScript 中的类继承可以成为函数式编程的使能器。

一个常见的错误是将构造函数传递给Object.create()而不是prototype对象。这个问题由于这样一个事实而变得更加复杂:在子类尝试使用继承的成员函数之前,不会抛出错误。

Foo.prototype = Object.create(Parent.prototype); // correct
Bar.prototype = Object.create(Parent); // incorrect
Bar.inheritedMethod(); // Error: function is undefined

如果inheritedMethod()方法已经附加到Foo.prototype类上,就找不到该函数。如果在Bar构造函数中将inheritedMethod()方法直接附加到带有this.inheritedMethod = function(){...}的实例,那么将Parent用作Object.create()的参数可能是正确的。

在 JavaScript 中混合函数式和面向对象编程

几十年来,面向对象编程一直是占主导地位的编程范式。它是在世界各地的计算机科学 101 课上教的,而函数编程不是。它是软件架构师用来设计应用的,而函数式编程不是。这也很有意义:面向对象使得抽象概念概念化变得容易。这使得编写代码更加容易。

因此,除非你能让你的老板相信应用需要全部是功能性的,否则我们将在面向对象的世界中使用功能性编程。本节将探讨如何做到这一点。

功能遗传

也许将函数式编程应用于 JavaScript 应用的最容易的方式是在 OOP 原则中使用一种主要是函数式的风格,比如继承。

为了探索这是如何工作的,让我们构建一个简单的应用来计算产品的价格。首先,我们需要一些产品类别:

var Shirt = function(size) {
  this.size = size;
};

var TShirt = function(size) {
  this.size = size;
};
TShirt.prototype = Object.create(Shirt.prototype);
TShirt.prototype.constructor = TShirt;
TShirt.prototype.getPrice = function(){
  if (this.size == 'small') {
    return 5;
  }
  else {
    return 10;
  }
}

var ExpensiveShirt = function(size) {
  this.size = size;
}
ExpensiveShirt.prototype = Object.create(Shirt.prototype);
ExpensiveShirt.prototype.constructor = ExpensiveShirt;
ExpensiveShirt.prototype.getPrice = function() {
  if (this.size == 'small') {
    return 20;
  }
  else {
    return 30;
  }
}

我们可以将他们组织在一个Store班级内,如下所示:

var Store = function(products) {
  this.products = products;
}
Store.prototype.calculateTotal = function(){
  return this.products.reduce(function(sum,product) {
    return sum + product.getPrice();
  }, 10) * TAX; // start with $10 markup, times global TAX var
};

var TAX = 1.08;
var p1 = new TShirt('small');
var p2 = new ExpensiveShirt('large');
var s = new Store([p1,p2]);
console.log(s.calculateTotal()); // Output: 35

calculateTotal()方法使用数组的reduce()函数将产品的价格干净地加在一起。

这很好,但是如果我们需要一种动态的方法来计算标记值呢?为此,我们可以求助于一个名为战略模式的概念。

战略模式

策略模式是定义一系列可互换算法的方法。它被 OOP 程序员用来在运行时操纵行为,但它基于几个函数式编程原则:

  • 逻辑和数据的分离
  • 职能的组成
  • 作为一级对象的功能

以及一些面向对象的原则:

  • 包装
  • 遗产

在我们计算产品成本的示例应用中,如前所述,假设我们希望给予某些客户优惠待遇,并且必须调整加价以反映这一点。

让我们创建一些客户类:

var Customer = function(){};
Customer.prototype.calculateTotal = function(products) {
  return products.reduce(function(total, product) {
    return total + product.getPrice();
  }, 10) * TAX;
};

var RepeatCustomer = function(){};
RepeatCustomer.prototype = Object.create(Customer.prototype);
RepeatCustomer.prototype.constructor = RepeatCustomer;
RepeatCustomer.prototype.calculateTotal = function(products) {
  return products.reduce(function(total, product) {
    return total + product.getPrice();
  }, 5) * TAX;
};

var TaxExemptCustomer = function(){};
TaxExemptCustomer.prototype = Object.create(Customer.prototype);
TaxExemptCustomer.prototype.constructor = TaxExemptCustomer;
TaxExemptCustomer.prototype.calculateTotal = function(products) {
  return products.reduce(function(total, product) {
    return total + product.getPrice();
  }, 10);
};

每个Customer类封装算法。现在我们只需要Store类调用Customer类的calculateTotal()方法。

var Store = function(products) {
  this.products = products;
  this.customer = new Customer();
  // bonus exercise: use Maybes from Chapter 5 instead of a default customer instance
}
Store.prototype.setCustomer = function(customer) {
  this.customer = customer;
}
Store.prototype.getTotal = function(){
  return this.customer.calculateTotal(this.products);
};

var p1 = new TShirt('small');
var p2 = new ExpensiveShirt('large');
var s = new Store([p1,p2]);
var c = new TaxExemptCustomer();
s.setCustomer(c);
s.getTotal(); // Output: 45

Customer类进行计算,Product类保存数据(价格),Store类维护上下文。这实现了非常高的内聚性,以及面向对象编程和函数编程的良好混合。JavaScript 的高水平表达能力使这成为可能,而且相当容易。

Mixins

简而言之,mixins 是允许其他类使用它们的方法的类。方法旨在成为其他类单独使用的,而mixin类本身永远不会被实例化。这有助于避免继承模糊。它们是混合函数式编程和面向对象编程的好方法。

Mixins 在每种语言中都有不同的实现。由于 JavaScript 的灵活性和表现力,mixins 被实现为只有方法的对象。虽然它们可以被定义为函数对象(即var mixin = function(){...};,但是对于代码的结构规则来说,将它们定义为对象文字(即var mixin = {...};)会更好。这将帮助我们区分类和 mixins。毕竟,mixins 应该被视为进程,而不是对象。

让我们从声明一些 mixins 开始。我们将从上一节扩展我们的Store应用,使用 mixins 扩展类。

var small = {
  getPrice: function() {
    return this.basePrice + 6;   
  },
  getDimensions: function() {
    return [44,63]
  }
}
var large = {
  getPrice: function() {
    return this.basePrice + 10;   
  },
  getDimensions: function() {
    return [64,83]
  }
};

我们不仅限于此。可以添加更多的混合物,如颜色或织物材料。我们将不得不稍微重写我们的Shirt类,如下面的代码片段所示:

var Shirt = function() {
  this.basePrice = 1;
};
Shirt.getPrice = function(){
  return this.basePrice;
}
var TShirt = function() {
  this.basePrice = 5;
};
TShirt.prototype = Object.create(Shirt.prototype);
TShirt..prototype.constructor = TShirt;

现在我们准备使用 mixins。

经典混合蛋白

你可能想知道这些混合蛋白是如何与 T2 类混合的。经典的方法是将 mixin 的函数复制到接收对象中。这可以通过对Shirt原型进行以下扩展来实现:

Shirt.prototype.addMixin = function (mixin) {
  for (var prop in mixin) {
    if (mixin.hasOwnProperty(prop)) {
      this.prototype[prop] = mixin[prop];
    }
  }
};

现在可以按如下方式添加 mixins:

TShirt.addMixin(small);
var p1 = new TShirt();
console.log( p1.getPrice() ); // Output: 11

TShirt.addMixin(large);
var p2 = new TShirt();
console.log( p2.getPrice() ); // Output: 15

然而,有一个主要问题。当p1的价格再计算一次,回来就是15,一个大件的价格。应该是小的值吧!

console.log( p1.getPrice() ); // Output: 15

问题是Shirt对象的prototype.getPrice()方法在每次添加 mixin 时都会被重写;这一点都不实用,也不是我们想要的。

功能性融合蛋白

还有另一种使用 mixins 的方式,一种更符合功能编程的方式。

我们不需要将 mixin 的方法复制到目标对象,而是需要创建一个新的对象,该对象是添加了 mixin 方法的目标对象的克隆。首先必须克隆对象,这是通过创建一个新的对象来实现的。我们称之为变异plusMixin

Shirt.prototype.plusMixin = function(mixin) {    
  // create a new object that inherits from the old
  var newObj = this;
  newObj.prototype = Object.create(this.prototype);
  for (var prop in mixin) {
    if (mixin.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixin[prop];
    }
  }
  return newObj;
};

var SmallTShirt = Tshirt.plusMixin(small); // creates a new class
var smallT = new SmallTShirt();
console.log( smallT.getPrice() );  // Output: 11

var LargeTShirt = Tshirt.plusMixin(large);
var largeT = new LargeTShirt();
console.log( largeT.getPrice() ); // Output: 15
console.log( smallT.getPrice() ); // Output: 11 (not effected by 2nd mixin call)

有趣的部分来了!现在我们可以使用 mixins 了。我们可以创造各种可能的产品和混合物组合。

// in the real world there would be way more products and mixins!
var productClasses = [ExpensiveShirt, Tshirt]; 
var mixins = [small, medium, large];

// mix them all together 
products = productClasses.reduce(function(previous, current) {
  var newProduct = mixins.map(function(mxn) {
    var mixedClass = current.plusMixin(mxn);
    var temp = new mixedClass();
    return temp;
  });
  return previous.concat(newProduct);
},[]);
products.forEach(function(o){console.log(o.getPrice())});

为了使更加面向对象,我们可以用这个功能重写Store对象。我们还将向Store对象而不是产品添加显示功能,以保持接口逻辑和数据的分离。

// the store
var Store = function() {
  productClasses = [ExpensiveShirt, TShirt];
  productMixins = [small, medium, large];
  this.products = productClasses.reduce(function(previous, current) {
    var newObjs = productMixins.map(function(mxn) {
      var mixedClass = current.plusMixin(mxn);
      var temp = new mixedClass();
      return temp;
    });
    return previous.concat(newObjs);
  },[]);
}
Store.prototype.displayProducts = function(){
  this.products.forEach(function(p) {
    $('ul#products').append('<li>'+p.getTitle()+': $'+p.getPrice()+'</li>');
  });
}

而我们要做的就是创建一个Store对象,调用它的displayProducts()方法,生成一个产品和价格的列表!

<ul id="products">
  <li>small premium shirt: $16</li>
  <li>medium premium shirt: $18</li>
  <li>large premium shirt: $20</li>
  <li>small t-shirt: $11</li>
  <li>medium t-shirt: $13</li>
  <li>large t-shirt: $15</li>
</ul>

需要将这些行添加到product类和 mixins 中,以使前面的输出生效:

Shirt.prototype.title = 'shirt';
TShirt.prototype.title = 't-shirt';
ExpensiveShirt.prototype.title = 'premium shirt';

// then the mixins got the extra 'getTitle' function:
var small = {
  ...
  getTitle: function() {
    return 'small ' + this.title; // small or medium or large
  }
}

就像这样,我们有一个高度模块化和可扩展的电子商务应用。新的衬衫款式添加起来非常容易——只需定义一个新的Shirt子类,并在其中添加Store类的数组product类。Mixins 也是以同样的方式添加的。所以现在,当我们的老板说:“嘿,我们有一种新型衬衫和一件外套,每件都有标准颜色,我们需要在你今天回家之前把它们添加到网站上”时,我们可以放心,我们不会熬夜!

总结

JavaScript 具有很高的表现力。这使得混合函数式和面向对象编程成为可能。现代 JavaScript 不仅仅是面向对象或功能性的——它是两者的混合。像 Strategy Pattern 和 mixins 这样的概念非常适合 JavaScript 的原型结构,它们有助于证明当今 JavaScript 的最佳实践共享等量的函数式编程和面向对象编程。

如果你只从这本书里拿走一件事,我希望它是如何将函数式编程技术应用于现实世界的应用。本章向你展示了如何做到这一点。