JavaScript中的工厂函数vs构造函数vs class

原文连接:JavaScript Factory Functions vs Constructor Functions vs Classes
做者:Eric Elliott
译者:sunny
转载需提早联系译者,未经容许不得转载。
本文首发于前端指南javascript

在ES6以前,JavaScript中的工厂函数和构造函数之间的差别令许多人困惑。因为ES6有了“class”关键字,不少人认为它解决了不少构造函数的问题。其实并无。让咱们来了解一下你仍然须要注意的事项。前端

咱们首先来看一个例子:java

// class
class ClassCar {
  drive () {
    console.log('Vroom!');
  }
}

const car1 = new ClassCar();
console.log(car1.drive());


// constructor
function ConstructorCar () {}

ConstructorCar.prototype.drive = function () {
  console.log('Vroom!');
};

const car2 = new ConstructorCar();
console.log(car2.drive());


// factory
const proto = {
  drive () {
    console.log('Vroom!');
  }
};

function factoryCar () {
  return Object.create(proto);
}
const car3 = factoryCar();
console.log(car3.drive());

每种方式都用到了原型,而且有选择地使用构造函数来建立私有变量。换句话说,它们有不少相同的特性,大多数状况下均可以互换使用。git

In JavaScript, any function can return a new object. When it’s not a constructor function or class, it’s called a factory function.es6

ES6的class是构造函数的语法糖,因此适用于构造函数的内容也适用于ES6的class:github

class Foo {}
console.log(typeof Foo); // function

构造函数和class的优势

  • 大部分书都会教你使用class或者是构造函数web

  • 'this'指向建立的新对象安全

  • 有些人喜欢myFoo = new Foo()这样的写法app

  • 可能会有一些性能上的微弱优点,可是基本不须要担忧,除非你对代码进行了分析而且证实这些差距对你而言很是重要。socket

构造函数和class的缺点

  1. 须要new

在ES6以前,忘记new是一种常见的bug。不少人都会用样板来解决这个问题:

function Foo() {
  if (!(this instanceof Foo)) { return new Foo(); }
}

在ES6(ES2015)中,若是你调用构造函数和class的时候忘记了new,会抛出错误。若是不将class包装在工厂函数中,那么难以免强迫调用者使用new。也有人建议在将来的JavaScript版本中能够容许调用者自定义调用行为时能够省略new,但这也意味着会给每一个使用它的class增长额外开销(也意味着不多使用它)。

  1. 实例化的细节被泄漏到了调用它的API(经过new)

全部的调用者都和构造函数的实现紧密耦合。若是你须要工厂的额外的灵活性,重构是一种突破性的变化。class到工厂的重构是常见的,他们出如今了Martin Fowler,Kent Beck,John Brant,William Opdyke和Don Roberts的创新重构的书:《Refactoring: Improving the Design of Existing Code》中。

  1. 构造函数违背了开闭原则

因为使用了new,构造器函数违背了开闭原则:接口对扩展开放,对修改关闭。

我认为class到工厂的重构是很是广泛的,它应该被做为构造函数扩展的标准。从class到工厂的升级原本不该该打破什么,可是在JavaScript中,它会。

若是你已经开始导出构造函数或是类,而且用户开始使用构造函数,慢慢你会意识到工厂的灵活性是很是重要的(例如:选择对象池来实现,或在执行上下文中实例化对象、或使用可选择的原型以得到更多的灵活性),你不能达到目标除非强制调用者重构。

不幸的是,在JavaScript中,从构造函数或class切换到工厂须要打破这种变化。

// Original Implementation:

// class Car {
//   drive () {
//     console.log('Vroom!');
//   }
// }

// const AutoMaker = { Car };

// Factory refactored implementation:
const AutoMaker = {
  Car (bundle) {
    return Object.create(this.bundle[bundle]);
  },

  bundle: {
    premium: {
      drive () {
        console.log('Vrooom!');
      },
      getOptions: function () {
        return ['leather', 'wood', 'pearl'];
      }
    }
  }
};

// The refactored factory expects:
const newCar = AutoMaker.Car('premium');
newCar.drive(); // 'Vrooom!'

// But since it's a library, lots of callers
// in the wild are still doing this:
const oldCar = new AutoMaker.Car();

// Which of course throws:
// TypeError: Cannot read property 'undefined' of
// undefined at new AutoMaker.Car

在上边这个例子中,咱们开始时使用了class,可是咱们想要提升可用性,增长不一样的车子类型。为了实现这个目标,工厂为不一样的车子提供了可选择的prototype。我曾经用这项技术实现了不一样的媒体播放器的接口,根据须要控制的播放器来选择正确的prototype。

  1. 使用构造函数会致使“instanceof”的欺骗性

