JS 5種不同的方法實(shí)現(xiàn)裝飾者模式(譯)

為了自身樂趣和加強(qiáng)理解使用閉包意鲸、猴子補(bǔ)丁烦周、原型、代理和中間件5種不同方式在 javascript 中實(shí)現(xiàn)裝飾者模式怎顾。

嗯读慎?這都是怎么一回事哪?

最近我有機(jī)會(huì)研究使用不同的方法在JavaScript中實(shí)現(xiàn)裝飾者模式(又稱為包裝模式)槐雾。我覺得有必要分享我所學(xué)到的夭委,關(guān)于使用這些技術(shù)來實(shí)現(xiàn)裝飾者模式的利弊。

"當(dāng)然不是這種裝飾者..."

當(dāng)然不是這種裝飾者...

這5種不同的實(shí)現(xiàn)方式分別是:

  1. 閉包
  2. 猴子補(bǔ)丁
  3. 原型繼承
  4. 代理(ES6)
  5. 中間件

如果你想要知道本文 a) 為什么使用ES6語法募强,b) 為什么不使用class株灸, c) 源文件列表,為了不打亂閱讀順序擎值,我已經(jīng)把這些都記在了附錄中慌烧,你可以到這篇文章的最后查看。

首先鸠儿,需要被裝飾的組件

'use strict'

function myComponentFactory() {
    let suffix = ''

    return {
        setSuffix: suf => suffix = suf,
        printValue: value => console.log(`value is ${value + suffix}`)
    }
}

const component = myComponentFactory()
component.setSuffix('!')
component.printValue('My Value')

