[譯]探祕ES2016中的Decorators

迭代器(iterators)、生成器(generators)、數組推導式(array comprehensions); 隨着JavaScript與Python之間的類似度與日俱增,對於這種變化,我絕對是最high的那個。那今天咱們就來聊聊又一個Python化在ES2016(也叫ES7)裏的體現——裝飾器(Decorators),by Yehuda Katzjavascript

裝飾器模式

裝飾器(Decorator)究竟是個什麼鬼?來讓咱們從頭兒說,在Python裏,裝飾器(Decorator)是一種極簡的調用高階函數(higher-order function)的語法。Python裏,裝飾器(Decorator)就是一個函數,她接受另外一個函數做爲參數,而後在不直接修改這個函數的狀況下,擴展該函數的行爲,最終再將該函數返回。Python裏一個最簡單的裝飾器(Decorator)使用起來長這個樣子:html

@mydecorator
def myfunc():
    pass

最上面那個(@mydecorator)就是一個裝飾器(Decorator),並且和ES2016(ES7)裏的裝飾器(Decorator)在特徵上沒什麼區別,因此這裏就要注意嘍!java

@符號告訴解析器咱們正在使用一個名叫mydecorator的裝飾器(Decorator),而且mydecorator就是真實的這個裝飾器(Decorator)的定義函數的名字。裝飾器(Decorator)要作的就是接受一個參數(那個即將被"裝飾"的函數),封裝額外功能,而後返回原被裝飾的函數。python

當你不想修改一個函數,又想經過該函數的輸入、輸出作點兒額外工做的時候,裝飾器(Decorator)就顯得格外耀眼了。這類功能常見於:緩存、訪問控制、鑑權、監控、計/定時器、日誌、級別控制等等。react

ES五、ES2015(ES6)裏的裝飾器(Decorator)

ES5裏,想要經過純粹的函數實現一個裝飾器(Decorator),很麻煩!從ES2015(ES6)開始,類原生支持繼承,咱們就須要一種更好的方式在多個類之間共享同一段代碼功能。一種更好的分配法。git

Yehuda在他的的提議裏認爲經過裝飾器(Decorator)在設計階段使用註解(annotating)、修改JavaScript類、屬性和字面量對象等方式來保持代碼的優雅。有興趣的朋友看這裏:Yehuda's Decorator Proposales6

下面仍是讓咱們一塊兒用ES2016(ES7)的裝飾器(Decorator)來試試拳腳吧!github

上手ES2016的裝飾器(Decorator)

先回顧一下咱們在Python裏學到的東東。ES2016(ES7)裏的裝飾器(decorator)是這樣一個表達式,她會返回一個函數,這個函數接受三個參數,分別是:targetnameproperty descriptor。咱們經過給這個表達式加前綴@,而且將這段表達式放在想要「裝飾」的內容上面。裝飾器(Decorator)能夠經過類或者屬性來定義。shell

裝飾一個屬性

下面是一個簡單的Cat類:express

class Cat{
    meow(){
        return `${this.name} says Meow!`;
    }
}

prototype的方式定義這個類,在Cat.prototype上增長meow方法,大概是這個樣子:

Object.defineProperty(Cat.prototype, 'meow', {
    value: specifiedFunction,
    enumerable: false,
    configurable: true,
    writable: true
});

咱們想要一個屬性或者方法不可被賦值運算符改變。放一個裝飾器(Decorator)在這個屬性上面就行。咱們來寫一個@readonly裝飾器(Decorator):

function readonly(target, name, descriptor){
    descriptor.writable = false;
    return descriptor;
}

而後,把這個裝飾器(Decorator)加在meow屬性上:

class Cat{
    @readonly
    meow(){
        return `${this.name} says Meow!`;
    }
}

裝飾器(Decorator)是一個會被執行的表達式,她還必須再返回一個函數。意思是,@readonly或者@something(parameter)均可以是合法的裝飾器(Decorator)。

那麼,在property descriptor被註冊到Cat.prototype以前,執行引擎會先執行裝飾器(Decorator):

let descriptor = {
    value: specifiedFunction,
    enumerable: false,
    configurable: true,
    writable: true
};

//裝飾器和`Object.defineProperty`具備相同的參數列表,
//有機會在真正的`defineProperty`之行以前作點兒事情
descriptor = readonly(Cat.prototype, 'meow', descriptor) || descriptor;
defineDecoratedProperty(Cat.prototype, 'meow', descriptor);

就這樣,meow如今成了只讀參數。來用如下代碼驗證咱們的實現:

var garfield = new Cat();

