javascript 設計模式(修言小冊乾貨,長文建議收藏)

前言

最近拜讀了一下修言大神的JavaScript 設計模式核⼼原理與應⽤實踐, 對於現階段的我,能夠說受益不淺,本身也學着總結下,分享下乾貨,力求共同進步!javascript

在軟件工程中,設計模式(design pattern)是對軟件設計中廣泛存在(反覆出現)的各類問題,所提出的解決方案。 ——維基百科html

先提煉下,文章缺乏小冊前兩章,歸納來講:前端

  • 技術寒冬,前端真的很缺人,經得起推敲的人太少;
  • 前端技術面很大,想把每一面都涵蓋到很難,不建議你們死磕
  • 追求極致設計,講究道術結合;
  • 掌握核心科技,以‘不變’應萬變;
  • 前端工程師,首先是軟件工程師;

這裏強調一下以不變應萬變的中不變的是什麼,由於這關係到你的核心競爭力是什麼在哪裏。所謂‘不變的東西’說的駕馭技術的能力,具體來講分如下三個層次:vue

  • 能用健壯的代碼去解決具體問題;
  • 能用抽象的思惟去應對複雜的系統;
  • 能用工程化的思想去規劃更大規模的業務;

這三種能力在你的成長過程當中是層層遞進的關係,然後兩種能力能夠說是對架構師的要求。能作到第一點,而且把它作到紮實、作到嫺熟的人,已經堪稱同輩楷模java

不少人缺少的並非這種高瞻遠矚的激情,而是咱們前面提到的「不變能力」中最基本的那一點——用健壯的代碼去解決具體的問題的能力。這個能力在軟件工程領域所對標的經典知識體系,偏偏就是設計模式。因此說,想作靠譜開發,先掌握設計模式react

小冊的知識體系與格局,用思惟導圖展現以下: jquery

下面涉及到的是小冊中細講的設計模式;git

目錄:

  • 工廠模式
  • 單例模式
  • 原型模式
  • 修飾器模式
  • 適配器模式
  • 代理模式
  • 策略模式
  • 狀態模式
  • 觀察者模式
  • 迭代器模式

工廠模式

定義: 工廠模式其實就是將建立的對象的過程單獨封裝github

簡單工廠模式

結合定義咱們來看一段需求,公司須要編寫一個員工信息錄入系統,當系統裏面只建立本身的時候咱們能夠:面試

const lilei = {
    name = 'lilei',
    age: 18,
    career: 'coder'
}
複製代碼

固然員工確定不會是一個,而且會不斷加入,因此使用構造函數寫成:

function User(name, age, career) {
    this.name = name;
    this.age = age;
    this.career = career;
}
const lilei = new User('lilei', 18, 'coder')
const lilei = new User('hanmeimei', 20, 'product manager')
// ...

複製代碼

上面的代碼其實就是構造器,關於構造器模式後面會有具體介紹,咱們採用ES5的構造函數來實現,ES6的class其本質仍是函數,class只不過是語法糖,構造函數,纔是它的這面目。

需求繼續增長,career字段能攜帶的信息有限,沒法完整詮釋人員職能,要給每一個工種的用戶添加上一個個性字段,來描述相應的職能。

function Coder(name, age){
    this.name = name;
    this.age = age;
    this.career = 'coder';
    this.work = ['敲代碼', '摸魚', '寫bug'];
}
function ProductManager(name, age) {
    this.name = name;
    this.age = age;
    this.career = 'product manager';
    this.work = ['訂會議室', '寫PRD', '催更']
}

function Factory(name, age, career) {
    switch(career) {
        case 'coder':
            return new Coder(name, age);
            break;
        case 'product manager':
            return new ProductManager(name, age);
            break;
        ...
    }
}
複製代碼

如今看至少咱們不用操心構造函數的分配問題了,那麼問題來了,你們都看到了省略號了吧,這就意味着每多一個工種就要手動添加一個類上去,假若有幾十個工種,那麼就會有幾十個類?相對來講,咱們仍是須要不停的聲明新的構造函數。

so:

function User(name, age, career, work) {
    this.name = name;
    this.age = age;
    this.career = career;
    this.work = work;
}

function Factory(name, age, career) {
    let work;
    switch() {
        case'coder':
            work =  ['寫代碼','摸魚', '寫bug'];
            break;
         case 'product manager':
            work = ['訂會議室', '寫PRD', '催更']
            break
        case 'boss':
            work = ['喝茶', '看報', '見客戶']
        case 'xxx':
            // 其它工種的職責分配
            ...
    }
    return new User(name, age, career)
}
複製代碼

這樣一來咱們須要作事情就簡單多了,只須要無腦傳參就能夠了,不須要手寫無數個構造函數,剩下的Factory都幫咱們處理了。