這是個(gè)簡單的組件屹蚊,含有一個(gè) printValue(val) 方法厕氨,用來在值的最后添加尾綴并在控制臺(tái)輸出,尾綴可以通過 setSuffix(val 方法設(shè)置汹粤。

我準(zhǔn)備用一個(gè)驗(yàn)證輸入的裝飾器命斧,以及一個(gè)將值轉(zhuǎn)換為小寫的驗(yàn)證器來展示裝飾鏈的情景。創(chuàng)建setSuffix(val) 方法是為了添加一些復(fù)雜性玄括,用來滿足組件擁有除裝飾方法以外還有其他成員冯丙。

值得注意的是,除了最后一個(gè)以外的所有例子都是使用獨(dú)立的函數(shù)對(duì)目標(biāo)對(duì)象進(jìn)行裝飾遭京,而不是添加對(duì)象的一個(gè)成員胃惜。

如何裝飾這個(gè)組件

下圖顯示我準(zhǔn)備如何裝飾這個(gè)組件,先將初始的 printValue(val) 方法先用 'lower case' 裝飾器包裝哪雕,然后再用 'validate' 裝飾器包裝船殉。當(dāng)一個(gè)被裝飾過的組件調(diào)用 printValue(val) 方法時(shí),首先它會(huì)驗(yàn)證它的值斯嚎,然后會(huì)將值轉(zhuǎn)為小寫利虫,最后打印它。

(注意:下圖表明堡僻,我們可以在原始調(diào)用之后返回過程時(shí)糠惫,給我們的裝飾器添加額外的行為,而本文并沒有涉及這些钉疫。)

組件裝飾設(shè)計(jì)圖

方法一: 閉包

我能想到最原生實(shí)現(xiàn)裝飾者模式的方法就是用一個(gè)對(duì)象來包裝需要被裝飾的對(duì)象硼讽,并返回一個(gè)新對(duì)象,在這個(gè)新對(duì)象中執(zhí)行一些處理后再調(diào)用原始的方法牲阁。

簡單

“簡單固阁!”

上代碼!

首先城菊,我會(huì)展示這些代碼作為一個(gè)整體备燃,然后我會(huì)帶你一步一步地分析它。

function myComponentFactory() {
    let suffix = ''

    return {
        setSuffix: suf => suffix = suf,
        printValue: value => console.log(`value is ${value + suffix}`)
    }
}

function toLowerDecorator(inner) {
    return {
        setSuffix: inner.setSuffix,
        printValue: value => inner.printValue(value.toLowerCase())
    }
}

function validatorDecorator(inner) {
    return {
        setSuffix: inner.setSuffix,
        printValue: value => {
            const isValid = ~value.indexOf('My')

            setTimeout(() => {
                if (isValid) inner.printValue(value)
                else console.log('not valid man...')
            }, 500)
        }
    }
}

const component = validatorDecorator(toLowerDecorator(myComponentFactory()))
component.setSuffix('!')
component.printValue('My Value')
component.printValue('Invalid Value')

這些都做了什么凌唬?

組件工廠還是和之前的一樣并齐。不過,我們通過用裝飾工廠包裹它的創(chuàng)建來裝飾它客税。

const component = validatorDecorator(toLowerDecorator(myComponentFactory()))

原始對(duì)象將作為參數(shù)傳入裝飾工廠中况褪,并返回一個(gè)經(jīng)過包裝的對(duì)象,它會(huì)將除了被裝飾的方法以外的調(diào)用直接傳遞給初始對(duì)象霎挟。

裝飾工廠接受原始對(duì)象作為參數(shù)窝剖,并返回一個(gè)經(jīng)過包裝后的對(duì)象麻掸,這個(gè)對(duì)象會(huì)將除了需要被裝飾的方法以外的調(diào)用直接傳遞給原始對(duì)象酥夭。

function toLowerDecorator(inner) {
    return {
        setSuffix: inner.setSuffix,
        printValue: value => inner.printValue(value.toLowerCase())
    }
}

裝飾器會(huì)將值轉(zhuǎn)換為小寫,并把這個(gè)“裝飾”(或“包裝”)后的值傳給了內(nèi)部函數(shù)。

然后熬北,我們可以繼續(xù)在對(duì)象的創(chuàng)建上添加裝飾工廠方法并等待調(diào)用疙描,這就像是在打開一個(gè)俄羅斯套娃。

俄羅斯套娃

在完成了對(duì)象的創(chuàng)建和裝飾之后讶隐,我們運(yùn)行我們的測(cè)試代碼:

component.setSuffix('!')
component.printValue('My Value')
component.printValue('Invalid Value')

結(jié)果是:

value is my value!
not valid man...

最外層的裝飾器將會(huì)第一個(gè)被執(zhí)行起胰。在這個(gè)例子中是驗(yàn)證方法。第一次調(diào)用是合法的巫延,所以結(jié)果會(huì)被傳遞給第二個(gè)方法效五,值將被轉(zhuǎn)換為小寫,然后再按順序調(diào)用原始方法給經(jīng)過小寫處理后的值添加尾綴炉峰,并在控制臺(tái)中輸出結(jié)果畏妖。

第二次調(diào)用沒有通過驗(yàn)證,所以值沒有被修改疼阔,它展示了如何停止裝飾鏈戒劫。

驗(yàn)證裝飾器為何要設(shè)置定時(shí)?

[(服務(wù)生比喻是解釋異步代碼最好的方法)](http://www.roidna.com/blog/what-is-node-js-benefits-overview/)

我在包裝方法中添加一些異步的代碼婆廊,因?yàn)槲覀冊(cè)贘avaScript的世界里:一個(gè)單線程迅细,無阻塞,異步為王的語言世界淘邻。如果你的代碼無法處理異步茵典,那么它就失去了大部分JavaScript語言設(shè)計(jì)的特點(diǎn)。

驗(yàn)證方法通過設(shè)置定時(shí)來模擬去數(shù)據(jù)庫驗(yàn)證值的合法性列荔,然后在回調(diào)函數(shù)中去調(diào)用內(nèi)部函數(shù)的方法敬尺。這樣我們能測(cè)試我們的實(shí)現(xiàn)方式在處理異步代碼時(shí)是否依舊能正常工作。

該如何使用閉包贴浙?
為了之后的調(diào)用砂吞,我們可以將內(nèi)部對(duì)象存儲(chǔ)到一個(gè)新對(duì)象上。但我們?yōu)槭裁床贿@樣做崎溃?因?yàn)檫@會(huì)使得它成為公共的蜻直,那時(shí),我該調(diào)用 instance.setSuffix() 袁串,還是 instance._original.setSuffix()概而?這會(huì)變得非常奇怪,會(huì)混淆對(duì)象的使用囱修,使對(duì)象成為一個(gè)私有成員這會(huì)好得多赎瑰。

“然而JavaScript并沒有私有成員,糟糕破镰!”

但我們可以使用閉包來達(dá)到這個(gè)效果餐曼。

官方:什么是閉包压储?

“即使函數(shù)在變量的作用域之外被調(diào)用,閉包允許函數(shù)訪問閉包引用的變量源譬〖铮”(我稍微重新措辭從維基百科的定義)

一個(gè)簡單的例子:

function wow() {
    const val = 5
    return () => console.log(val)
}

wow()()

這是一個(gè)我能想到用JavaScript實(shí)現(xiàn)最簡單的例子〔饶铮“wow”方法返回一個(gè)打印“val”的方法刮刑,然而,一旦“wow”返回养渴,“val”變量就不在作用域之中雷绢。

然而,它會(huì)正常顯示理卑,因?yàn)殚]包在方法返回時(shí)就被創(chuàng)建了习寸,它已經(jīng)記錄了作用域里的變量(在這個(gè)例子中是“val”),即使離開了當(dāng)前作用域傻工,閉包依舊可以訪問它內(nèi)部的變量霞溪,
回到我們的裝飾器
再來看看之前的裝飾器:

