「類」設計模式和「原型」設計模式——「複製」和「委託」的差別

小引

JavaScript 技能持有者必定有問過這個問題:css

JavaScript 是面嚮對象語言嗎?html

你指望獲得的答案應該爲:「是」 或 「不是」。vue

可是惋惜,你得不到這樣簡單的答案!java

你大概瞭解一通以後,你會被告知:git

JavaScript 不是純粹的面嚮對象語言!express

wtf!爲何是不純粹?能不能純粹一點?!咱們喜歡純粹,不喜歡混沌!編程

......設計模式

實際上,死扣定義真的沒太必要。定義背後的故事纔是最重要的!數組

看完本篇,你就會明白這種「混沌」是什麼、來自何處,以及去往何方!!markdown

撰文不易,多多鼓勵。點贊再看,養成習慣。👍👍👍

「類」設計模式

婦孺皆知,面向對象三大特性:【封裝】、【繼承】、【多態】。

  1. 所謂封裝,即把客觀事物封裝成抽象的類。

  2. 所謂繼承,即子類繼承父類的能力。

  3. 所謂多態,即子類能夠用更特殊的行爲重寫所繼承父類的通用行爲。

其中,「類」的概念最最關鍵!【類】描述了一種代碼的組織結構形式,它是軟件中對真實世界中問題領域的建模方法。

舉個例子:

就比如咱們現實中修房子,你要修一個「寫字樓」、或者一個「居民樓」、或者一個「商場」,你就得分別找到修「寫字樓」、「居民樓」、「商場」的【設計藍圖】。

可是設計藍圖只是一個建築計劃,並非真正的建築。要想在真實世界實現這個建築,就得由建築工人將設計藍圖的各種特性(好比長寬高、功能)【複製】到現實世界來。

這裏的【設計藍圖】就是【類】,【複製】的過程就是【實例化】,【實例】就是【對象】

類的內部一般有一個同名的構造方法,咱們設想下,它的僞代碼就多是這樣的:

class Mall { // 「商場」類

    Mall( num ){ // 同名構造方法
        garage = num // 地下車庫數量
    }

    shop( goods ) { // 買東西
       output( "We can buy: ", goods )
    }

}

// 構造函數大多須要用 new 來調,這樣語言引擎才知道你想要構造一個新的類實例。
vanke = new Mall(1) // vanke 有 1 個地下車庫

vanke.shop("KFC") // "We can buy: KFC"

複製代碼

java 是典型的面嚮對象語言。基於「類」,咱們再經過如下一段 java 代碼來看看對繼承多態的理解。

public abstract class Animal{ // 抽象類
     abstract void sound();
}
public class Chicken extends Animal{ // 繼承
    public void sound(){
      sound("咯咯咯");
    }
}
public class Duck extends Animal{
    public void sound(){
      sound("嘎嘎嘎");
    }
}

public static void main(String args[]){
  Aninal chicken = new Chicken();
  Animal duck = new Duck();
  chicken.sound(); //咯咯咯
  duck.sound();  //嘎嘎嘎
}
複製代碼

雞和鴨都屬於動物分類,均可以發出叫聲(繼承),可是它們卻能夠發出不一樣的叫聲(多態),很容易理解。

繼承可使子類得到父類的所有功能; 多態可使程序有良好的擴展;

回想下:在 JS 中,咱們可能會怎樣寫:

var Duck = function () {};
var Chicken = function () {};
var makeSound = function ( animal ) {
    if( animal instanceof Duck){
        console.log("嘎嘎嘎");    
    }else if( animal instanceof Chicken){
        console.log("咯咯咯");    
    }
};
makeSound(new Duck());
makeSound(new Chicken());
複製代碼

這裏既沒用到繼承,也沒用到多態。這樣【寫判斷】是代碼「不清爽」的罪魁禍首!

  • 此處留一個疑問,若是不用判斷,還能夠怎麼寫?

在 vue2 中,咱們可能會這麼寫:

export default {
  data() {
      return {

      },
      mounted(){
          this.Chicken()
          this.Duck()
      },
      methods:{
          funtion AnimalSound(sound){
              console.log("叫聲:" + sound)
          },
          funtion Chicken(){
              this.AnimalSound("咯咯咯")
          },
          funtion Duck(){
              this.AnimalSound("嘎嘎嘎")
          }
      }
  }
複製代碼

像這種函數嵌套調用是很常見的。沒有看到繼承,也沒有看到多態,甚至都沒有看到最根本的「類」?!

(實際上,每一個函數都是一個 Function 對象。按照最開始定義所述,對象是類的實例,因此也是能在函數中看到「類」的!)

在 JavaScript 中,函數成了第一等公民! 函數彷佛什麼都能作!它能夠返回一個對象,能夠賦值給一個變量,能夠做爲數組項,能夠做爲對象的一個屬性......

但這明顯不是「類的設計模式」吧!

image.png

「類的設計模式」 意味着對【設計藍圖】的【複製】,在 JS 各類函數調用的場景下基本看不到它的痕跡。

「原型」設計模式

其實,衆所周知,JS 也是能作到【繼承】和【多態】的!只不過它不是經過類複製的方式,而是經過原型鏈委託的方式!

一圖看懂原型鏈?

image.png

看不懂?不要緊,記住這兩句話再來看:

  1. 一個對象的顯示原型的構造函數指向對象自己(很熟悉有沒有?在本文哪裏見過?)
  2. 一個對象的隱式原型指向構造這個對象的函數的顯示原型。

原來,JS 不是經過在類裏面寫同名構造函數的方式來進一步實現的實例化,它的構造函數在原型上!這種更加奇特的代碼服用機制有異於經典類的代碼複用體系。

這裏再附一個經典問題?JS new 操做會發生什麼?

會是像類那樣進行復制嗎?

答案是否認的!

JS 訪問一個對象的屬性或方法的時候,先在對象自己中查找,若是找不到,則到原型中查找,若是仍是找不到,則進一步在原型的原型中查找,一直到原型鏈的最末端。複製不是它所作的,這種查找的方式纔是!對象之間的關係更像是一種委託關係,就像找東西,你在我這找不到?就到有委託關係的其它人那裏找找看,再找不到,就到委託委託關係的人那裏找......直至盡頭,最後還找不到,指向 null。

因此:JavaScript 和麪向對象的語言不一樣,它並無類來做爲對象的抽象模式或者設計藍圖。JavaScript 中只有對象,對象直接定義本身的行爲。對象之間的關係是委託關係,這是一種極其強大的設計模式。在你的腦海中對象並非按照父類到子類的關係垂直組織的,而是經過任意方向的委託關聯並排組織的!

不過你也能夠經過這種委託的關係來模擬經典的面向對象體系:類、繼承、多態。但「類」設計模式只是一種可選的設計模式,你能夠模擬,也能夠不模擬!

現實是 ES6 class 給咱們模擬了:

class Widget { 
    constructor(width,height) { 
        this.width = width || 50; 
        this.height = height || 50; 
        this.$elem = null; 
    } 
    render($where){ 
        if (this.$elem) { 
            this.$elem.css( { 
                width: this.width + "px", 
                height: this.height + "px" 
            }).appendTo( $where ); 
        } 
    } 
} 
class Button extends Widget { 
    constructor(width,height,label) { 
        super( width, height ); 
        this.label = label || "Default"; 
        this.$elem = $( "<button>" ).text( this.label ); 
    } 
    render($where) { 
        super.render( $where ); 
        this.$elem.click( this.onClick.bind( this ) ); 
    } 
    onClick(evt) { 
        console.log( "Button '" + this.label + "' clicked!" ); 
    } 
}
複製代碼

看起來,很是不錯,很清晰!

沒有 .prototype 顯示原型複雜的寫法,也無需設置 .proto 隱式原型。還彷佛用 extends 、super 實現了繼承和多態。

然而,這只是語法糖的陷阱!JS 沒有類,沒有複製,它的機制是「委託」。

class 並不會像傳統面向類的語言同樣在申明時做靜態複製的行爲,若是你有意或者無心修改了父類,那子類也會收到影響。

舉例:

class C { 
 constructor() { 
    this.num = Math.random(); 
 } 
 rand() { 
    console.log( "Random: " + this.num ); 
 } 
} 
var c1 = new C(); 
c1.rand(); // "Random: 0.4324299..."
C.prototype.rand = function() { 
    console.log( "Random: " + Math.round( this.num * 1000 )); 
}; 
var c2 = new C(); 
c2.rand(); // "Random: 867"
c1.rand(); // "Random: 432" ——噢!
複製代碼

ES6 class 混淆了「類設計模式」和「原型設計模式」。它最大的問題在於,它的語 法有時會讓你認爲,定義了一個 class 後,它就變成了一個(將來會被實例化的)東西的 靜態定義。你會完全忽略 Class 是一個對象,是一個具體的能夠直接交互的東西。固然,它還有其它細節問題,好比屬性覆蓋方法、super 綁定的問題,有興趣自行了解。

總地來講,ES6 的 class 想假裝成一種很好的語法問題的解決方案,可是實際上卻讓問題更難解決並且讓 JavaScript 更加難以理解。 —— 《你不知道的 JavaScript》

小結

  • 「類設計模式」的構造函數掛在同名的類裏面,類的繼承意味着複製,多態意味着複製 + 自定義。

  • 「原型設計模式」的構造函數掛在原型上,原型的查找是一種自下而上的委託關係。

  • 「類設計模式」的類定義以後就不支持修改。

  • 「原型設計模式」講究的是一種動態性,任何對象的定義均可以修改,這和 JavaScript 做爲腳本語言所需的動態十分契合!

你能夠用「原型設計模式」來模擬「類設計模式」,可是這大機率是得不償失的。

最後,若是再被問道:JavaScript 是面嚮對象語言嗎?

若是這篇文章看懂了,就能夠圍繞:「類設計模式」和「原型設計模式」來吹了。

若是本文沒有看懂,就把下面的標答背下來吧......

image.png

關注公衆號《掘金安東尼》,持續輸出ing!!!

最近有點疲於寫業務代碼😭......誰能支支招?

參考文獻

相關文章
相關標籤/搜索