1. 程式人生 > >JavaScript中的工廠方法、構造函數與class

JavaScript中的工廠方法、構造函數與class

格式 ret 存在 mon pillar bug 多種方法 希望 journal

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.prototypethis),然後轉而探索另外一個對象(沒有指向 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.

JavaScript中的工廠方法、構造函數與class