function toLowerDecorator(inner) {
    return {
        setSuffix: inner.setSuffix,
        printValue: value => inner.printValue(value.tolowercase())
    }
}

它返回一個(gè)包含以下方法的對(duì)象:

value => inner.printValue(value.tolowercase())

這個(gè)方法引用“inner”對(duì)象,當(dāng)裝飾方法被返回時(shí)中捆,“inner”對(duì)象就已經(jīng)在作用域之外了鸯匹。但因?yàn)椋诜椒▋?nèi)部是一個(gè)被使用的變量泄伪,所以內(nèi)部方法會(huì)記錄這個(gè)變量殴蓬,一旦這個(gè)方法被返回,那么閉包就形成了蟋滴。

這意味著為了嵌套的方法能在之后正常調(diào)用染厅,變量的生命周期被我們的內(nèi)部方法給延長了。

因?yàn)殚]包津函,我們的方法能使用“inner”對(duì)象肖粮,但它是私有變量,并不是公共的尔苦。

閉包是 JavaScript 最重要和實(shí)用的特性之一涩馆,所以確保你現(xiàn)在已經(jīng)領(lǐng)悟它了。

私有

優(yōu)缺點(diǎn)
在這介紹閉包允坚,雖然它和上面的包裝方法有一點(diǎn)關(guān)系魂那,但事實(shí)上,本文所展示的技術(shù)都使用了閉包來隱藏私有變量稠项。

除此之外涯雅,這是一個(gè)非常簡單的實(shí)現(xiàn),但有一個(gè)非常明顯的缺點(diǎn):我們必須包裝內(nèi)部對(duì)象的每個(gè)方法展运,而非裝飾那一個(gè)目標(biāo)方法活逆。就像這樣:

return {
    setSuffix: inner.setSuffix,
    ...

這既丑陋又痛苦轻腺。可不可以我們的裝飾器只定義裝飾行為而不去關(guān)心剩下的划乖?幸運(yùn)的是有不少技術(shù)能這樣做。讓我們看看猴子補(bǔ)丁是如何做的挤土。

方法二:猴子補(bǔ)丁

什么是猴子補(bǔ)肚兮帧?

“動(dòng)態(tài)修改一個(gè)類或模型仰美∶缘睿” -維基百科

簡單地在當(dāng)前情景下解釋一下:

“我將要采用JavaScript的動(dòng)態(tài)性并結(jié)合對(duì)象可變性的特點(diǎn)來用我的方法取代你的方法!” -(那時(shí)的我)

那該如何用猴子補(bǔ)丁來裝飾咖杂?

“我準(zhǔn)備用我的方法替換你的庆寺,然后我會(huì)從我的方法內(nèi)部包裝并調(diào)用你的方法∷咦郑” -(依舊是我)

該怎么做懦尝!
你問該怎么做?好壤圃,我會(huì)像你展示陵霉。首先,我會(huì)展示這些代碼作為一個(gè)整體伍绳,然后我會(huì)帶你一步一步地分析它:(譯者注:這里是原作者的一處幽默踊挠,看原文更能體會(huì)。)

function myComponentFactory() {
    let suffix = ''

    return {
        setSuffix: suf => suffix = suf,
        printValue: value => console.log(`value is ${value + suffix}`)
    }
}

function decorateWithToLower(inner) {
    const originalPrintValue = inner.printValue
    inner.printValue = value => originalPrintValue(value.toLowerCase())
}

function decorateWithValidator(inner) {
    const originalPrintValue = inner.printValue

    inner.printValue = value => {
        const isValid = ~value.indexOf('My')

        setTimeout(() => {
            if (isValid) originalPrintValue(value)
            else console.log('not valid man...')
        }, 500)
    }
}

const component = myComponentFactory()
decorateWithToLower(component)
decorateWithValidator(component)

component.setSuffix('!')
component.printValue('My Value')
component.printValue('Invalid Value')

這都做了些什么冲杀?

組件還是那個(gè)組件效床,裝飾器變了,并且調(diào)用方式也變了权谁。我們通過在現(xiàn)有對(duì)象上進(jìn)行處理的方式剩檀,來代替通過工廠方法傳遞對(duì)象的方式來實(shí)現(xiàn)我們的裝飾方法:

decorateWithToLower(component)

這個(gè)裝飾方法通過保存初始 “printValue” 方法到一個(gè)本地變量的辦法來實(shí)現(xiàn)猴子補(bǔ)丁:

const originalPrintValue = inner.printValue

然后用一個(gè)方法覆蓋原始方法旺芽,這個(gè)方法先將值轉(zhuǎn)換為小寫谨朝,再將值傳遞給之前儲(chǔ)存的原始方法的副本去調(diào)用:

inner.printValue = value => originalPrintValue(value.toLowerCase())

我們和之前一樣創(chuàng)建我們的裝飾器。我們先用一個(gè)轉(zhuǎn)換小寫裝飾器包裝 printValue()甥绿,再用一個(gè)驗(yàn)證裝飾器來包裝它:

const component = myComponentFactory()
decorateWithToLower(component)
decorateWithValidator(component)

注意這里依舊使用了閉包來用于內(nèi)部函數(shù)鏈的存儲(chǔ)字币。與例一真正的區(qū)別在于我們只替換了現(xiàn)有對(duì)象中的一個(gè)方法,而不是返回一個(gè)全新包裝后的對(duì)象共缕。

猴子補(bǔ)丁的優(yōu)缺點(diǎn)

人們討厭猴子補(bǔ)丁通常有著好的理由洗出。

猴子

額......

為什么所有人都討厭?因?yàn)楫?dāng)我調(diào)用一個(gè)庫函數(shù)時(shí)图谷,我不希望功能因?yàn)槲乙肓艘恍┢渌耆珶o關(guān)的“巨坑”庫而被修改了翩活。