工廠模式的目的就是爲了實現無腦傳參,就是爲了爽。 -修言

乍一看沒什麼問題,可是經不起推敲呀。首先映入眼簾的 Bug,是咱們把 Boss 這個角色和普通員工塞進了一個工廠。職能和權限會有很大區別,所以咱們須要對這個羣體的對象進行單獨的邏輯處理。

怎麼辦?去修改 Factory的函數體、增長管理層相關的判斷和處理邏輯嗎?單從功能上來說是可行的,可是這樣操做到後期會致使Factory異常龐大,稍有不慎就有可能摧毀整個系統,這一切悲劇的根源只有一個——沒有遵照開放封閉原則

開放封閉原則對拓展開放,對修改封閉。說得更準確點,軟件實體(類、模塊、函數)能夠擴展,可是不可修改

由此咱們引出抽象工廠模式

抽象工廠模式

抽象工廠這塊知識,對入行以來一直寫純 JavaScript 的同窗可能不太友好——由於抽象工廠在很長一段時間裏,都被認爲是 Java/C++ 這類語言的專利。

定義:抽象工廠模式是指當有多個抽象角色時,使用的一種工廠模式。抽象工廠模式能夠向客戶端提供一個接口,使客戶端在沒必要指定產品的具體的狀況下,建立多個產品族中的產品對象。

說白了抽象工廠模式,我認爲就是工廠模式的擴充版,簡單工廠生產實例,抽象工廠生產的是工廠,實際上是實現子類繼承父類的方法。

這裏比較繞,因此我可恥的把原文的例子搬過來了括弧笑,讓咱們來看一下:

假如要作一個山寨手機,基本組成是操做系統(Operating System,咱們下面縮寫做 OS)和硬件(HardWare)組成,咱們須要開一個手機工廠才能量產,可是咱們又不知道具體生產的是什麼手機,只知道有這兩部分組成,因此我先來一個抽象類來約定住這臺手機的基本組成

class MobilePhoneFactory {
    // 提供操做系統的接口
    createOS (){
        throw new Error('抽象工廠方法不容許直接調用,你須要將我重寫!');
    }
    // 提供硬件的接口
    createHardWare(){
        throw new Error('抽象工廠方法不容許直接調用,你須要將我重寫!');
    }
}
複製代碼

樓上這個類除了約定手機流水線的通用能力以外,啥也不幹,若是你嘗試new一個MobilePhoneFactory實力並調用裏面的方法,它都會給你報錯。在抽象工廠模式裏,樓上這個類就是咱們食物鏈頂端最大的Boss——AbstractFactory(抽象工廠);

抽象工廠不幹活,具體工廠(ConcreteFactory)幹活!當咱們明確了生產方案之後就能夠化抽象爲具體,好比如今須要生產Android系統 + 高通硬件手機的生產線,咱們給手機型號起名叫FakeStar,那我就能夠定製一個具體工廠

//具體工廠繼承自抽象工廠
class FakeStarFactory entends MobilePhptoFactory {
    cresteOS() {
        // 提供安卓系統視力
        return new AndroidOS();
    }
    createHardWare() {
        // 提供高通硬件實例
        return new QualcommHardeWare()
    }
}
複製代碼

這裏咱們在提供按安卓系統的時候,調用了兩個構造函數:AndroidOS和QualcommHardWare,它們分別用於生成具體的操做系統和硬件實例。像這種被咱們拿來用於 new 出具體對象的類,叫作具體產品類(ConcreteProduct)。具體產品類每每不會孤立存在,不一樣的具體產品類每每有着共同的功能,好比安卓系統類和蘋果系統類,它們都是操做系統,都有着能夠操控手機硬件系統這樣一個最基本的功能。所以咱們能夠用一個抽象產品(AbstractProduct)類來聲明這一類產品應該具備的基本功能。

// 定義操做系統這類產品的抽象產品類
class OS {
    controlHardWare() {
        throw new Error('抽象產品方法不容許直接調用,你須要將我重寫!');
    }
}

// 定義具體操做系統的具體產品類
class AndroidOS extends OS {
    controlHardWare() {
        console.log('我會用安卓的方式去操做硬件')
    }
}

class AppleOS extends OS {
    controlHardWare() {
        console.log('我會用🍎的方式去操做硬件')
    }
}
...
複製代碼

硬件產品同理這裏就不重複了。如此一來,當咱們須要生產一臺FakeStar手機時,咱們只須要:

// 這是個人手機
const myPhone = new FakeStarFactory()
// 讓它擁有操做系統
const myOS = myPhone.createOS()
// 讓它擁有硬件
const myHardWare = myPhone.createHardWare()
// 啓動操做系統(輸出‘我會用安卓的方式去操做硬件’)
myOS.controlHardWare()
// 喚醒硬件(輸出‘我會用高通的方式去運轉’)
myHardWare.operateByOrder()
複製代碼

