Decorator 裝飾器

語言: CN / TW / HK

前言

大家在前端開發過程中有遇到過 @ + 方法名 這種寫法嗎?當我第一次看到的時候,直接懵了,這是什麼東東……

遇到困難解決困難,在我的一番查詢後,我知道了,原來這東西叫 裝飾器 ,英文名叫 Decorator ,那它到底是幹什麼的呢?接下來就讓我跟大家說道說道~

什麼是裝飾器

裝飾者模式

裝飾者模式就是能夠在不改變物件自身的基礎上,在程式執行期間給物件動態地新增職責。打個比方,一個人在天氣冷的時候要穿棉衣,天氣熱的時候穿短袖,可無論穿什麼,本質上他還是一個人,只不過身上穿了不同的衣服。

所以簡單來說, Decorator 就是一種動態地往一個類中新增新的行為的設計模式, 它可以在類執行時, 擴充套件一個類的功能, 並且去修改類本身的屬性和方法, 使其可以在不同類之間更靈活的共用一些屬性和方法。

@ 是針對這種設計模式的一個語法糖,不過目前還處於第 2 階段提案中,使用它之前需要使用 Babel 模組編譯成 ES5 或 ES6。

怎麼使用裝飾器

三方庫使用

Babel 版本 ≥ 7.x

如果專案的 Babel 版本大於等於 7.x,那麼可以使用 @babel/plugin-proposal-decorators

  • 安裝

    npm install --save-dev @babel/plugin-proposal-decorators
  • 配置 .babelrc

    {
      "plugins": [
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
      ]
    }

Babel 版本 ≤ 6.x

如果小於等於 6.x,則可以使用 babel-plugin-transform-decorators-legacy

  • 安裝

    npm install --save-dev @babel/plugin-proposal-decorators
  • 配置 .babelrc

    {
        "plugins": ["transform-decorators-legacy"]
    }

使用方法

裝飾器的寫法是 @ + 返回裝飾器函式的表示式 ,所以其使用方法如下:

@classDecorator
class TargetClass { // 類
  @fieldDecorator
  targetField = 0; // 類例項屬性

  @funDecorator
  targetFun() { } // 類方法

  @accessorDecorator
  get targetGetFun() { } // 類訪問器
}

如果一個物件使用多個裝飾器,那麼執行順序是什麼呢?

function decorator1() {
  console.log('decorator1');
  return function decFn1(targetClass) {
    console.log('decFn1');
    return targetClass;
  };
}

function decorator2() {
  console.log('decorator2');
  return function decFn2(targetClass) {
    console.log('decFn2');
    return targetClass;
  };
}

執行順序:

列印結果:

根據以上,我們可知,裝飾器的執行順序為由外向內進入,由內向外執行。

使用範圍

根據使用方法,我們可以看出裝飾器可以應用於以下幾種型別:

  • 類(class)
  • 類例項屬性(公共、私有和靜態)
  • 類方法(公共、私有和靜態)
  • 類訪問器(公共、私有和靜態)

函式的裝飾

當我們看完裝飾器的使用方法和使用範圍時,我們發現,裝飾器不能修飾函式,那原因到底是什麼呢?原因就是函式有 函式提升

var num = 0;
function add () {
  num ++;
}
@add
function fn() {}

在這個例子中,我們想要在執行後讓 num 等於 1,但其實結果並不是這樣,因為函式提升,實際上程式碼是這樣執行的:

function add () {
  num ++;
}
@add
function fn() {}
var num;
num = 0;

如果一定要裝飾函式的話,可以採用高階函式的形式,這篇文章主要講裝飾器,有關高階函式就不在此贅述了,不瞭解的小夥伴們可自行查閱資料哈~

裝飾器原理

根據裝飾器的使用範圍,可以把它分為兩大類:類的裝飾與類方法的裝飾,下面就讓我為大家逐個分享一下。

類的裝飾

傳參

首先我們先根據一個小例子看一下裝飾器接收引數的情況:

function decorator(...args) {
  args.forEach((arg, index) => {
    console.log(`引數${index}`, arg);
  });
}