不幸的是阱洪,如果那個(gè)愚蠢的人類決定去猴子補(bǔ)丁一些原生方法或一些共享的依賴,那對(duì)我來說就沒有驚喜菠镇,只剩驚嚇了冗荸。

如果猴子補(bǔ)丁只是現(xiàn)在用于我自己的代碼,它可能并不那么糟糕利耍,但它依舊有點(diǎn)古怪蚌本,一些人依舊會(huì)對(duì)它說“不”。

盡管如此隘梨,它比我們之前的方法還是有一個(gè)優(yōu)勢(shì)程癌。我們的裝飾方法只需處理我們想要裝飾的方法,組件其余的部分保持不變轴猎。這意味著我們的裝飾方法只有一個(gè)職責(zé):用新的行為包裝去方法嵌莉。

所以,如果你不介意猴子補(bǔ)丁捻脖,你的基礎(chǔ)對(duì)象又擁有需要被額外維護(hù)的公共方法锐峭,而且你希望保持代碼簡潔,那么這項(xiàng)技術(shù)可能適合你可婶。

好只祠,那有關(guān)原型繼承是怎樣的?

方法三:原型繼承

什么是原型繼承扰肌?

大多數(shù)開發(fā)者習(xí)慣于 Java 或 C# 這種一個(gè)類基于另一個(gè)類的經(jīng)典繼承方式抛寝。簡單來說,原型繼承就都是用對(duì)象代替類:“一個(gè)對(duì)象從其他對(duì)象繼承上屬性曙旭〉两ⅲ”

它的實(shí)現(xiàn)機(jī)制在 JavaScript 中也十分簡單。所有對(duì)象都有同一個(gè)原型桂躏。事實(shí)上钻趋,所有原型鏈的終點(diǎn)都指向 “Object”,它也是所有原型鏈的基礎(chǔ)剂习。

委托

委托

你可以通過設(shè)置對(duì)象的原型聲明一個(gè)對(duì)象基于另一個(gè)對(duì)象蛮位。這就意味著:如果需要訪問一個(gè)對(duì)象成員,對(duì)象首先會(huì)在自身之中查找鳞绕,但如果沒有找到失仁,它會(huì)去它的原型上繼續(xù)查找,并一直按照這個(gè)方式查找到原型鏈的終點(diǎn)们何。

我不喜歡使用 JavaScript 中的 new 關(guān)鍵字萄焦,我不在這深入說為什么,如果你感興趣,可以到文章的最后查看拂封。而在我看來茬射,實(shí)現(xiàn)原型繼承最好的方法是使用 Object.create(prototype)

const myBaseObject = { myProperty: 'oh hai' }

const myNewObject = Object.create(myBaseObject)
myNewObject.newMethod = () => { console.log(myBaseObject.myProperty) }

這里我們不僅設(shè)置 myBaseObject 作為 myNewObject 的原型來繼承,還展示了如何訪問基類的成員冒签。這里沒有受保護(hù)的或私有的作用域在抛,也沒有抽象成員需要我們考慮。如果你想只暴露新的對(duì)象而不顯示基類對(duì)象萧恕,只需通過一個(gè)函數(shù)包裹刚梭,然后返回所有你想要的。函數(shù)總是能處理你在 JavaScript 中遇到的任何問題廊鸥。

看代碼

function myComponentFactory() {
    let suffix = ''

    return {
        setSuffix: suf => suffix = suf,
        printValue: value => console.log(`value is ${value + suffix}`)
    }
}

function toLowerDecorator(inner) {
    const instance = Object.create(inner)
    instance.printValue = value => inner.printValue(value.toLowerCase())
    return instance
}

function validatorDecorator(inner) {
    const instance = Object.create(inner)
    instance.printValue = value => {
        const isValid = ~value.indexOf('My')

        setTimeout(() => {
            if (isValid) inner.printValue(value)
            else console.log('not valid man...')
        }, 500)
    }
    return instance
}