當有一天須要產出一款新機投入市場的時候,咱們是否是不須要對抽象工廠MobilePhoneFactory作任何修改,只須要拓展它的種類:

class newStarFactory extends MobilePhoneFactory {
    createOS() {
        // 操做系統實現代碼
    }
    createHardWare() {
        // 硬件實現代碼
    }
}
複製代碼

這麼個操做,對原有的系統不會形成任何潛在影響所謂的「對拓展開放,對修改封閉」就這麼圓滿實現了。

總結

抽象工廠模式的四個角色:

  • 抽象工廠(不能用於生成具體實例):用於成名最終目標產品的共性。
  • 具體工廠:繼承抽象工廠、實現抽象工廠裏聲明的方法,用於建立具體的產品類。
  • 抽象產品(不能用於生成具體實例):用於具體產品中共性的抽離。
  • 具體產品:好比上面咱們提到的具體的硬件等;

單例模式

定義: 保證一個類只有一個實例,並提供一個訪問他的全局訪問點。

通常狀況下咱們建立一個類(本質是構造函數)後,能夠經過new關鍵字調用構造函數進而生成任意多的實例對象:

class SingleDog {
    show() {
        console.log('我是一隻單身狗');
    }
}

const s1 = new SingleDog();
const s2 = new SingleDog();

// false 
s1 === s2 
複製代碼

很明顯s1與s2沒有任何瓜葛,由於每次new出來的實例都會給咱們開闢一塊新的內存空間。那麼咱們怎麼才能讓對此new出來都是那惟一的一個實例呢?那就須要咱們的構造函數具有判斷本身是否被建立過一個實例的能力。

核心代碼:

// 定義Storage
class SingleDog {
    show() {
        console.log('我是一隻單身狗');
    }
    getInstace() {
        // 判斷是否已經new過一個實例
        if(!SingleDog.instance){
            // 若這個惟一實例不存在,則建立它
            SingleDog.instance = new SingleDog();
        }
        // 若是有則直接返回
        return SingleDog.instance;
    }
}
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()

// true
s1 === s2
複製代碼

生產實踐:redux、vuex中的Store,或者咱們常用的Storage都是單例模式。

咱們來實現一下Storage:

class Storage{
    static getInstance() {
        if(!Storage.instance) {
            Storage.instance = new Storage();
        }
        return  Storage.instance;
    }
    getItem(key) {
        return localStorage.getItem(key);
    }
    setItem(key, value){
        return localStorage.setItem(key, value);
    }
}
const storage1 = Storage.getInstance()
const storage2 = Storage.getInstance()

storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')

// 返回true
storage1 === storage2
複製代碼

思考一下如何實現一個全局惟一的模態框呢?

原型模式

原型模式不只是一種設計模式,它仍是一種編程範式(programming paradigm),是 JavaScript 面向對象系統實現的根基。

原型模式這一章節小冊並無講述什麼稀奇的知識點主要是關於Prototype相關的須要強調的是javascript是以原型爲中心的語言,ES6中的類實際上是原型繼承的語法糖。

ECMAScript 2015 中引入的JavaScript類實質上是JavaScript現有的基於原型的繼承的語法糖。類語法不會爲 JavaScript 引入新的面向對象的繼承模型。 ——MDN

在原型模式下當咱們想要建立一個對象時會先找到一個對象做爲原型,而後在經過克隆原型的方式來建立出一個與原型同樣(共享一套數據/方法)的對象。

其實談原型模式就是在談原型範式,原型編程範式的核心思想就是利用實例來描述對象,用實例做爲定義對象和繼承的基礎。在JavaScript中,原型編程範式的體現就是基於原型鏈的繼承。這其中,對原型、原型鏈的理解是關鍵。

這裏應當注意,在一些面試中,面試官可能會能夠混淆javascript中的原型範式和強類型語言中的原型模式,當他們這麼作的時候頗有多是爲了考察你對對象深拷貝的理解。

在JavaScript中實現深拷貝,有一種取巧的方式——JSON.stringify:

注意這方法是本身的侷限性的,好比沒法處理function、沒法處理正則等等,咱們在面試中不該該侷限於這種方法,應該拓展出更多的可實施方案,好比遞歸等其餘方法,回答遞歸的時候應該注意遞歸函數中值的類型的判斷以及遞歸爆棧的問題。

深拷貝是沒有完美方案的,每一種方案都有他本身的case

關於深拷貝,有想深刻研究的,小冊做者在這裏推薦了個比較好的地址能夠關注下:

裝飾器模式

裝飾器模式(DecoratorPattern)容許向一個現有的對象添加新的功能,同時又不改變其結構。這種類型的設計模式屬於結構型模式,它是做爲現有的類的一個包裝。