由构造器向工厂的重构其中一种突破性变化就是‘instanceof’。有时人们会试图用“instanceof”来检查代码中的数据类型。这就会致使问题,我建议你避免使用“instanceof”

// instanceof is a prototype identity check.
// NOT a type check.

// That means it lies across execution contexts,
// when prototypes are dynamically reassigned,
// and when you throw confusing cases like this
// at it:

function foo() {}
const bar = { a: 'a'};

foo.prototype = bar;

// Is bar an instance of foo? Nope!
console.log(bar instanceof foo); // false

// Ok... since bar is not an instance of foo,
// baz should definitely not be an instance of foo, right?
const baz = Object.create(bar);

// ...Wrong.
console.log(baz instanceof foo); // true. oops.

“instanceof”并无作到你指望的类型检查。相反,它进行了身份认证,将对象的prototype对象与构造器的prototype属性进行比较。

在执行上下文中是不会起做用的例如(一般是bug、沮丧和没必要要的限制的缘由)。若是你的构造函数的prototype被替换,也不会发挥做用。

若是你在把构造函数转换成工厂方法的时候,使用class或者构造函数(返回“this”,与构造函数的prototype属性相关),而后切换到任意对象(没有与构造函数的prototype属性相关),也会致使失败。

简而言之,“instanceof”是从构造函数切换到工厂方法时的另外一种突破性的变化。

使用class的优势

  • 简便、独立的语法

  • 单一的、规范的模仿JavaScript中类的方法。在ES6以前,还有几种流行库中的不一样的实现方法。

  • 人们更加熟悉基于类的语言。

使用类的缺点

全部构造函数的缺点,还有:

  • 诱惑用户使用extends关键字建立多层次的class,很容易致使问题。

多层次的class会致使面向对象中一些众所周知的问题,包括:脆弱的基类问题、大猩猩香蕉问题、必要性致使的重复问题等等。不幸的是,class提供了像球能够投掷、椅子能够坐这样的扩展。更多详情,请阅读 “The Two Pillars of JavaScript: Prototypal OO”“Inside the Dev Team Death Spiral”两篇文章。

构造函数和工厂均可以用来建立多层次的结构,可是class能够用extends关键字致使你走向错误的方向。换句话说,它鼓励你考虑不灵活的(一般是错的)is-a关系,而不是更灵活的has-a或者是can-do成分关系。

另外一个提供的特性是支持执行特定行为的机会。例如,旋钮能够旋转,杠杆能够拉动,按钮能够按压等等。

使用工厂的优势

工厂比构造函数和class更加灵活,同时也不会用extends关键字和层次继承引导人们走向错误的方向。你可使用不一样的方法从工厂方法继承。特别是,用组合工工厂函数检查邮票规格

  1. 返回任意对象,使用任意原型

例如:你能够很容易建立实现了一样接口的不一样的对象。一种媒体播放器,它能够实例化多种视频的播放,这些视频在引擎下使用不一样的API,它们的事件库使用了不一样的Dom事件或web socket事件。

工厂函数也会根据执行上下文实例化对象,利用对象池的优点,这容许更灵活的基于原型的继承。

  1. 不用担忧重构

你历来都不须要把工厂转换为构造函数,因此重构不会是问题。

  1. 不须要new

new没有歧义,不要使用(这会形成this指向不明确,请看下一点)。

  1. 标准的this

this指向出了它该指向的,因此你能够经过它来获取父对象。例如:player.create(),this指向了player,就像call和apply方法肯定了this的指向。

  1. 不会有欺骗instanceof

  2. 有些人喜欢“myFoo=createFoo()”这种方式

工厂方法的缺点

  • 不会建立对象与工厂的prototype之间的联系,但这其实是一个好事情,由于你不会被instanceof欺骗。相反,isntanceof会失败。请看优势。

  • this没有指向工厂中的新对象。请看优势。

  • 在微优化的基准测试中,它可能比构造函数的执行速度慢。若是非要测试的话,请确保你须要关注这个问题。

总结

在我看来,class确实有简便的语法,可是实际上它可能会诱导用户创造继承的class。这也是有风险的,在将来,你可能会升级为工厂,可是因为new关键字的使用,全部的调用者都与你的构造函数紧密耦合,从类转换为工厂,会致使突破性的变化。

你可能考虑你只须要重构调用部分,可是在大型团队中,或者你的class是公共API的一部分,你不可能去修改不在你控制下的代码。换句话说,你不能老是假设重构调用者是可能的选项。

工厂模式不只仅更增强大灵活,同时也会鼓励整个团队、全部的API用户使用一种简单、灵活安全的模式。
关于工厂模式其实还有不少内容,尤为是关于使用邮票规格进行对象组合的效用。更多关于这个话题,以及与class的不一样,请阅读“3 Different Kinds of Prototypal Inheritance”

相关文章
相关标签/搜索