<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文讀懂 JS 裝飾器,這是一個(gè)會(huì)打扮的裝飾器

          共 11451字,需瀏覽 23分鐘

           ·

          2020-07-28 12:59

          俗話說(shuō),人靠衣裝,佛靠金裝。大街上的小姐姐都喜歡把自己打扮得美美的,讓你忍不住多看幾眼,這就是裝飾的作用。

          1. 前言

          裝飾器是最新的 ECMA 中的一個(gè)提案,是一種與類(class)相關(guān)的語(yǔ)法,用來(lái)注釋或修改類和類方法。裝飾器在 Python 和 Java 等語(yǔ)言中也被大量使用。裝飾器是實(shí)現(xiàn) AOP(面向切面)編程的一種重要方式。

          下面是一個(gè)使用裝飾器的簡(jiǎn)單例子,這個(gè)?@readonly?可以將?count?屬性設(shè)置為只讀??梢钥闯鰜?lái),裝飾器大大提高了代碼的簡(jiǎn)潔性和可讀性。

          class Person {
          @readonly count = 0;
          }

          由于瀏覽器還未支持裝飾器,為了讓大家能夠正??吹叫Ч?,這里我使用 Parcel 進(jìn)行了一下簡(jiǎn)單的配置,可以去 clone 這個(gè)倉(cāng)庫(kù)后再來(lái)運(yùn)行本文涉及到的所有例子,倉(cāng)庫(kù)地址:[learn es6][https://github.com/yinguangyao/ES6]

          本文涉及到 [Object.defineProperty][https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty]、高階函數(shù)等知識(shí),如果之前沒(méi)有了解過(guò)相關(guān)概念,建議先了解后再來(lái)閱讀本文。

          2. 裝飾器模式

          在開始講解裝飾器之前,先從經(jīng)典的裝飾器模式說(shuō)起。裝飾器模式是一種結(jié)構(gòu)型設(shè)計(jì)模式,它允許向一個(gè)現(xiàn)有的對(duì)象添加新的功能,同時(shí)又不改變其結(jié)構(gòu),是作為對(duì)現(xiàn)有類的一個(gè)包裝。一般來(lái)說(shuō),在代碼設(shè)計(jì)中,我們應(yīng)當(dāng)遵循「多用組合,少用繼承」的原則。通過(guò)裝飾器模式動(dòng)態(tài)地給一個(gè)對(duì)象添加一些額外的職責(zé)。就增加功能來(lái)說(shuō),裝飾器模式相比生成子類更為靈活。

          2.1 一個(gè)英雄聯(lián)盟的例子

          下班回去和朋友愉快地開黑,當(dāng)我正在用亞索「面對(duì)疾風(fēng)吧」的時(shí)候,突然想到,如果讓我設(shè)計(jì)亞索英雄,我該怎么實(shí)現(xiàn)呢?

          我想了想,肯定會(huì)先設(shè)計(jì)一個(gè)英雄的類。

          class Hero {
          attack() {}
          }

          然后,再實(shí)現(xiàn)一個(gè) Yasuo 的類來(lái)繼承這個(gè)?Hero?類。

          class Yasuo extends Hero {
          attack() {
          console.log("斬鋼閃");
          }
          }

          我還在想這個(gè)問(wèn)題的時(shí)候,隊(duì)友已經(jīng)打了大龍,我的亞索身上就出現(xiàn)了大龍 buff 的印記。我突然想到,那該怎么給英雄增加大龍 buff 呢?那增加個(gè)大龍 buff 的屬性不行嗎?當(dāng)然不太行,要知道,英雄聯(lián)盟里面的大龍 buff 是會(huì)增加收益的。嗯,聰明的我已經(jīng)想到辦法了,再繼承一次不就好了嗎?

          class BaronYasuo extends Yasuo {
          }

          厲害了,但是如果亞索身上還有其他 buff 呢?畢竟 LOL 里面是有紅 buff、藍(lán) buff、大龍 buff等等存在,那豈不是有多少種就要增加多少個(gè)類嗎?

          可以換種思路來(lái)思考這個(gè)問(wèn)題,如果把 buff 當(dāng)做我們身上的衣服。在不同的季節(jié),我們會(huì)換上不同的衣服,到了冬天,甚至還會(huì)疊加多件衣服。當(dāng) buff 消失了,就相當(dāng)于把這件衣服脫了下來(lái)。如下圖所示:

          衣服對(duì)人來(lái)說(shuō)起到裝飾的作用,buff 對(duì)于亞索來(lái)說(shuō)也只是增強(qiáng)效果。那么,你是不是有思路了呢?沒(méi)錯(cuò),可以創(chuàng)建 Buff 類,傳入英雄類后獲得一個(gè)新的增強(qiáng)后的英雄類。

          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 類之后,就可以直接套用到英雄身上,這樣看起來(lái)是不是清爽了很多呢?這種寫法看起來(lái)很像函數(shù)組合。

          const yasuo = new Yasuo();
          const redYasuo = new RedBuff(yasuo); // 紅buff亞索
          const blueYasuo = new BlueBuff(yasuo); // 藍(lán)buff亞索
          const redBlueYasuo = new BlueBuff(redYasuo); // 紅藍(lán)buff亞索

          3. ES7 裝飾器

          decorator(裝飾器)是 ES7 中的一個(gè)提案,目前處于 stage-2 階段,提案地址:[JavaScript Decorators][https://github.com/tc39/proposal-decorators]。裝飾器與之前講過(guò)的函數(shù)組合(compose)以及高階函數(shù)很相似。裝飾器使用?@?作為標(biāo)識(shí)符,被放置在被裝飾代碼前面。在其他語(yǔ)言中,早就已經(jīng)有了比較成熟的裝飾器方案。

          3.1 Python 中的裝飾器

          先來(lái)看一下 Python 中的一個(gè)裝飾器的例子:

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

          這個(gè)?auth?裝飾器是通過(guò)檢查?cookie?來(lái)判斷用戶是否登錄的。auth?函數(shù)是一個(gè)高階函數(shù),它接收了一個(gè)?func?函數(shù)作為參數(shù),返回了一個(gè)新的?inner?函數(shù)。在?inner?函數(shù)中進(jìn)行?cookie?的檢查,由此來(lái)判斷是跳回登錄頁(yè)面還是繼續(xù)執(zhí)行?func?函數(shù)。在所有需要權(quán)限驗(yàn)證的函數(shù)上,都可以使用這個(gè)?auth?裝飾器,很簡(jiǎn)潔明了且無(wú)侵入。

          3.2 JavaScript 裝飾器

          JavaScript 中的裝飾器和 Python 的裝飾器類似,依賴于?Object.defineProperty,一般是用來(lái)裝飾類、類屬性、類方法。使用裝飾器可以做到不直接修改代碼,就實(shí)現(xiàn)某些功能,做到真正的面向切面編程。這在一定程度上和 Proxy 很相似,但使用起來(lái)比 Proxy 會(huì)更加簡(jiǎn)潔。

          注意:裝飾器目前還處于 stage-2,意味著語(yǔ)法之后也許會(huì)有變動(dòng)。裝飾器用于函數(shù)、對(duì)象等等已經(jīng)有一些規(guī)劃,請(qǐng)看:[Future built-in decorators][https://github.com/tc39/proposal-decorators/blob/master/NEXTBUILTINS.md#applying-built-in-decorators-to-other-syntactic-forms]

          3.3 類裝飾器

          裝飾類的時(shí)候,裝飾器方法一般會(huì)接收一個(gè)目標(biāo)類作為參數(shù)。下面是一個(gè)給目標(biāo)類增加靜態(tài)屬性?test?的例子:

          const decoratorClass = (targetClass) => {
          targetClass.test = '123'
          }
          @decoratorClass
          class Test {}
          Test.test; // '123'

          除了可以修改類本身,還可以通過(guò)修改原型,給實(shí)例增加新屬性。下面是給目標(biāo)類增加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

          利用高階函數(shù)的屬性,還可以給裝飾器傳參,通過(guò)參數(shù)來(lái)判斷對(duì)類進(jìn)行什么處理。

          const withLanguage = (language) => (targetClass) => {
          targetClass.prototype.language = language;
          }
          @withLanguage('Chinese')
          class Student {
          }
          const student = new Student();
          student.language; // 'Chinese'

          如果你經(jīng)常編寫 react-redux 的代碼,那么也會(huì)遇到需要將 store 中的數(shù)據(jù)映射到組件中的情況。connect 是一個(gè)高階組件,它接收了兩個(gè)函數(shù)?mapStateToProps??mapDispatchToProps以及一個(gè)組件?App,最終返回了一個(gè)增強(qiáng)版的組件。

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

          有了裝飾器之后,connect 的寫法可以變得更加優(yōu)雅。

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

          3.4 類屬性裝飾器

          類屬性裝飾器可以用在類的屬性、方法、get/set 函數(shù)中,一般會(huì)接收三個(gè)參數(shù):

          1. target:被修飾的類
          2. name:類成員的名字
          3. descriptor:屬性描述符,對(duì)象會(huì)將這個(gè)參數(shù)傳給?Object.defineProperty使用類屬性裝飾器可以做到很多有意思的事情,比如最開始舉的那個(gè)?readonly?的例子:
          function readonly(target, name, descriptor) {
          descriptor.writable = false;
          return descriptor;
          }
          class Person {
          @readonly name = 'person'
          }
          const person = new Person();
          person.name = 'tom';

          還可以用來(lái)統(tǒng)計(jì)一個(gè)函數(shù)的執(zhí)行時(shí)間,以便于后期做一些性能優(yōu)化。

          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 知名的狀態(tài)管理庫(kù) mobx 中,也通過(guò)裝飾器來(lái)將類屬性置為可觀察屬性,以此來(lái)實(shí)現(xiàn)響應(yīng)式編程。

          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 的值,會(huì)引起 autorun 中的函數(shù)自動(dòng)執(zhí)行。

          3.5 裝飾器組合

          如果你想要使用多個(gè)裝飾器,那么該怎么辦呢?裝飾器是可以疊加的,根據(jù)離被裝飾類/屬性的距離來(lái)依次執(zhí)行。

          class Person {
          @time
          @log
          say() {}
          }

          除此之外,在裝飾器的提案中,還出現(xiàn)了一種組合了多種裝飾器的裝飾器例子。目前還沒(méi)見到被使用。通過(guò)使用?decorator?來(lái)聲明一個(gè)組合裝飾器?xyz,這個(gè)裝飾器組合了多種裝飾器。

          decorator @xyz(arg, arg2 {
          @foo @bar(arg) @baz(arg2)
          }
          @xyz(1, 2) class C { }

          和下面這種寫法是一樣的。

          @foo @bar(1) @baz(2)
          class C { }

          4. 裝飾器可以做哪些有意思的事情?

          4.1 多重繼承

          之前我們?cè)谥v解 JavaScript 多重繼承的時(shí)候,使用過(guò)?mixin?的方式,這里結(jié)合裝飾器甚至還能更進(jìn)一步簡(jiǎn)化?mixin?的使用。mixin?方法將會(huì)接收一個(gè)父類列表,用其裝飾目標(biāo)類。我們理想中的用法應(yīng)該是這樣:

          @mixin(Parent1, Parent2, Parent3)
          class Child {}

          和之前實(shí)現(xiàn)多重繼承的時(shí)候?qū)崿F(xiàn)原理一致,只需要拷貝父類的原型屬性和實(shí)例屬性就可以實(shí)現(xiàn)了。這里創(chuàng)建了一個(gè)新的?Mixin?類,來(lái)將?mixins??targetClass?上面的所有屬性都拷貝過(guò)去。

          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)); // 拷貝實(shí)例屬性
          }
          }
          }

          for (let mixin of mixins) {
          copyProperties(Mixin, mixin); // 拷貝靜態(tài)屬性
          copyProperties(Mixin.prototype, mixin.prototype); // 拷貝原型屬性
          }
          return Mixin;
          }

          export default mixin

          我們來(lái)測(cè)試一下這個(gè)?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);

          最終在瀏覽器中打印出來(lái)的?child?對(duì)象是這樣的,證明了這個(gè)?mixin?是可以正常工作的。

          注意:這里的?Child?類就是前面的?Mixin?類。

          image.png-69.4kB

          也許你會(huì)問(wèn),為什么還要多創(chuàng)建一個(gè)多余的?Mixin?類呢?為什么不能直接修改?targetClass?constructor?呢?前面不是講過(guò) Proxy 可以攔截 constructor 嗎?恭喜你,你已經(jīng)想到了 Proxy 的一種使用場(chǎng)景。沒(méi)錯(cuò),這里用 Proxy 的確會(huì)更加優(yōu)雅。

          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); // 拷貝靜態(tài)屬性
          copyProperties(targetClass.prototype, mixin.prototype); // 拷貝原型屬性
          }
          // 攔截 construct 方法,進(jìn)行實(shí)例屬性的拷貝
          return new Proxy(targetClass, {
          construct(target, args) {
          const obj = new target(...args);
          for (let mixin of mixins) {
          copyProperties(obj, new mixin()); // 拷貝實(shí)例屬性
          }
          return obj;
          }
          });
          }

          4.2 防抖和節(jié)流

          以往我們?cè)陬l繁觸發(fā)的場(chǎng)景下,為了優(yōu)化性能,經(jīng)常會(huì)使用到節(jié)流函數(shù)。下面以 React 組件綁定滾動(dòng)事件為例子:

          class App extends React.Component {
          componentDidMount() {
          this.handleScroll = _.throttle(this.scroll, 500);
          window.addEveneListener('scroll', this.handleScroll);
          }
          componentWillUnmount() {
          window.removeEveneListener('scroll', this.handleScroll);
          }
          scroll() {}
          }

          在組件中綁定事件需要注意應(yīng)當(dāng)在組件銷毀的時(shí)候進(jìn)行解綁。而由于節(jié)流函數(shù)返回了一個(gè)新的匿名函數(shù),所以為了之后能夠有效解綁,不得不將這個(gè)匿名函數(shù)存起來(lái),以便于之后使用。但是在有了裝飾器之后,我們就不必在每個(gè)綁定事件的地方都手動(dòng)設(shè)置 throttle 方法,只需要在 scroll 函數(shù)添加一個(gè) 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();
          }
          }
          }
          }
          }

          使用起來(lái)比原來(lái)要簡(jiǎn)潔很多。

          class App extends React.Component {
          componentDidMount() {
          window.addEveneListener('scroll', this.scroll);
          }
          componentWillUnmount() {
          window.removeEveneListener('scroll', this.scroll);
          }
          @throttle(50)
          scroll() {}
          }

          而實(shí)現(xiàn)防抖(debounce)函數(shù)裝飾器和節(jié)流函數(shù)類似,這里也不再多說(shuō)。

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

          如果對(duì)節(jié)流和防抖函數(shù)比較感興趣,那么可以去閱讀一下這篇文章:[函數(shù)節(jié)流與函數(shù)防抖][https://juejin.im/entry/58c0379e44d9040068dc952f]

          4.3 數(shù)據(jù)格式驗(yàn)證

          通過(guò)類屬性裝飾器來(lái)對(duì)類的屬性進(jìn)行類型的校驗(yàn)。

          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
          }

          如果你覺得對(duì)屬性一個(gè)個(gè)手動(dòng)去校驗(yàn)太過(guò)麻煩,也可以通過(guò)編寫校驗(yàn)規(guī)則,來(lái)對(duì)整個(gè)類進(jìn)行校驗(yàn)。

          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();

          4.4 core-decorators.js

          core-decorators 是一個(gè)封裝了常用裝飾器的 JS 庫(kù),它歸納了下面這些裝飾器(只列舉了部分)。

          1. autobind:自動(dòng)綁定?this,告別箭頭函數(shù)和?bind
          2. readonly:將類屬性設(shè)置為只讀
          3. override:檢查子類的方法是否正確覆蓋了父類的同名方法
          4. debounce:防抖函數(shù)
          5. throttle:節(jié)流函數(shù)
          6. enumerable:讓一個(gè)類方法變得可枚舉
          7. nonenumerable:讓一個(gè)類屬性變得不可枚舉
          8. time:打印函數(shù)執(zhí)行耗時(shí)
          9. mixin:將多個(gè)對(duì)象混入類(和我們上面的 mixin 不太一樣)


          瀏覽 31
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩一级在线播放 | 能看的三级网站 | 少妇搡B午夜躁 | 成人电影无码三区 | 国产精品五月天 |