@decorator
class TargetClass { }

console.log('targetClass:', TargetClass);

列印結果如下:

看到結果,我們發現裝飾器只接收一個引數,就是被裝飾的類定義本身。

返回值

我們繼續通過一個小例子來看返回值的情況:

function returnStr(targetClass) {
  return 'hello world~';
}

function returnClass(targetClass) {
  return targetClass;
}

@returnStr
class ClassA { }

@returnClass
class ClassB { }

console.log('ClassA:', ClassA);
console.log('ClassB:', ClassB);

結果如下:

根據結果,我們發現裝飾器返回什麼輸出的就是什麼。

結論

通過以上的兩個例子,我們可以得出一下這個結論:

@decorator
class TargetClass { }

// 等同於

class TargetClass { }
TargetClass = decorator(TargetClass) || TargetClass;

所以說,裝飾器的第一個引數就是要裝飾的類,它的功能就是對類進行處理。

類裝飾器的使用

  • 新增屬性

    因為裝飾器接收的引數就是類定義本身,所以我們可以給類新增屬性:

    function addAttribute(targetClass) {
      targetClass.isUseDecorator = true;
    }
    
    @addAttribute
    class TargetClass { }
    
    console.log(TargetClass.isUseDecorator); // true

    在這個例子中,我們定義了 addAttribute 的裝飾器,用於對 TargetClass 新增 isUseDecorator 標記,這個用法就跟 Java 中的註解比較相似,僅僅是對目標型別打上一些標記。

  • 返回裝飾器函式的表示式

    上面有說裝飾器的寫法是 @ + 返回裝飾器函式的表示式 ,也就是說, @ 後邊可以不是一個方法名,還可以是 能返回裝飾器函式的表示式

    function addAttribute(content) {
      return function decFn(targetClass) {
        targetClass.content = content;
        return targetClass;
      };
    }
    
    @addAttribute('這是內容~~~')
    class TargetClass { }
    
    console.log(TargetClass.content); // 這是內容~~~

    我們看到 TargetClass 通過 addAttribute 的裝飾,添加了 content 這個屬性,並且可以向 addAttribute 傳參來給 content 屬性賦值,這種使用方法使裝飾器變得更加靈活。

  • 新增原型方法

    在前面的例子中我們新增的都是類的靜態屬性,但是既然裝飾器接收的引數就是類定義本身,那麼它也可以通過訪問類的 prototype 屬性來新增或修改原型方法:

    function decorator(targetClass) {
      targetClass.prototype.decFun = function () {
        console.log('這裡是裝飾器 decorator 新增的原型方法 decFun~');
      };
    }
    
    @decorator
    class TargetClass { }
    
    const targetClass = new TargetClass();
    
    console.log(targetClass);
    targetClass.decFun();

    結果如下:

以上就是類裝飾器的使用,由此我們可以得出,裝飾器還可以對型別進行靜態標記和方法擴充套件,還挺有用的對吧~那麼看到這裡,小夥伴們是不是發現了在實際專案中就有類裝飾器的使用,比如 react-redux 的 connect 就是一個類裝飾器、Antd 中的 Form.create 也是一個類裝飾器。

// connect
class App extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(App);

// 等同於

@connect(mapStateToProps, mapDispatchToProps)
export default class App extends React.Component {}

// Form.create
const WrappedApp = Form.create()(App);

// 等同於

@Form.create()
class App extends React.Component {}

類方法的裝飾

傳參

我們把類例項屬性、類方法、類訪問器都歸到這一類中的原因其實是因為它們三個就是作為某個物件的屬性(例項屬性、原型方法、例項訪問器屬性),也就是說它們接收的引數是類似的:

function decorator(...args) {
  args.forEach((arg, index) => {
    console.log(`引數${index}`, arg);
  });
  console.log('****************');
}

class TargetClass {
  @decorator
  field = 0;

  @decorator
  fn() { }

  @decorator
  get getFn() { }
}

const targetOne = new TargetClass();
console.log(targetOne.field, Object.getOwnPropertyDescriptor(targetOne, 'field'));

