JavaScript中的工厂方法、构造函数与class
本文转载自:众成翻译
译者:谢于中
链接:http://www.zcfy.cc/article/1129
原文:https://medium.com/javascript-scene/javascript-factory-functions-vs-constructor-functions-vs-classes-2f22ceddf33e#.wby148xu6
在ES6出现之前,人们常常疑惑JavaScript中的工厂模式和构造函数模式到底有什么区别。ES6中提出了class
关键字,许多人认为这解决了构造函数模式中的诸多问题。事实上,问题并没有得到解决。下面就让我们来看看工厂模式、构造函数模式和class
的一些重要区别。
首先,让我们看看这三种方式的例子:
// 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());
这些方式都将方法存储于共享的原型中,然后通过构造函数的闭包有选择的支持私有数据。换句话说,他们几乎拥有相同的特性,所以通常来说也能交替使用。
> 在JavaScript中,任何函数都能返回一个新的对象。当这个函数不是构造函数或class
时,它就叫做工厂函数。
ES6中的class
其实是构造函数的语法糖,所以它具有构造函数的一切优点和缺点:
class Foo {}
console.log(typeof Foo); // function
构造函数和class
的优点
- 大多数书本都教了
class
或者构造函数。 -
this
指向新的对象。 - 一些人喜欢
myFoo = new Foo()
这种写法。 - 构造函数和
class
存在许多微小的优化,不过除非你已经定量的分析了代码,并且这些优化确实对程序的性能很重要,否则这不应该被列入考虑范围。
构造函数和class
的缺点
1.需要new
在ES6之前,漏写new
是一个普遍的bug。为了对付它,许多人使用了以下方式:
function Foo() {
if (!(this instanceof Foo)) { return new Foo(); }
}
在ES6(ES2015)中,如果你试图不使用new
来调用class构造函数,将抛出错误。只有用工厂模式包裹class,才有可能避免必须使用new
。对于未来版本的JavaScript,有人提出建议,希望能够定制忽略new
时class构造函数的行为。不过这种方式依然增加了使用class时的开销(这也意味着会有更少的人使用它)。
2. 调用API时,实例化的细节被泄露(从new
的角度展开)
构造函数的调用方法与构造函数的实现方式紧密耦合。当你需要其具有工厂方法的灵活性时,重构起来将会是巨大的变化。将class放入工厂方法进行重构是非常常见的,它甚至被写入了Martin Fowler, Kent Beck, John Brant, William Opdyke, 和 Don Roberts的 “Refactoring: Improving the Design of Existing Code” 。
3. 构造函数模式违背了开/闭原则
由于对new
的要求,构造函数违背了开/闭原则:即一个API应该对扩展开放,对改装关闭。
我的意见是,既然从类到工厂方法的重构是非常常见的,那么应该将不应该造成任何破坏作为所有构造函数进行扩展时的标准。
如果你开放了一个构造函数或者类,而用户使用了这个构造函数,在这之后,如果需要增加这个方法的灵活性,(例如,换成使用对象池的方式进行实现,或者跨执行上下文的实例化,或者使用替代原型来拥有更多的继承灵活性),都需要用户同时进行重构。
不幸的是,在JavaScript中,从构造函数或者类切换到工厂方法需要进行巨大的改变:
// 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
// 这样的话,就会抛出:
// TypeError: Cannot read property 'undefined' of
// undefined at new AutoMaker.Car
在上面的例子中,我们首先提供了一个类,但是接下来希望提供不同的汽车种类。于是,工厂方法为不同的汽车种类使用了不同的原型。我曾经使用这种技术来存储不同的播放器接口,然后通过要处理的文件格式选择合适的原型。
4. 使用构造函数会导致instanceof
具有欺骗性
与工厂方法相比,构造函数带来的巨大变化就是instanceof
的表现。人们有时使用instanceof
进行类型检查。这种方式其实是经常会出问题的,我建议你避免使用instanceof
。
> instanceof
会撒谎。
// instanceof is a prototype identity check.
// NOT a type check.
// instanceof是一个原型检查
// 而不是类型检查
// 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]]
对象和Constructor.prototype
属性进行一致性检查。
例如,当执行上下文发生变化时,instanceof
会发生错误。当Constructor.prototype
变化了之后,instanceof
一样不会正常工作。
当你从一个class或构造函数开始(这将会返回指向 Constructor.prototype
的this
),然后转而探索另外一个对象(没有指向 Constructor.prototype
),这同样会导致instanceof的失败。这种情况在将构造函数转换为工厂函数时会出现。
简而言之,instanceof
是另外一种将构造函数转换为工厂函数时会发生的巨大变化。
使用class的优点
- 方便的,自包含的语法。
- 是JavaScript中使用类的一种单一、规范的方式。在ES6之前,在一些流行的库中已经出现了其实现方式。
- 对于有基于类的语言的背景的人来说,会更加熟悉。
使用class的缺点
除了具有构造函数的缺点外,还有:
- 用户可能会尝试使用extends关键字来创建导致问题的多层级的类。
多层级的类将会导致许多在面向对象程序设计中广为人知的问题,包括脆弱的基类问题,香蕉猴子雨林问题,必要性重复问题等等。不幸的是,class可以用来extends就像球可以用来扔,椅子可以用来坐一样自然。想了解更多内容,请阅读“The Two Pillars of JavaScript: Prototypal OO” and “Inside the Dev Team Death Spiral”.
值得指出的是,构造函数和工厂函数都有可能导致有问题的层次继承,但通过extends
关键字,class提供了一个让你犯错的功能可见性。换句话说,它鼓励你思考不灵活的的而且通常是错误的is-a关系,而不是更加灵活的has-a 或者 can-do组件化关系。
> 功能可见性是让你能够执行一定动作的机会。例如,旋钮可以用来旋转,杠杆可以用来拉,按钮可以用来按,等等。
使用工厂方法的优点
工厂方法比构造函数或类都要灵活,并且它不会诱惑人们使用extends
来构造太深的继承层级。你可以使用多种方法来继承工厂函数。特别的,如果想了解组合式工厂方法,请查看Stamp Specification。
1. 返回任意对象与使用任意原型
例如,你可以通过同一API轻松的创建多种类型的对象,例如,一个能够针对不同类型视频实例化播放器的媒体播放器,活着能够出发DOM事件或web socket事件的事件库。
工厂函数还能跨越之行上下文来实例化对象,充分利用对象池,并且允许更灵活的原型模型继承。
2. 没有重构的忧虑
你永远不需要从一个工厂转换到一个构造函数,所以重构将永远不会是一个问题。
3. 没有new
关于要不要使用new
只有一个选择,那就是不要用。(这将会使this
表现不好,原因见下一点)。
4. 标准的this
行为
this
和它通常的表现一样,所以你可以用它来获取其父级对象。例如,在player.create()
中,this
指向player,正如其他方法调用那样。call()
和apply()
也会同样的指向this
。
5. 不会有欺骗性的instanceof
问题
6. 有些人喜欢myFoo = createFoo()
这种写法
工厂方法的缺点
- 不会创建一个指向
Factory.prototype
的链接——但这其实是件好事,因为这样你就不会得到一个具有欺骗性的instanceof
。相反,instanceof
会一直失败。详情见工厂方法的优点。 -
this
不会指向工厂方法中的新对象。详情见工厂方法的优点。 - 在经过微优化的基准中,工厂方法可能会稍微比构造函数模式慢一些。如果这对你有影响,请务必在你程序的上下文中进行测试。
结论
在我看来,class
或许有简单的语法形式,但这不能弥补它引诱粗心的用户在类继承中犯错的事实。对于未来它也是有风险的,因为你有可能会想将其升级成为一个工厂函数,而由于new
关键字,你的所有调用都将和构造函数紧密耦合,于是从class向工厂方法迁移将会是一个巨大的改变。
你也许会想可以只重构调用部分,不过在大的团队中,或者你使用的class是公共API的一部分,你就有可能要破坏不在你掌控中的代码。换句话说,不能假设只重构调用部分永远是一个可选项。
关于工厂方法,有趣的事情在于它们不仅更加强大和灵活,而且是鼓励整个团队,以及所有API用户使用简单、灵活和安全的模式的最简单的方法。
关于工厂函数的好处,特别是关于对象组合的能力,还有许多内容可以详述。想要了解更多内容,以及这种方式与类继承的区别,请阅读“3 Different Kinds of Prototypal Inheritance”.
For more training in in prototypal inheritance techniques, factory functions, and object composition, be sure to check out “The Two Pillars of JS: Composition with Prototypes” — free for members. Not a member yet?
Learn JavaScript with Eric Elliott
_Eric Elliott_**_ is the author of “Programming JavaScript Applications” (O’Reilly), and “Learn JavaScript with Eric Elliott”. He has contributed to software experiences for **_Adobe Systems_, _Zumba Fitness_, _The Wall Street Journal, ESPN_, _BBC_, and top recording artists including _Usher_, _Frank Ocean_,_Metallica_, and many more._
He spends most of his time in the San Francisco Bay Area with the most beautiful woman in the world.