俗話說,人靠衣裝,佛靠金裝。大街上的小姐姐都喜歡把本身打扮得美美的,讓你忍不住多看幾眼,這就是裝飾的做用。html
裝飾器是最新的 ECMA 中的一個提案,是一種與類(class
)相關的語法,用來註釋或修改類和類方法。裝飾器在 Python 和 Java 等語言中也被大量使用。裝飾器是實現 AOP(面向切面)編程的一種重要方式。前端
下面是一個使用裝飾器的簡單例子,這個 @readonly
能夠將 count
屬性設置爲只讀。能夠看出來,裝飾器大大提升了代碼的簡潔性和可讀性。react
class Person {
@readonly count = 0;
}
複製代碼
因爲瀏覽器還未支持裝飾器,爲了讓你們可以正常看到效果,這裏我使用 Parcel 進行了一下簡單的配置,能夠去 clone 這個倉庫後再來運行本文涉及到的全部例子,倉庫地址:learn es6git
本文涉及到 Object.defineProperty、高階函數等知識,若是以前沒有了解過相關概念,建議先了解後再來閱讀本文。es6
在開始講解裝飾器以前,先從經典的裝飾器模式提及。裝飾器模式是一種結構型設計模式,它容許向一個現有的對象添加新的功能,同時又不改變其結構,是做爲對現有類的一個包裝。github
通常來講,在代碼設計中,咱們應當遵循「多用組合,少用繼承」的原則。經過裝飾器模式動態地給一個對象添加一些額外的職責。就增長功能來講,裝飾器模式相比生成子類更爲靈活。面試
下班回去和朋友愉快地開黑,當我正在用亞索「面對疾風吧」的時候,忽然想到,若是讓我設計亞索英雄,我該怎麼實現呢?編程
我靈光一閃,那確定會先設計一個英雄的類。redux
class Hero {
attack() {}
}
複製代碼
而後,再實現一個 Yasuo 的類來繼承這個 Hero
類。設計模式
class Yasuo extends Hero {
attack() {
console.log("斬鋼閃");
}
}
複製代碼
我還在想這個問題的時候,隊友已經打了大龍,個人亞索身上就出現了大龍 buff
的印記。我忽然想到,那該怎麼給英雄增長大龍 buff
呢?那增長個大龍 buff
的屬性不行嗎?
固然不太行,要知道,英雄聯盟裏面的大龍 buff
是會增長收益的。
嗯,聰明的我已經想到辦法了,再繼承一次不就行了嗎?
class BaronYasuo extends Yasuo {}
複製代碼
厲害了,可是若是亞索身上還有其餘 buff
呢?畢竟 LOL
裏面是有紅 buff
、藍 buff
、大龍 buff
等等存在,那豈不是有多少種就要增長多少個類嗎?
能夠換種思路來思考這個問題,若是把 buff
當作咱們身上的衣服。在不一樣的季節,咱們會換上不一樣的衣服,到了冬天,甚至還會疊加多件衣服。當 buff
消失了,就至關於把這件衣服脫了下來。以下圖所示:
衣服對人來講起到裝飾的做用,buff
對於亞索來講也只是加強效果。那麼,你是否是有思路了呢? 沒錯,能夠建立 Buff
類,傳入英雄類後得到一個新的加強後的英雄類。
class RedBuff extends Buff {
constructor(hero) {
this.hero = hero;
}
// 紅buff形成額外傷害
extraDamage() {
}
attack() {
return this.hero.attack() + this.extraDamage();
}
}
class BlueBuff extends Buff {
constructor(hero) {
this.hero = hero;
}
// 技能CD(減10%)
CDR() {
return this.hero.CDR() * 0.9;
}
}
class BaronBuff extends Buff {
constructor(hero) {
this.hero = hero;
}
// 回城速度縮短一半
backSpeed() {
return this.hero.backSpeed * 0.5;
}
}
複製代碼
定義好全部的 buff
類以後,就能夠直接套用到英雄身上,這樣看起來是否是清爽了不少呢?這種寫法看起來很像函數組合。
const yasuo = new Yasuo();
const redYasuo = new RedBuff(yasuo); // 紅buff亞索
const blueYasuo = new BlueBuff(yasuo); // 藍buff亞索
const redBlueYasuo = new BlueBuff(redYasuo); // 紅藍buff亞索
複製代碼
decorator
(裝飾器)是 ES7 中的一個提案,目前處於 stage-2
階段,提案地址:JavaScript Decorators。
裝飾器與以前講過的函數組合(compose
)以及高階函數很類似。裝飾器使用 @
做爲標識符,被放置在被裝飾代碼前面。在其餘語言中,早就已經有了比較成熟的裝飾器方案。
先來看一下 Python 中的一個裝飾器的例子:
def auth(func):
def inner(request,*args,**kwargs):
v = request.COOKIES.get('user')
if not v:
return redirect('/login')
return func(request, *args,**kwargs)
return inner
@auth
def index(request):
v = request.COOKIES.get("user")
return render(request,"index.html",{"current_user":v})
複製代碼
這個 auth
裝飾器是經過檢查 cookie
來判斷用戶是否登陸的。auth
函數是一個高階函數,它接收了一個 func
函數做爲參數,返回了一個新的 inner
函數。
在 inner
函數中進行 cookie
的檢查,由此來判斷是跳回登陸頁面仍是繼續執行 func
函數。
在全部須要權限驗證的函數上,均可以使用這個 auth
裝飾器,很簡潔明瞭且無侵入。
JavaScript 中的裝飾器和 Python 的裝飾器相似,依賴於 Object.defineProperty
,通常是用來裝飾類、類屬性、類方法。
使用裝飾器能夠作到不直接修改代碼,就實現某些功能,作到真正的面向切面編程。這在必定程度上和 Proxy
很類似,但使用起來比 Proxy
會更加簡潔。
注意:裝飾器目前還處於 stage-2,意味着語法以後也許會有變更。裝飾器用於函數、對象等等已經有一些規劃,請看:Future built-in decorators
裝飾類的時候,裝飾器方法通常會接收一個目標類做爲參數。下面是一個給目標類增長靜態屬性 test
的例子:
const decoratorClass = (targetClass) => {
targetClass.test = '123'
}
@decoratorClass
class Test {}
Test.test; // '123'
複製代碼
除了能夠修改類自己,還能夠經過修改原型,給實例增長新屬性。下面是給目標類增長 speak
方法的例子:
const withSpeak = (targetClass) => {
const prototype = targetClass.prototype;
prototype.speak = function() {
console.log('I can speak ', this.language);
}
}
@withSpeak
class Student {
constructor(language) {
this.language = language;
}
}
const student1 = new Student('Chinese');
const student2 = new Student('English');
student1.speak(); // I can speak Chinese
student2.speak(); // I can speak Chinese
複製代碼
利用高階函數的屬性,還能夠給裝飾器傳參,經過參數來判斷對類進行什麼處理。
const withLanguage = (language) => (targetClass) => {
targetClass.prototype.language = language;
}
@withLanguage('Chinese')
class Student {
}
const student = new Student();
student.language; // 'Chinese'
複製代碼
若是你常常編寫 react-redux 的代碼,那麼也會遇到須要將 store
中的數據映射到組件中的狀況。connect
是一個高階組件,它接收了兩個函數 mapStateToProps
和 mapDispatchToProps
以及一個組件 App
,最終返回了一個加強版的組件。
class App extends React.Component {
}
connect(mapStateToProps, mapDispatchToProps)(App)
複製代碼
有了裝飾器以後,connect 的寫法能夠變得更加優雅。
@connect(mapStateToProps, mapDispatchToProps)
class App extends React.Component {
}
複製代碼
類屬性裝飾器能夠用在類的屬性、方法、get/set
函數中,通常會接收三個參數:
Object.defineProperty
使用類屬性裝飾器能夠作到不少有意思的事情,好比最開始舉的那個 readonly
的例子:
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly name = 'person'
}
const person = new Person();
person.name = 'tom';
複製代碼
還能夠用來統計一個函數的執行時間,以便於後期作一些性能優化。
function time(target, name, descriptor) {
const func = descriptor.value;
if (typeof func === 'function') {
descriptor.value = function(...args) {
console.time();
const results = func.apply(this, args);
console.timeEnd();
return results;
}
}
}
class Person {
@time
say() {
console.log('hello')
}
}
const person = new Person();
person.say();
複製代碼
在 react 知名的狀態管理庫 mobx 中,也經過裝飾器來將類屬性置爲可觀察屬性,以此來實現響應式編程。
import {
observable,
action,
autorun
} from 'mobx'
class Store {
@observable count = 1;
@action
changeCount(count) {
this.count = count;
}
}
const store = new Store();
autorun(() => {
console.log('count is ', store.count);
})
store.changeCount(10); // 修改 count 的值,會引發 autorun 中的函數自動執行。
複製代碼
若是你想要使用多個裝飾器,那麼該怎麼辦呢?裝飾器是能夠疊加的,根據離被裝飾類/屬性的距離來依次執行。
class Person {
@time
@log
say() {}
}
複製代碼
除此以外,在裝飾器的提案中,還出現了一種組合了多種裝飾器的裝飾器例子。目前還沒見到被使用。
經過使用 decorator
來聲明一個組合裝飾器 xyz
,這個裝飾器組合了多種裝飾器。
decorator @xyz(arg, arg2 {
@foo @bar(arg) @baz(arg2)
}
@xyz(1, 2) class C { }
複製代碼
和下面這種寫法是同樣的。
@foo @bar(1) @baz(2)
class C { }
複製代碼
在實現 JavaScript 多重繼承的時候,可使用 mixin
的方式,這裏結合裝飾器甚至還能更進一步簡化 mixin
的使用。
mixin
方法將會接收一個父類列表,用其裝飾目標類。咱們理想中的用法應該是這樣:
@mixin(Parent1, Parent2, Parent3)
class Child {}
複製代碼
和以前實現多重繼承的時候實現原理一致,只須要拷貝父類的原型屬性和實例屬性就能夠實現了。
這裏建立了一個新的 Mixin
類,來將 mixins
和 targetClass
上面的全部屬性都拷貝過去。
const mixin = (...mixins) => (targetClass) => {
mixins = [targetClass, ...mixins];
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if (key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
class Mixin {
constructor(...args) {
for (let mixin of mixins) {
copyProperties(this, new mixin(...args)); // 拷貝實例屬性
}
}
}
for (let mixin of mixins) {
copyProperties(Mixin, mixin); // 拷貝靜態屬性
copyProperties(Mixin.prototype, mixin.prototype); // 拷貝原型屬性
}
return Mixin;
}
export default mixin
複製代碼
咱們來測試一下這個 mixin
方法是否可以正常工做吧。
class Parent1 {
p1() {
console.log('this is parent1')
}
}
class Parent2 {
p2() {
console.log('this is parent2')
}
}
class Parent3 {
p3() {
console.log('this is parent3')
}
}
@mixin(Parent1, Parent2, Parent3)
class Child {
c1 = () => {
console.log('this is child')
}
}
const child = new Child();
console.log(child);
複製代碼
最終在瀏覽器中打印出來的 child
對象是這樣的,證實了這個 mixin
是能夠正常工做的。
注意:這裏的
Child
類就是前面的Mixin
類。
也許你會問,爲何還要多建立一個多餘的 Mixin
類呢?爲何不能直接修改 targetClass
的 constructor
呢?前面不是講過 Proxy
能夠攔截 constructor
嗎?
恭喜你,你已經想到了 Proxy
的一種使用場景。沒錯,這裏用 Proxy
的確會更加優雅。
const mixin = (...mixins) => (targetClass) => {
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
for (let mixin of mixins) {
copyProperties(targetClass, mixin); // 拷貝靜態屬性
copyProperties(targetClass.prototype, mixin.prototype); // 拷貝原型屬性
}
// 攔截 construct 方法,進行實例屬性的拷貝
return new Proxy(targetClass, {
construct(target, args) {
const obj = new target(...args);
for (let mixin of mixins) {
copyProperties(obj, new mixin()); // 拷貝實例屬性
}
return obj;
}
});
}
複製代碼
以往咱們在頻繁觸發的場景下,爲了優化性能,常常會使用到節流函數。下面以 React 組件綁定滾動事件爲例子:
class App extends React.Component {
componentDidMount() {
this.handleScroll = _.throttle(this.scroll, 500);
window.addEveneListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEveneListener('scroll', this.handleScroll);
}
scroll() {}
}
複製代碼
在組件中綁定事件須要注意應當在組件銷燬的時候進行解綁。而因爲節流函數返回了一個新的匿名函數,因此爲了以後可以有效解綁,不得不將這個匿名函數存起來,以便於以後使用。
可是在有了裝飾器以後,咱們就沒必要在每一個綁定事件的地方都手動設置 throttle
方法,只須要在 scroll
函數添加一個 throttle
的裝飾器就好了。
const throttle = (time) => {
let prev = new Date();
return (target, name, descriptor) => {
const func = descriptor.value;
if (typeof func === 'function') {
descriptor.value = function(...args) {
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
}
}
}
}
}
複製代碼
使用起來比原來要簡潔不少。
class App extends React.Component {
componentDidMount() {
window.addEveneListener('scroll', this.scroll);
}
componentWillUnmount() {
window.removeEveneListener('scroll', this.scroll);
}
@throttle(50)
scroll() {}
}
複製代碼
而實現防抖(debounce
)函數裝飾器和節流函數相似,這裏也再也不多說。
const debounce = (time) => {
let timer;
return (target, name, descriptor) => {
const func = descriptor.value;
if (typeof func === 'function') {
descriptor.value = function(...args) {
if(timer) clearTimeout(timer)
timer = setTimeout(()=> {
fn.apply(this, args)
}, wait)
}
}
}
}
複製代碼
若是對節流和防抖函數比較感興趣,那麼能夠去閱讀一下這篇文章:函數節流與函數防抖
經過類屬性裝飾器來對類的屬性進行類型的校驗。
const validate = (type) => (target, name) => {
if (typeof target[name] !== type) {
throw new Error(`attribute ${name} must be ${type} type`)
}
}
class Form {
@validate('string')
name = 111 // Error: attribute name must be ${type} type
}
複製代碼
若是你以爲對屬性一個個手動去校驗太過麻煩,也能夠經過編寫校驗規則,來對整個類進行校驗。
const rules = {
name: 'string',
password: 'string',
age: 'number'
}
const validator = rules => targetClass => {
return new Proxy(targetClass, {
construct(target, args) {
const obj = new target(...args);
for (let [name, type] of Object.entries(rules)) {
if (typeof obj[name] !== type) {
throw new Error(`${name} must be ${type}`)
}
}
return obj;
}
})
}
@validator(rules)
class Person {
name = 'tom'
password = '123'
age = '21'
}
const person = new Person();
複製代碼
core-decorators
是一個封裝了經常使用裝飾器的 JS 庫,它概括了下面這些裝飾器(只列舉了部分)。
this
,告別箭頭函數和 bind
裝飾器雖然還屬於不穩定的語法,但在不少框架中都已經普遍使用,例如 Angular、Nestjs 等等,和 Java 中的註解用法很是類似。 裝飾器在 TypeScript 中結合反射後還有一些更高級的應用,下篇文章會進行深刻講解。
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙: