Decorator 裝飾器

本文首發於政採雲前端團隊博客:Decorator 裝飾器

https://www.zoo.team/article/decorator

前言


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

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

什麼是裝飾器

裝飾者模式

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

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

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

怎麼使用裝飾器

三方庫使用

Babel 版本 ≥ 7.x

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

Babel 版本 ≤ 6.x

如果小於等於 6.x,則可以使用 babel-plugin-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;
  };
}

執行順序:

打印結果:

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

使用範圍

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

函數的裝飾

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

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;

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

類裝飾器的使用

以上就是類裝飾器的使用,由此我們可以得出,裝飾器還可以對類型進行靜態標記和方法擴展,還挺有用的對吧~那麼看到這裏,小夥伴們是不是發現了在實際項目中就有類裝飾器的使用,比如 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() 的作用就是直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。該方法一共接收三個參數:

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

它們共享以下可選鍵值:

數據描述符特有鍵值:

存取操作符特有鍵值:

講完 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>
    )
  }
}

總結

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

參考鏈接

裝飾器(https://www.bookstack.cn/read/es6-3rd/docs-decorator.md)

ES7 提案: Decorators 裝飾器(https://blog.csdn.net/weixin_44691608/article/details/117180409)

Object.defineProperty()(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)

babel-plugin-transform-decorators-legacy(https://www.npmjs.com/package/babel-plugin-transform-decorators-legacy)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DexxNuRG-x29dZrWCcJDuQ