garfield.meow = function(){
    console.log('I want lasagne!');
};

//Exception: Attempted to assign to readonly property

碉堡了,有木有?待會兒咱們就來瞧瞧如何裝飾一個類(只玩兒屬性是否是有點兒low)。不過咱們仍是先來看一個類庫(https://github.com/jayphelps/core-decora... by Jay Phelps)。儘管她還年輕,不過ES2016裏的裝飾器(Decorator)已經均可以經過她使用了哦。

和上面咱們寫的給meow屬性的@readonly裝飾器同樣,她已內置了@readonly實現,引入便可使用:

import { readonly } from 'core-decorators';

class Meal{
    @readonly
    entree = 'steak';
}

var dinner = new Meal();
dinner.entree = 'salmon';
//Cannot assign to read only property 'entree' of [object Object]

core-decorators裏面還有其餘經常使用的裝飾器(Decorator)工具,譬如:@deprecate,當API須要一些變動提示信息時,能夠寫成這樣:

調用console.warn()輸出廢棄提示信息。你也能傳入一個自定義的信息覆蓋默認值。甚至還能夠給一個包含url的options對象,讓用戶能夠了解更多

import { deprecate } from 'core-decorators';

class Person{
    @deprecate
    facepalm() {}

    @deprecate('We stopped facepalming')
    facepalmHard() {}

    @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
    facepalmHarder() {}
}

let captainPicard = new Person();

captainPicard.facepalm();
//DEPRECATION Person#facepalm: This function will be removed in future versions.

captainPicard.facepalmHard();
//DEPRECATION Person#facepalmHard: We stopped facepalming

captainPicard.facepalmHarder();
//DEPRECATION Person#facepalmHarder: We stopped facepalming
//
//     see: http://knowyourmeme.com/memes/facepalm for more details.
//

裝飾一個類

下面咱們再來看看如何裝飾一個類。在下面這個例子中,標準提議中稱裝飾器(Decorator)也能接受一個構造器。有一個小說類MySuperHero,咱們來爲她寫一個簡單的@superhero裝飾器:

function superhero(target){
    target.isSuperhero = true;
    target.power = 'flight';
}

@superhero
class MySuperHero{}

console.log(MySuperHero.isSuperhero);//true

咱們還能再進一步,但願裝飾器(Decorator)可以接受參數獲取不一樣的行爲,就像工廠方法那樣:

function superhero(isSuperhero) {
    return function(target){
        target.isSuperhero = isSuperhero;
    };
}

@superhero(true)
class MySuperHeroClass{}

console.log(MySuperHeroClass.isSuperhero);//true

@superhero(false)
class MySuperHeroClass{}

console.log(MySuperHeroClass.isSuperhero);//false

ES2016的裝飾器(Decorator)在屬性和類上皆可工做。裝飾器能夠自動獲取傳入的property nametarget object,咱們稍後會講到。裝飾器(Decorator)裏能拿到descriptor使得譬如:用getter替換屬性獲取、自動綁定等之前要很繁瑣才能完成的工做變爲可能。

ES2016裝飾器(Decorator)和Mixins

最近看Reg Braithwaite寫ES2016 Decorators as mixins,以及以前的Functional mixins,很是贊同他的觀點。Reg經過一個Helper向任意target(class prototype 或者對象)中植入不一樣的行爲,以此來表示一個具有指定行爲的類。Functional mixin將不一樣的實例行爲植入類的prototype中,實現參考以下:

function mixin(behaviour, sharedBehaviour = {}) {
    const instanceKeys = Reflect.ownKeys(behaviour);
    const sharedKeys = Reflect.ownKeys(sharedBehaviour);
    const typeTag = Symbol('isa');

    function _mixin(target) {
        for (let property of instanceKeys) {
            Object.defineProperty(target.prototype, property, { value: behaviour[property] });
        }
        Object.defineProperty(target, typeTag, { value: true });
        return target;
    }
    for (let property of sharedKeys){
        Object.defineProperty(_mixin, property, {
            value: sharedBehaviour[property],
            enumerable: sharedBehaviour.propertyIsEnumerable(property)
        });
    }
    Object.defineProperty(_mixin, Symbol.hasInstance, {
        value: (i) => !!i[typeTag]
    });
    return _mixin;
}

屌!如今咱們能夠定義mixins,而後用她們裝飾其餘類了。想象一下咱們有如下簡單ComicBookCharacter類:

class ComicBookCharacter{
    constructor(first, last) {
        this.firstName = first;
        this.lastName = last;
    }

    realName() {
        return this.firstName + ' ' + this.lastName;
    }
}

這估計是史上最無聊的角色了,不過咱們能夠定義一些mixins來給她提高一下能力,諸如SuperPowersUtilityBelt。來試試用Reg的mixins Helper吧:

const SuperPowers = mixin({
    addPower(name) {
        this.powers().push(name);
        return this;
    },
    powers() {
        return this._powers_pocessed || (this._powers_pocessed = []);
    }
});

const UtilityBelt = mixin({
    addToBelt(name) {
        this.utilities().push(name);
        return this;
    },

    utilities() {
        return this._utility_items || (this._utility_items = []);
    }
});

有了這些,咱們如今能夠前綴@符號,把上述兩個mixins做爲裝飾器(Decorator)掛在ComicBookCharacter上爲其提供額外功能了。注意下面代碼多裝飾器(Decorators)是如何做用於類上的:

@SuperPowers
@UtilityBelt
class ComicBookCharacter {
    constructor(first, last) {
        this.firstName = first;
        this.lastName = last;
    }

    realName() {
        return this.firstName + ' ' + this.lastName;
    }
}

好吧,咱們如今要用上面定義好的類來創造一個蝙蝠俠嘍:

const batman = new ComicBookCharacter('Bruce', 'Wayne');
console.log(batman.realName());
//Bruce Wayne

batman.addToBelt('batarang');
batman.addToBelt('cape');

console.log(batman.utilities());
//['batarang', 'cape']

batman.addPower('detective');
batman.addPower('voice sounds like Gollum has asthma');

console.log(batman.powers());
//['detective', 'voice sounds like Gollum has asthma']

經過裝飾器(Decorators),代碼更加簡潔。我本身在使用過程當中將她們做爲函數調用的另外一種選擇,或者高階函數組件的Helper。
注:@WebReflection也寫了一個上述mixin的實現,你能夠在這兒看看和Reg的版本有什麼不一樣。

經過Babel使用裝飾器(Decorators)

裝飾器(Decorator)現仍處於提議階段,沒被經過列入標準。咱們要感謝Babel提供了一個試驗模式支持裝飾器(Decorator)的轉義實現,使得上述例子基本均可以直接使用。

若是使用Babel CLI,傳入以下參數便可:

babel --optional es7.decorators

或者你能夠本身寫程序調用她的transformer:

babel.transform('code', {
    optional: ['es7.decorators']
});

Babel還有個在線版的REPL;選中「Experimental」複選框就能夠激活裝飾器(Decorator),來試試?

牛逼的各類實驗

坐我旁邊的是一個超走運的小夥兒,他叫Paul Lewis,用裝飾器(Decorator)作了個試驗性的功能重塑了讀/寫DOM的體驗。其中借鑑了Wilson Page的FastDOM,不過提供了更簡單的API接口。Paul的讀/寫裝飾器(Decorator)甚至還能在@write執行時若是被裝飾的函數內部有方法或者屬性被調用來改變頁面佈局就會經過console警告你,同理當@read執行時若是有「寫」類型的DOM變動也會收到警告。

下面是Paul試驗代碼是簡單使用場景,若是在@read裏試圖變動DOM時,就會有異常信息打在console裏:

class MyComponent {
    @read
    readSomeStuff() {
        console.log('read');

        //拋出異常
        document.querySelector('.button').style.top = '100px';
    }

    @write
    writeSomeStuff() {
        console.log('write');

        //拋出異常
        document.querySelector('.button').focus();
    }
}

開始用裝飾器(Decorator)吧!

簡短地說,ES2016的裝飾器對聲明式的裝飾、註解、類型檢查以及在ES2015裏類上增長裝飾器的各類奇淫技巧都很是有好處。往深裏說,對靜態分析(編譯時的類型檢查,自動補全)也是大有裨益。

她和經典的面向對象編程(OOP)裏的裝飾器也沒有太大區別,OOP裏是在對象上提供裝飾器,或靜態、或動態的植入不一樣行爲,但卻不改變原類。
裝飾器的語義其實在flux中也有體現,不管如何,咱們仍是最好時刻關注Yehuda的更新。

最近React社區也在討論用裝飾器替換以前的mixins設計來建立高階組件了。

我我的看到這些試驗性示例非常興奮,但願你也能用Babel試一把,說不定你也搞出來點好玩的東東,而後像Paul同樣分享給你們了。

更多閱讀

特別鳴謝:Jay Phelps, Sebastian McKenzie, Paul Lewis and Surma 的意見

原文地址:exploring-es7-decorators

相關文章
相關標籤/搜索