原文連接: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或者是構造函數web
'this'指向建立的新對象安全
有些人喜歡myFoo = new Foo()這樣的寫法app
可能會有一些性能上的微弱優點,可是基本不須要擔憂,除非你對代碼進行了分析而且證實這些差距對你而言很是重要。socket
須要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」是從構造函數切換到工廠方法時的另外一種突破性的變化。
簡便、獨立的語法
單一的、規範的模仿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」。