const component = validatorDecorator(toLowerDecorator(myComponentFactory()))
component.setSuffix('!')
component.printValue('My Value')
component.printValue('Invalid Value')

這個(gè)例子里我構(gòu)造了一個(gè)新的對(duì)象來訪問內(nèi)部的對(duì)象,這和第一個(gè)例子十分地相似辖所。然而惰说,第一個(gè)例子中有個(gè)缺陷就是為了確保初始對(duì)象的每個(gè)成員都可訪問,需要將每個(gè)成員變量復(fù)制到新的包裝對(duì)象上缘回。在這個(gè)例子中吆视,我們發(fā)揮對(duì)象繼承的優(yōu)勢(shì),新對(duì)象創(chuàng)建時(shí)使用初始對(duì)象作為它的原型酥宴,這樣我們就不必在新對(duì)象上定義 “setSuffix” 方法啦吧,當(dāng)這個(gè)方法被調(diào)用時(shí),原型鏈會(huì)檢查這個(gè)成員是否存在拙寡。

在 JavaScript 中使用繼承來實(shí)現(xiàn)一個(gè)裝飾器是一個(gè)顯而易見并高效的方式授滓。有趣的是,裝飾者模式的最初設(shè)計(jì)目的之一就是解決傳統(tǒng)繼承的一些局限性肆糕。也就是說般堆,采用傳統(tǒng)的繼承,無法將不同的行為聯(lián)系起來诚啃,必須事先定義類的繼承關(guān)系淮摔,這會(huì)導(dǎo)致它成為一個(gè)僵化的層次結(jié)構(gòu)(譯者并不贊同這個(gè)觀點(diǎn))。幸運(yùn)的是始赎,原型繼承沒有這個(gè)限制和橙,從上面的例子就可以看到,我可以選擇任何對(duì)象作為原型造垛。

這使得用原型繼承來實(shí)現(xiàn)裝飾器是一個(gè)極好的選擇魔招。

方法四:代理

ES6中增加了代理模塊,它看上去有希望去完成一些關(guān)于面向切片的編程技術(shù)五辽。讓我們來看看仆百,它能不能幫我們創(chuàng)建一個(gè)裝飾器。

什么是代理奔脐?

“代理對(duì)象通常用來為基本操作定義自定義行為(例如:屬性查找俄周,賦值吁讨,枚舉或函數(shù)調(diào)用等)。 -MDN

哇~我們可以在屬性查找和函數(shù)調(diào)用時(shí)注入自定義行為峦朗?聽起來很強(qiáng)大建丧?沒錯(cuò),很強(qiáng)大波势。

看代碼

require('harmony-reflect')

function myComponentFactory() {
    let suffix = ''

    return {
        setSuffix: suff => suffix = suff,
        printValue: value => console.log(`value is ${value + suffix}`)
    }
}

function toLowerDecorator(inner) {
    return new Proxy(inner, {
        get: (target, name) => {
            return (name === 'printValue')
                ? value => target.printValue(value.toLowerCase())
                : target[name]
        }
    })
}

function validatorDecorator(inner) {
    return new Proxy(inner, {
        get: (target, name) => {
            return (name === 'printValue')
                ? value => {
                    const isValid = ~value.indexOf('my')

                    setTimeout(() => {
                        if (isValid) target.printValue(value)
                        else console.log('not valid man...')
                    }, 500)
                }
                : target[name]
        }
    })
}

const component = toLowerDecorator(validatorDecorator(myComponentFactory()))
component.setSuffix('!')
component.printValue('My Value')
component.printValue('Invalid Value')

首先翎朱,這是什么?

require('harmony-reflect')

因?yàn)槌呦常矣?node.js 運(yùn)行代碼拴曲,然而 node.js 暫時(shí)還不支持代理模塊。如果你想要在 node 中使用代理需要使用以下代碼:

node.exe --harmony-proxies

即使這樣凛忿,在寫這篇博客時(shí)澈灼,node 中的代理模塊依舊不是ES6的標(biāo)準(zhǔn)模塊。然而店溢,如果你:

npm install harmony-reflect

并向之前一樣在代碼中引入該模塊叁熔,那么你會(huì)得到一個(gè)接近最新ES6標(biāo)準(zhǔn)的代理對(duì)象來使用,而你現(xiàn)在仍必須使用上面的方法床牧。(我猜測(cè) npm 模塊的底層使用的仍是不符合ES6規(guī)范的代理對(duì)象荣回。)

接下來你會(huì)發(fā)現(xiàn)組件依舊沒有變化,而裝飾方法變得不同了:

function toLowerDecorator(inner) {
    return new Proxy(inner, {
        get: (target, name) => {
            return (name === 'printValue')
                ? value => target.printValue(value.toLowerCase())
                : target[name]
        }
    })
}

