JavaScript中的工厂函数vs构造函数vs class
栏目: JavaScript · 发布时间: 7年前
内容简介:JavaScript中的工厂函数vs构造函数vs class
原文链接: JavaScript Factory Functions vs Constructor Functions vs Classes
作者:Eric Elliott
译者:sunny
转载需提前联系译者,未经允许不得转载。
本文首发于 前端指南
在ES6之前,JavaScript中的工厂函数和构造函数之间的差异令许多人困惑。由于ES6有了“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());
每种方式都用到了原型,并且有选择地使用构造函数来创建私有变量。换句话说,它们有很多相同的特性,大多数情况下都可以互换使用。
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的class是构造函数的语法糖,所以适用于构造函数的内容也适用于ES6的class:
class Foo {} console.log(typeof Foo); // function
构造函数和class的优点
-
大部分书都会教你使用class或者是构造函数
-
'this'指向创建的新对象
-
有些人喜欢myFoo = new Foo()这样的写法
-
可能会有一些性能上的微弱优势,但是基本不需要担心,除非你对代码进行了分析并且证明这些差距对你而言非常重要。
构造函数和class的缺点
-
需要new
在ES6之前,忘记new是一种常见的bug。很多人都会用样板来解决这个问题:
function Foo() { if (!(this instanceof Foo)) { return new Foo(); } }
在ES6(ES2015)中,如果你调用构造函数和class的时候忘记了new,会抛出错误。如果不将class包装在工厂函数中,那么难以避免强迫调用者使用new。也有人建议在未来的JavaScript版本中可以允许调用者自定义调用行为时可以省略new,但这也意味着会给每个使用它的class增加额外开销(也意味着很少使用它)。
-
实例化的细节被泄漏到了调用它的API(通过new)
所有的调用者都和构造函数的实现紧密耦合。如果你需要工厂的额外的灵活性,重构是一种突破性的变化。class到工厂的重构是常见的,他们出现在了Martin Fowler,Kent Beck,John Brant,William Opdyke和Don Roberts的创新重构的书: 《Refactoring: Improving the Design of Existing Code》 中。
-
构造函数违背了开闭原则
由于使用了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。
-
使用构造函数会导致“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关键字和层次继承引导人们走向错误的方向。你可以使用不同的方法从工厂方法继承。特别是,用组合工工厂函数检查 邮票规格 。
-
返回任意对象,使用任意原型
例如:你可以很容易创建实现了同样接口的不同的对象。一种媒体播放器,它可以实例化多种视频的播放,这些视频在引擎下使用不同的API,它们的事件库使用了不同的Dom事件或web socket事件。
工厂函数也会根据执行上下文实例化对象,利用对象池的优势,这允许更灵活的基于原型的继承。
-
不用担心重构
你从来都不需要把工厂转换为构造函数,所以重构不会是问题。
-
不需要new
new没有歧义,不要使用(这会造成this指向不明确,请看下一点)。
-
标准的this
this指向出了它该指向的,所以你可以通过它来获取父对象。例如:player.create(),this指向了player,就像call和apply方法确定了this的指向。
-
不会有欺骗instanceof
-
有些人喜欢“myFoo=createFoo()”这种方式
工厂方法的缺点
-
不会创建对象与工厂的prototype之间的联系,但这实际上是一个好事情,因为你不会被instanceof欺骗。相反,isntanceof会失败。请看优点。
-
this没有指向工厂中的新对象。请看优点。
-
在微优化的基准测试中,它可能比构造函数的执行速度慢。如果非要测试的话,请确保你需要关注这个问题。
总结
在我看来,class确实有简便的语法,但是实际上它可能会诱导用户创造继承的class。这也是有风险的,在未来,你可能会升级为工厂,但是由于new关键字的使用,所有的调用者都与你的构造函数紧密耦合,从类转换为工厂,会导致突破性的变化。
你可能考虑你只需要重构调用部分,但是在大型团队中,或者你的class是公共API的一部分,你不可能去修改不在你控制下的代码。换句话说,你不能总是假设重构调用者是可能的选项。
工厂模式不仅仅更加强大灵活,同时也会鼓励整个团队、所有的API用户使用一种简单、灵活安全的模式。
关于工厂模式其实还有很多内容,尤其是关于使用邮票规格进行对象组合的效用。更多关于这个话题,以及与class的不同,请阅读 “3 Different Kinds of Prototypal Inheritance” 。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Java构造函数与普通函数用法详解
- 构造函数、原型、原型链、继承
- Vue源码: 构造函数入口
- Hashmap源码解析-构造函数
- JavaScript 构造函数的继承
- Swift学习之构造函数与析构函数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
腾讯网UED体验设计之旅
任婕 等 / 电子工业出版社 / 2015-4 / 99.00元
《腾讯网UED体验设计之旅》是腾讯网UED的十年精华输出,涵盖了丰富的案例、极富冲击力的图片,以及来自腾讯网的一手经验,通过还原一系列真实案例的幕后设计故事,从用户研究、创意剖析、绘制方法、项目管理等实体案例出发,带领读者经历一场体验设计之旅。、 全书核心内容涉及网媒用户分析与研究方法、门户网站未来体验设计、H5技术在移动端打开的触控世界、手绘原创设计、改版迭代方法、文字及信息图形化设计、媒......一起来看看 《腾讯网UED体验设计之旅》 这本书的介绍吧!