結果如下:

根據結果我們發現,類方法裝飾器接收了三個引數:類定義物件、例項屬性/方法/例項訪問器屬性名、屬性操作符。眼熟吧,沒錯,它與 Object.defineProperty() 接收的引數很像。

Object.defineProperty(obj, props, descriptor)

Object.defineProperty() 的作用就是直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回此物件。該方法一共接收三個引數:

  • 要定義屬性的物件 (obj)
  • 要定義或修改的屬性名或 Symbol (props)
  • 要定義或修改的屬性描述符 (descriptor)

而物件裡目前存在的 屬性描述符 有兩種主要形式: 資料描述符存取描述符資料描述符 是一個具有值的屬性,該值可以是可寫的,也可以是不可寫的; 存取描述符 是由 getter 函式和 setter 函式所描述的屬性。一個描述符只能是這兩者其中之一,不能同時是兩者。

它們共享以下可選鍵值:

  • configurable

    屬性是否可以被刪除和重新定義特性,預設值為 false

  • enumerable

    是否會出現在物件的列舉屬性中,預設值為 false

資料描述符特有鍵值:

  • value

    該屬性對應的值,預設值為 undefined

  • writable

    是否可以被更改,預設值為 false

存取操作符特有鍵值:

  • get

    屬性的 getter 函式,如果沒有 getter ,則為 undefined ;預設為 undefined

  • set

    屬性的 setter 函式,如果沒有 setter ,則為 undefined ;預設為 undefined

講完 Object.defineProperty() ,接下來就讓我們看看該怎麼使用它吧~

類方法裝飾器的使用

讓我們通過一個例子來了解一下:

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

class Person {
  @readonly
  name = 'zhangsan';
}

const person = new Person();
console.log(person.name, Object.getOwnPropertyDescriptor(person, 'name'));

列印結果如下:

上面程式碼說明,裝飾器會修改屬性的描述物件,然後被修改的描述物件再用來定義屬性。

結論

由此我們可以得出結論:

function changeName(target, name, descriptor) {
  descriptor.value = 'lisi';
  return descriptor;
}
class Person {
  @changeName
  name = 'zhangsan';
}
const person = new Person();

// 等同於

class Person {
  name = 'zhangsan';
}
const person = new Person();
Object.defineProperty(person, 'name', {
  value: 'lisi',
});

裝飾器的應用

在專案中,可能會遇到這樣一種情況,好幾個元件的資料都是呼叫同一個後端介面獲得,只是傳參不同,有些小夥伴們在寫程式碼的時候可能就是每個元件都去手動呼叫一次後端介面(以 React 專案為例):

...
export default class CompOne extends Component {
  ...
  getData = async () => { // 呼叫後端介面
    const data = await request('/xxx', {
      params: {
        id: '123', // 不同元件傳參不同
      },
    });
    this.setState({ data });
  }
  render() {
    ...
    return (
      <div>
        ...
        我是元件一: {data}
        ...
      </div>
    )
  }
}

遇到這種情況,我們就可以用裝飾器解決呀~

// 裝飾器
function getData(params) {
  return (Comp) => {
    class WrapperComponent extends Component {
      ...
      getData = async () => {
        const data = await request('/xxx', {
          params,
        });
        this.setState({ data });
      }
      render() {
        ...
        return (
          <Comp data={data} />
        )
      }
    }

    return Comp;
  }
}

// 元件
...
@getData({
  id: '123'
})
export default class index extends Component {
  ...
  render() {
    ...
    const data = this.props.data; // 直接從 this.props 中獲取想要的資料
    return (
      <div>
        ...
        我是元件一: {data}
        ...
      </div>
    )
  }
}

總結

好啦,今天的分享就要到此結束了哦,希望通過這篇文章大家能夠對裝飾器有一定的瞭解,如有不同意見,歡迎在評論區評論呦~就讓暴風雨來得更猛烈些吧!

參考連結

裝飾器

ES7 提案: Decorators 裝飾器

Object.defineProperty()

babel-plugin-transform-decorators-legacy

❉ 作者介紹 ❉