代理賦予你無比強(qiáng)大的力量戈咳,值得你閱讀 MDN 上代理部分心软。

代理

在這里,裝飾器將內(nèi)部對(duì)象作為參數(shù)輸入著蛙,并返回它的代理糯累。在代理中,我們只處理一件事:屬性訪問册踩。我們通過為 “get” 處理程序添加自定義行為來做到這點(diǎn)泳姐。

我們測(cè)試一下看看屬性是否是我們想裝飾的屬性,如果是則返回新的裝飾器方法(在這個(gè)例子中暂吉,方法就是將值轉(zhuǎn)換為小寫并傳遞值給內(nèi)部對(duì)象的 printValue 方法)胖秒;如果屬性名不符合就直接返會(huì)內(nèi)部對(duì)象的成員。

細(xì)心的你一定會(huì)發(fā)現(xiàn)我們這里又使用到了閉包慕的。

代理模式的優(yōu)劣勢(shì)

這里的關(guān)鍵點(diǎn)是阎肝,雖然為了創(chuàng)建我們的代理對(duì)象不得不做一些額外的工作,但無論裝飾對(duì)象中有多少個(gè)成員肮街,裝飾器都不會(huì)變的更復(fù)雜风题。所以,代理模式有2個(gè)優(yōu)點(diǎn):

  1. 它不是猴子補(bǔ)丁
  2. 不必手動(dòng)重新定義內(nèi)部對(duì)象的每個(gè)成員

然而,這可能有點(diǎn)殺雞用牛刀了沛硅,原型繼承有著相同的優(yōu)勢(shì)眼刃。代理的實(shí)現(xiàn)是被用來處理面向切片風(fēng)格的東西,而不是裝飾器摇肌。

還有擂红,就像我之前說的,支持還不夠好围小。如果你在 node 環(huán)境中昵骤,那你可以用我之前的方法 polyfill。然而肯适,如果你在瀏覽器環(huán)境中变秦,現(xiàn)在所有的 IE 版本都不支持, Chrome 也只在 49 版本支持框舔。不幸的是蹦玫,從我的理解看來,可能在瀏覽器中 ployfill 這個(gè)特性將會(huì)很困難雨饺,很可能會(huì)造成嚴(yán)重的性能問題苍糠。

方法五:中間件

之前的那些例子都有一個(gè)非常棒的特性簸呈,那就是初始的對(duì)象不必知道它被裝飾了。通過閉包碴里,猴子補(bǔ)丁歧焦,繼承或者代理來擴(kuò)展初始對(duì)象的行為而不必修改它移斩,這就是面向?qū)ο笤O(shè)計(jì)開閉原則

假設(shè)基礎(chǔ)對(duì)象一開始就知道自己的創(chuàng)建過程中會(huì)被一個(gè)特定的方法裝飾绢馍,會(huì)怎么樣向瓷?還有,假設(shè)想在基礎(chǔ)功能和裝飾器之間增加更多影響舰涌,會(huì)怎么樣猖任?假設(shè)通過把一些裝飾器的邏輯放到基礎(chǔ)對(duì)象中使裝飾器的代碼更為簡單,會(huì)怎么樣?

看代碼

function myComponentFactory() {
    let suffix = ''
    const instance = {
        setSuffix: suff => suffix = suff,
        printValue: value => console.log(`value is ${value + suffix}`),
        addDecorators: decorators => {
            let printValue = instance.printValue
            decorators.slice().reverse().forEach(decorator => printValue = decorator(printValue))
            instance.printValue = printValue
        }
    }
    return instance
}

function toLowerDecorator(inner) {
    return value => inner(value.toLowerCase())
}

function validatorDecorator(inner) {
    return value => {
        const isValid = ~value.indexOf('My')

        setTimeout(() => {
            if (isValid) inner(value)
            else console.log('not valid man...')
        }, 500)
    }
}

const component = myComponentFactory()
component.addDecorators([toLowerDecorator, validatorDecorator])
component.setSuffix('!')
component.printValue('My Value')
component.printValue('Invalid Value')

注意到主要的區(qū)別了么瓷耙?我們的初始對(duì)象知道它會(huì)被裝飾朱躺,并提供了一個(gè)特別的方法來添加裝飾器。這里組件設(shè)置自己的裝飾鏈搁痛,你只需提供裝飾方法的列表:

component.addDecorators([toLowerDecorator, validatorDecorator])

“addDecorators” 方法會(huì)遍歷傳入到方法中的裝飾器长搀,然后將最后一個(gè)裝飾器方法執(zhí)行后的結(jié)果賦給公共成員變量。這就是基礎(chǔ)對(duì)象給自身設(shè)置裝飾鏈鸡典。值得注意的是源请,方法里翻轉(zhuǎn)了裝飾器調(diào)用的順序,為了參數(shù)傳遞時(shí)更具可讀性:

