淺談JS中的裝飾器

什么是裝飾器?

裝飾器模式(Decorator Pattern)是一種結(jié)構(gòu)型設(shè)計模式,旨在促進代碼復(fù)用,可以用于修改現(xiàn)有的系統(tǒng),希望在系統(tǒng)中為對象添加額外的功能,同時又不需要大量修改原有的代碼。

JS中的裝飾器是ES7中的一個新語法,可以對、方法屬性進行修飾,從而進行一些相關(guān)功能定制, 它的寫法與Java的注解(Annotation)類似,但是功能有比較大的區(qū)別。

大家可能聽說過 組合函數(shù) 和 高階函數(shù) 的概念,也可以這么理解。

我們先來看一下以下代碼:

function doSomething(name) {
  console.log('Hi, I\'' + name);
}

funtion useLogging(func, name) {
    console.log('Starting');
    func(name);
    console.log('Finished');
}

以上邏輯不難理解,給原有的函數(shù)加一個打日志的功能,但是這樣的話,每次都要傳參數(shù)給useLogging,而且破壞了之前的代碼結(jié)構(gòu),之前直接doSomething就好了,現(xiàn)在要改成useLogging(doSomething, 'Jiang')。
那有沒有更好的方式呢,當(dāng)然有啦。

簡單裝飾器:

function useLogging(func) {
    return function() {
        console.log('Starting');
        const result = func.apply(this, arguments)
        console.log('Done');
        return result;
    }
}

const wrapped = useLogging(doSomething);

以上代碼返回了一個新的函數(shù) wrapped , 調(diào)用方式和doSomething相同,在原來的基礎(chǔ)上能做多一點事情。

doSomething('angry');
// Hi, I'angry

const wrapped = useLogging(doSomething);


wrapped('angry');
// Starting
// Hi, I'angry
// Done

怎么使用裝飾器?

裝飾器主要有兩種用法:

  • 裝飾類方法或?qū)傩?類成員)
class MyClass {
  @readonly
  method() { }
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}
  • 裝飾類
@annotation
class MyClass { }

function annotation(target) {
   target.annotated = true;
}

類成員裝飾器

類成員裝飾器用來裝飾類里面的屬性、方法、gettersetter。這個裝飾器函數(shù)調(diào)用三個參數(shù)調(diào):

  • target: 被裝飾的類的原型
  • name: 被裝飾的類、屬性、方法的名字
  • descriptor: 被裝飾的類、屬性、方法的descriptor,將傳遞給Object.defineProperty

我們來寫幾個裝飾器,代碼如下:

寫一個@readonly裝飾器,簡單版實現(xiàn):

class Example {
  @log
  add(a, b) {
    return a + b;
  }

  @unenumerable
  @readonly
  name = "alibaba"
}

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

function unenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

function log(target, name, descriptor) {
  const original = descriptor.value;
  if (typeof original === 'function') {
    descriptor.value = function(...args) {
      console.log(`Arguments: ${args}`);
      try {
        const result = original.apply(this, args);
        console.log(`Result: ${result}`);
        return result;
      } catch (e) {
        console.log(`Error: ${e}`);
        throw e;
      }
    }
  }
  return descriptor;
}

const e = new Example();

// Calling add with [2, 4]
e.add(2, 4);
e.name = 'antd'; // Error

我們可以通過Babel查看編譯后的代碼,也可以在本地編譯。

npm i @babel/core @babel/cli
npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D

.babelrc文件

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

編譯 ES6 語法輸出到文件

因為沒用全局安裝@babel/cli, 建議用 npx 命令來執(zhí)行,或者./node_modules/.bin/babel,關(guān)于npx命令,可以看下官方文檔

npx babel decorator.js --out-file complied.js

編譯后的代碼:

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
  var desc = {};
  // 拷貝屬性
  Object['ke' + 'ys'](descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;
  if ('value' in desc || desc.initializer) {
    desc.writable = true;
  }
  desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    return decorator(target, property, desc) || desc;
  }, desc);
  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined;
  }
  if (desc.initializer === void 0) {
    Object['define' + 'Property'](target, property, desc); desc = null;
  }
  return desc;
}

_applyDecoratedDescriptor(_class.prototype, "add", [log], Object.getOwnPropertyDescriptor(_class.prototype, "add"), _class.prototype)

Babel 構(gòu)建了一個 _applyDecoratedDescriptor函數(shù),用于裝飾類成員

Object.getOwnPropertyDescriptor

Object.getOwnPropertyDescriptor()方法返回指定對象上一個自有屬性對應(yīng)的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不需要從原型鏈上進行查找的屬性),不是原型鏈上的這點很關(guān)鍵。

詳情可以查看官方文檔,這里就不細說了。

var desc = {};
  // 這里對 descriptor 屬性做了一層拷貝
  Object['ke' + 'ys'](descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;
  // 沒有 value 或者 initializer 屬性的,都是 get 和 set 方法
  if ('value' in desc || desc.initializer) {
    desc.writable = true;
  }

這里的 initializer 是 Babel 為了配合 decorator 而產(chǎn)生的一個屬性,就比方說對于上面代碼中的 name 屬性,被編譯成:

_descriptor = _applyDecoratedDescriptor(_class.prototype, "name", [unenumerable, readonly], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: function initializer() {
    return "alibaba";
  }
})
desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    return decorator(target, property, desc) || desc;
  }, desc);

處理多個 decorator 的情況,這里執(zhí)行了slice()和reverse(),所以我們可以得出,一個類成員有多個裝飾器,會由內(nèi)向外執(zhí)行。

if (context && desc.initializer !== void 0) {
  desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
  desc.initializer = undefined;
}
if (desc.initializer === void 0) {
  Object['define' + 'Property'](target, property, desc); desc = null;
}
return desc;

最后無論是裝飾方法還是屬性,都會執(zhí)行:

Object["define" + "Property"](target, property, desc);

由此可見,裝飾方法本質(zhì)上還是使用 Object.defineProperty() 來實現(xiàn)的。

類裝飾器

類裝飾器相對簡單

function log(Class) {
  return (...args) => {
    console.log(args);
    return new Class(...args);
  };
}
@log
class Example {
  constructor(name, age) {
  }
}

const e = new Example('Graham', 34);
// [ 'Graham', 34 ]
console.log(e);
// Example {}

裝飾器中傳入?yún)?shù):

function log(name) {
  return function decorator(Class) {
    return (...args) => {
      console.log(`Arguments for ${name}: args`);
      return new Class(...args);
    };
  }
}

@log('Demo')
class Example {
  constructor(name, age) {}
}

const e = new Example('Graham', 34);
// Arguments for Demo: args
console.log(e);
// Example {}

應(yīng)用

在 React 中,經(jīng)常會用到 redux 或者高階組件。

class A extends React.Component {}
export default connect()(A);

裝飾器寫法:

@connect()
export default connect()(A);

總結(jié)

Decorator 雖然原理非常簡單,但是的確可以實現(xiàn)很多實用又方便的功能.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容