優勢:裝飾類和被裝飾類能夠獨立發展,不會相互耦合,裝飾模式是繼承的一個替代模式,裝飾模式能夠動態擴展一個實現類的功能。

缺點:多層裝飾比較複雜。

類裝飾器的參數

當咱們給一個類添加裝飾器時:

function classDecorator(target) {
    target.hasDecorator = true
  	return target
}

// 將裝飾器「安裝」到Button類上
@classDecorator
class Button {
    // Button類的相關邏輯
}
複製代碼

此處的 target 就是被裝飾的類自己。看着眼熟不?react中的高級組件(HOC)就是使用這個實現的。

方法裝飾器的參數

而當咱們給一個方法添加裝飾器時:

function funcDecorator(target, name, descriptor) {
    let originalMethod = descriptor.value
    descriptor.value = function() {
        console.log('我是Func的裝飾器邏輯');
        ... // 你須要拓展的操做
        return originalMethod.apply(this, arguments);
    }
    return descriptor
}

class Button {
    @funcDecorator
    onClick () { 
        console.log('我是Func的原有邏輯')
    }
}   
複製代碼
  • 第一個參數target 變成了Button.prototype,即類的原型對象。這是由於 onClick 方法老是要依附其實例存在的,修飾onClik實際上是修飾它的實例。但咱們的裝飾器函數執行的時候,Button 實例還並不存在。爲了確保實例生成後能夠順利調用被裝飾好的方法,裝飾器只能去修飾 Button 類的原型對象

  • 第二個參 數name,是咱們修飾的目標屬性屬性名。

  • 第三個參數descriptor,它的真面目就是「屬性描述對象」(attributes object),它由各類各樣的屬性描述符組成,這些描述符又分爲數據描述符和存取描述符,很明顯,拿到了 descriptor,就至關於拿到了目標方法的控制權。:

這裏須要注意

  • 當咱們在react中給方法添加裝飾器的時候,方法樣使用上邊寫法,不能使用()=>{}箭頭函數的寫法,緣由是箭頭函數寫法若是class類沒有實例出來是獲取不到的

  • 接着上一條說,使用上述寫法的時候應該在組件的constructor中使用bind修改onClick方法的this指向。

高階組件(HOC)的應用

高階組件(HOC)的主要有兩個類型:

  • 屬性代理

新組件類繼承子React.component類,對傳入的組件進行一系列操做,從而產生一個新的組件,達到加強組件的做用。

一、 操做props

二、 訪問ref

三、 抽取state

四、 封裝組件

class WrappedComponent extends Component {
    render() {
        return <input name="name" {...this.props.name} />;
    }
}

const HOC = (WrappedComponent) =>
    class extends Component {
        constructor(props) {
            super(props);
            this.state = {
                name: '',
            };

            this.onNameChange = this.onNameChange.bind(this);
        }

        onNameChange(event) {
            this.setState({
                name: event.target.value,
            })
        }

        render() {
            const newProps = {
                name: {
                    value: this.state.name,
                    onChange: this.onNameChange,
                },
            }
            return <WrappedComponent {...this.props} {...newProps} />;
        }
    }
複製代碼
  • 反向繼承

新組件類繼承子原組件類,攔截生命週期、渲染劫持和控制state。

export default function ConsoleLog(WrappedComponent, params = []) {
  return class extends WrappedComponent {

    consoleLog() {
      if (params && params.length > 0) {
        params.forEach((info) => {
          console.log(`${info}==` + JSON.stringify(this.props[info]));
        })
      } else {
        console.log("this.props", JSON.stringify(this.props))
      }
    }

    render() {
      this.consoleLog()
      return super.render();
    }
  }
}
複製代碼

反向繼承不能保證完整的子組件樹被解析React Components, Elements, and Instances這篇文章主要明確了一下幾個點:

  • 元素(element)是一個是用DOM節點或者組件來描述屏幕顯示的純對象,元素能夠在屬性(props.children)中包含其餘的元素,一旦建立就不會改變。咱們經過JSX和React.createClass建立的都是元素。

  • 組件(component)能夠接受屬性(props)做爲輸入,而後返回一個元素樹(element tree)做爲輸出。有多種實現方式:Class或者函數(Function)。

因此, 反向繼承不能保證完整的子組件樹被解析的意思的解析的元素樹中包含了組件(函數類型或者Class類型),就不能再操做組件的子組件了,這就是所謂的不能徹底解析。

小結(未完待續)

關於react的高階組件事後我會在整理出一份詳細的博客,由於可操做性很強,一段兩段也說不清。

因爲後半部分做者還在更新中,因此沒有加進去,有興趣的能夠關注下,以後就能夠愉快的閱讀了。

關注我而後帶走它!

相關文章
相關標籤/搜索