addDecorators: decorators => {
    let printValue = instance.printValue
    decorators.slice().reverse().forEach(decorator => printValue = decorator(printValue))
    instance.printValue = printValue
}

裝飾器方法本身將便的十分簡單,它所要做的全部就是更具需要裝飾傳入的方法并返回這個(gè)方法谁尸。

function toLowerDecorator(inner) {
    return value => inner(value.toLowerCase())
}

中間件的優(yōu)劣勢(shì)
通過基礎(chǔ)對(duì)象自身來創(chuàng)建裝飾鏈舅踪,能夠獲得裝飾器更多的控制權(quán)。在這個(gè)例子中症汹,它被用來通過 reverse() 方法來改變裝飾器數(shù)組的順序硫朦。

在創(chuàng)建裝飾鏈時(shí)獲得更多的控制權(quán)也導(dǎo)致了裝飾方法便得極其簡單。

因此背镇,通過將對(duì)象設(shè)置為可以被裝飾和完成建立裝飾鏈的工作咬展,我們達(dá)成了這些目標(biāo):

  1. 簡單的裝飾方法
  2. 建立裝飾鏈時(shí),更多的控制權(quán)
  3. 簡單地建立裝飾列表瞒斩,只需傳遞一個(gè)有順序的裝飾器數(shù)組的方法破婆,而不必關(guān)心特殊裝飾者模式實(shí)現(xiàn)的構(gòu)造機(jī)制
  4. 依舊符合開閉原則,基本實(shí)現(xiàn)允許在不修改原始對(duì)象的情況下完成裝飾
  5. 它不是猴子補(bǔ)丁胸囱,也不依賴于代理

這是最復(fù)雜的實(shí)現(xiàn)方式祷舀,如果你設(shè)置了一些重量級(jí)的裝飾器,需要更多的管理而不是簡單的包裝烹笔,那么這個(gè)實(shí)現(xiàn)可能適合你裳扯。

我稱呼它為“中間件”實(shí)現(xiàn),是因?yàn)椋?/p>

  1. 我想不出比這個(gè)更好的(譯者:- -||)
  2. Dan Abramov使用相同的方法在他的 redux 中間件實(shí)現(xiàn)中

結(jié)論

我們著眼于用5個(gè)不同的技術(shù)來實(shí)現(xiàn)裝飾者模式谤职,在這過程中我們學(xué)到了不少饰豺。

  1. 一個(gè)原始的做法,它需要手動(dòng)地從內(nèi)部對(duì)象復(fù)制每個(gè)成員到裝飾對(duì)象上允蜈。但我們從中學(xué)到了通過閉包來遮蓋變量冤吨,就好像它們是私有的。
  2. 用猴子補(bǔ)丁的方法解決了“復(fù)制每個(gè)成員”的問題饶套,但是它也有相當(dāng)大的副作用漩蟆。
  3. 用原型繼承的方式解決了“復(fù)制每個(gè)成員”的問題,似乎完美無缺妓蛮。
  4. 使用 ES6 代理對(duì)象又一次解決了之前的問題怠李。然而,代理對(duì)象還沒有被很好的支持蛤克,雖然它是無比強(qiáng)大的捺癞,但也強(qiáng)大到超出了這個(gè)使用場景。在當(dāng)前場景下咖耘,它并不能比原型繼承做得更多翘簇。
  5. 在“中間件”實(shí)現(xiàn)中,基礎(chǔ)對(duì)象設(shè)置了自身的裝飾鏈儿倒,這使得它和簡單的裝飾方法一起成為一個(gè)強(qiáng)大并靈活的實(shí)現(xiàn)版保。

從中我們能總結(jié)哪些結(jié)論呜笑?

每個(gè)實(shí)現(xiàn)方式都使用了閉包。這應(yīng)該能讓你明白它在 JavaScript 中有多重要彻犁。如果你仍不理解它叫胁,退回去再讀一次,或者去閱讀一些別人更好的描述汞幢。

哪個(gè)是明顯的贏家驼鹅?當(dāng)我開始寫這篇的時(shí)候,我期望每個(gè)技術(shù)都有它的優(yōu)勢(shì)和劣勢(shì)森篷,取決于使用場景输钩。事實(shí)上,寫到最后我認(rèn)為只有2種實(shí)現(xiàn)方式值得被使用:

  1. 原型繼承
  2. 中間件

只有當(dāng)需要對(duì)裝飾鏈進(jìn)行更多控制的時(shí)候才使用中間件的方式仲智,否則买乃,原型繼承似乎對(duì)我來說就是最終贏家。它具有所有的優(yōu)點(diǎn):

  1. 不需要修改基礎(chǔ)對(duì)象
  2. 不需要復(fù)制每個(gè)成員到新的對(duì)象
  3. 不是猴子補(bǔ)丁
  4. 不支持差的代碼
  5. 相對(duì)簡單的裝飾方法

附錄

ES6

我在本文中使用ES6語法出于以下多種原因的:

  1. 我愛上了 ES6 中的許多事钓辆,尤其是箭頭函數(shù)和 const 關(guān)鍵字
  2. 最新的 node.js 中已經(jīng)原生支持它的大部分功能剪验,也可以通過一個(gè)簡單的 babel 轉(zhuǎn)換在瀏覽器中運(yùn)行 ES6
  3. 用它更容易寫文章中的例子(缺點(diǎn)是,這對(duì)沒有學(xué)過 ES6 的讀者并不是這樣)
  4. 最近我花了些時(shí)間在讀和寫一些 React 的代碼前联,它所有都是用 ES6 的功戚,所以我們是時(shí)候都上這條船了(譯者:歪果仁動(dòng)不動(dòng)就開船,果仁一言不合就開車)
  5. 我開始學(xué)習(xí) ES6 在 babel 的官網(wǎng)似嗤,如果你也想開始學(xué)習(xí) ES6 可以從這里開始

為什么我使用 ES6 卻不適用 class 關(guān)鍵字哪啸臀?有兩個(gè)非常重要的原因:

  1. 類在 ES6 中有很多問題,這有一整篇文章關(guān)于它双谆。(對(duì)我來說壳咕,真正的煩惱是缺乏私有變量和這樣做的“意義是什么”席揽,這個(gè)關(guān)鍵字比一個(gè)簡單的工廠方法做得更少顽馋。)
  2. 也許更重要的是為了這篇文章:我們談?wù)摰氖茄b飾器,然而裝飾器很難和 ES6 class 語法一起工作幌羞。正因?yàn)榇擞许?xiàng)提議在 ES7 中應(yīng)當(dāng)解決裝飾器的問題寸谜。然而,正如我希望你看到這篇文章属桦,如果你繼續(xù)使用函數(shù)語法熊痴,通過簡單的 JavaScript 語法就能創(chuàng)建功能強(qiáng)大的裝飾器。
  3. 無論是 new 關(guān)鍵字還是 class 關(guān)鍵字在 ES6 中都讓人迷惑聂宾。這看上去讓 JavaScript 便得
    把它們加進(jìn) JavaScript 中看上去會(huì)讓從傳統(tǒng)語言果善,像 Java,轉(zhuǎn)過來的人感覺更舒適系谐,但結(jié)果是笨重的巾陕,只會(huì)掩蓋原型的真正能力和簡單讨跟。這里是另一篇優(yōu)秀的文章關(guān)于剛剛所提到的。

源文件

  1. 閉包
  2. 猴子補(bǔ)丁
  3. 原型繼承
  4. 代理
  5. 中間件

原文:The decorator pattern in JavaScript using closures, monkey patching, prototypes, proxies and 'middleware'

------------------------- 華麗的分割線 -----------------------------
最后譯者推薦飛狐系列鄙煤,對(duì)理解JS設(shè)計(jì)模式很有幫助晾匠。

PPS:翻譯的好壞的確是由語文水平?jīng)Q定的,而非外語水平梯刚。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凉馆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子亡资,更是在濱河造成了極大的恐慌澜共,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锥腻,死亡現(xiàn)場離奇詭異咳胃,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)旷太,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門展懈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人供璧,你說我怎么就攤上這事存崖。” “怎么了睡毒?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵来惧,是天一觀的道長。 經(jīng)常有香客問我演顾,道長供搀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任钠至,我火速辦了婚禮葛虐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘棉钧。我一直安慰自己屿脐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布宪卿。 她就那樣靜靜地躺著的诵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪佑钾。 梳的紋絲不亂的頭發(fā)上西疤,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音休溶,去河邊找鬼代赁。 笑死撒遣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的管跺。 我是一名探鬼主播义黎,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼豁跑!你這毒婦竟也來了廉涕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤艇拍,失蹤者是張志新(化名)和其女友劉穎狐蜕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卸夕,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡层释,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了快集。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贡羔。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖个初,靈堂內(nèi)的尸體忽然破棺而出乖寒,到底是詐尸還是另有隱情,我是刑警寧澤院溺,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布楣嘁,位于F島的核電站,受9級(jí)特大地震影響珍逸,放射性物質(zhì)發(fā)生泄漏逐虚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一谆膳、第九天 我趴在偏房一處隱蔽的房頂上張望叭爱。 院中可真熱鬧,春花似錦摹量、人聲如沸涤伐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至祝迂,卻和暖如春睦尽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背型雳。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國打工当凡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留山害,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓沿量,卻偏偏與公主長得像浪慌,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子朴则,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容