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

          前端需要掌握的設(shè)計(jì)模式

          共 10483字,需瀏覽 21分鐘

           ·

          2020-09-28 12:14

          王君,微醫(yī)云服務(wù)團(tuán)隊(duì)前端工程師,一個切菜比切圖還快的碼農(nóng)。

          烹飪有菜譜,游戲有攻略,各個行業(yè)都存在一些快捷又高效的“套路”。而編程的“套路”就是設(shè)計(jì)模式。

          提到設(shè)計(jì)模式,相信知道的同學(xué)都會脫口而出,五大基本原則(SOLID)和 23 種設(shè)計(jì)模式。SOLID 所指的五大基本原則分別是:單一功能原則、開放封閉原則、里式替換原則、接口隔離原則和依賴反轉(zhuǎn)原則。逐字逐句詮釋這五大基本原則違背了寫這篇文章的初衷,引用社區(qū)大佬的理解,SOLID 可以簡單概括為六個字,即“高內(nèi)聚,低耦合”:

          • 層模塊不依賴底層模塊,即為依賴反轉(zhuǎn)原則。
          • 內(nèi)部修改關(guān)閉,外部擴(kuò)展開放,即為開放封閉原則。
          • 合單一功能,即為單一功能原則。
          • 知識要求,對外接口簡單,即為迪米特法則。
          • 合多個接口,不如獨(dú)立拆分,即為接口隔離原則。
          • 成復(fù)用,子類繼承可替換父類,即為里式替換原則。

          23 種設(shè)計(jì)模式分為“創(chuàng)建型”、“行為型”和“結(jié)構(gòu)型”。具體類型如下圖:設(shè)計(jì)模式說白了就是“封裝變化”。比如“創(chuàng)建型”封裝了創(chuàng)建對象的變化過程,“結(jié)構(gòu)型”將對象之間組合的變化封裝,“行為型”則是抽離對象的變化行為。接下來,本文將以“單一功能”和“開放封閉”這兩大原則為主線,分別介紹“創(chuàng)建型”、“結(jié)構(gòu)型”和“行為型”中最具代表性的幾大設(shè)計(jì)模式。

          創(chuàng)建型

          工廠模式

          工廠模式根據(jù)抽象程度可分為三種,分別為簡單工廠、工廠方法和抽象工廠。其核心在于將創(chuàng)建對象的過程封裝其他,然后通過同一個接口創(chuàng)建新的對象。 簡單工廠模式又叫靜態(tài)工廠方法,用來創(chuàng)建某一種產(chǎn)品對象的實(shí)例,用來創(chuàng)建單一對象。

          //?簡單工廠
          class?Factory?{
          ??constructor?(username,?pwd,?role)?{
          ???this.username?=?username;
          ????this.pwd?=?pwd;
          ????this.role?=?role;
          ??}
          }

          class?CreateRoleFactory?{
          ?static?create?(username,?pwd,?role)?{
          ???return?new?Factory(username,?pwd,?role);
          ??}
          }

          const?admin?=?CreateRoleFactory.create('張三',?'222',?'admin');

          在實(shí)際工作中,各用戶角色所具備的能力是不同的,因此簡單工廠是無法滿足的,這時候就可以考慮使用工廠方法來代替。工廠方法的本意是將實(shí)際創(chuàng)建對象的工作推遲到子類中。

          class?User?{
          ?constructor?(name,?menuAuth)?{
          ???if?(new.target?===?User)?throw?new?Error('User?不能被實(shí)例化');
          ????this.name?=?name;
          ????this.menuAuth?=?menuAuth;
          ??}
          }

          class?UserFactory?extends?User?{
          ?constructor?(...props)?{
          ???super(...props);
          ??}
          ??static?create?(role)?{
          ???const?roleCollection?=?new?Map([
          ?????['admin',?()?=>?new?UserFactory('管理員',?['首頁',?'個人中心'])],
          ??????['user',?()?=>?new?UserFactory('普通用戶',?['首頁'])]
          ????])
          ????
          ????return?roleCollection.get(role)();
          ??}
          }

          const?admin?=?UserFactory.create('admin');
          console.log(admin);?//?{name:?"管理員",?menuAuth:?Array(2)}
          const?user?=?UserFactory.create('user');
          console.log(user);?//?{name:?"普通用戶",?menuAuth:?Array(1)}

          隨著業(yè)務(wù)形態(tài)的變化,一個用戶可能在多個平臺上同時存在,顯然工廠方法也不再滿足了,這時候就要用到抽象工廠。抽象工廠模式是對類的工廠抽象用來創(chuàng)建產(chǎn)品類簇,不負(fù)責(zé)創(chuàng)建某一類產(chǎn)品的實(shí)例。

          class?User?{
          ??constructor?(hospital)?{
          ????if?(new.target?===?User)?throw?new?Error('抽象類不能實(shí)例化!');
          ????this.hospital?=?hospital;
          ??}
          }
          //?浙一
          class?ZheYiUser?extends?User?{
          ??constructor(name,?departmentsAuth)?{
          ????super('zheyi_hospital');
          ????this.name?=?name;
          ????this.departmentsAuth?=?departmentsAuth;
          ??}
          }
          //?蕭山醫(yī)院
          class?XiaoShanUser?extends?User?{
          ??constructor(name,?departmentsAuth)?{
          ????super('xiaoshan_hospital');
          ????this.name?=?name;
          ????this.departmentsAuth?=?departmentsAuth;
          ??}
          }

          const?getAbstractUserFactory?=?(hospital)?=>?{
          ??switch?(hospital)?{
          ????case?'zheyi_hospital':
          ??????return?ZheYiUser;
          ??????break;
          ????case?'xiaoshan_hospital':
          ??????return?XiaoShanUser;
          ??????break;
          ??}
          }

          const?ZheYiUserClass?=?getAbstractUserFactory('zheyi_hospital');
          const?XiaoShanUserClass?=?getAbstractUserFactory('xiaoshan_hospital');

          const?user1?=?new?ZheYiUserClass('王醫(yī)生',?['外科',?'骨科',?'神經(jīng)外科']);
          console.log(user1);
          const?user2?=?newXiaoShanUserClass('王醫(yī)生',?['外科',?'骨科']);
          console.log(user2);

          小結(jié): 構(gòu)造函數(shù)和創(chuàng)建對象分離,符合開放封閉原則。

          使用場景: 比如根據(jù)權(quán)限生成不同用戶。

          單例模式

          單例模式理解起來比較簡單,就是保證一個類只能存在一個實(shí)例,并提供一個訪問它的全局接口。單例模式又分懶漢式和餓漢式兩種,其區(qū)別在于懶漢式在調(diào)用的時候創(chuàng)建實(shí)例,而餓漢式則是在初始化就創(chuàng)建好實(shí)例,具體實(shí)現(xiàn)如下:

          //?懶漢式
          class?Single?{
          ?static?getInstance?()?{
          ???if?(!Single.instance)?{
          ?????Single.instance?=?new?Single();
          ????}
          ????return?Single.instance;
          ??}
          }

          const?test1?=?Single.getInstance();
          const?test2?=?Single.getInstance();

          console.log(test1?===?test2);?//?true
          //?餓漢式
          class?Single?{
          ?static?instance?=?new?Single();

          ??static?getInstance?()?{
          ????return?Single.instance;
          ??}
          }

          const?test1?=?Single.getInstance();
          const?test2?=?Single.getInstance();

          console.log(test1?===?test2);?//?true

          小結(jié): 實(shí)例如果存在,直接返回已創(chuàng)建的,符合開放封閉原則。

          使用場景: Redux、Vuex 等狀態(tài)管理工具,還有我們常用的 window 對象、全局緩存等。

          原型模式

          對于前端來說,原型模式在常見不過了。當(dāng)新創(chuàng)建的對象和已有對象存在較大共性時,可以通過對象的復(fù)制來達(dá)到創(chuàng)建新的對象,這就是原型模式。

          //?Object.create()實(shí)現(xiàn)原型模式
          const?user?=?{
          ?name:?'zhangsan',
          ??age:?18
          };
          let?userOne?=?Object.create(user);
          console.log(userOne.__proto__);?//?{name:?"zhangsan",?age:?18}


          //?原型鏈繼承實(shí)現(xiàn)原型模式
          class?User?{
          ?constructor?(name)?{
          ???this.name?=?name;
          ??}
          ??getName?()?{
          ???return?this.name;
          ??}
          }

          class?Admin?extends?User?{
          ?constructor?(name)?{
          ???super(name);
          ??}
          ??setName?(_name)?{
          ???return?this.name?=?_name;
          ??}
          }

          const?admin?=?new?Admin('zhangsan');
          console.log(admin.getName());
          console.log(admin.setName('lisi'));

          小結(jié): 原型模式最簡單的實(shí)現(xiàn)方式---Object.create()。

          使用場景: 新創(chuàng)建對象和已有對象無較大差別時,可以使用原型模式來減少創(chuàng)建新對象的成本。

          結(jié)構(gòu)型

          裝飾器模式

          講裝飾器模式之前,先聊聊高階函數(shù)。高階函數(shù)就是一個函數(shù)就可以接收另一個函數(shù)作為參數(shù)。

          const?add?=?(x,?y,?f)?=>?{
          ?return?f(x)?+?f(y);
          }
          const?num?=?add(2,?-2,?Math.abs);
          console.log(num);?//?4

          函數(shù) add 就是一個簡單的高階函數(shù),而 add 相對于 Math.abs 來說相當(dāng)于一個裝飾器,因此這個例子也可以理解為一個簡單的裝飾器模式。在 react 中,高階組件(HOC)也是裝飾器模式的一種體現(xiàn),通常用來不改變原來組件的情況下添加一些屬性,達(dá)到組件復(fù)用的功能。

          import?React?from?'react';

          const?BgHOC?=?WrappedComponent?=>?class?extends?React.Component?{
          ?render?()?{
          ???return?(
          ?????<div?style={{?background:?'blue'?}}>
          ???????<WrappedComponent?/>
          ??????div>

          ????);
          ??}
          }

          小結(jié): 裝飾器模式將現(xiàn)有對象和裝飾器進(jìn)行分離,兩者獨(dú)立存在,符合開放封閉原則和單一職責(zé)模式。

          使用場景: es7 裝飾器、vue mixins、core-decorators 等。

          適配器模式

          適配器別名包裝器,其作用是解決兩個軟件實(shí)體間的接口不兼容的問題。以 axios 源碼為例:

          function?getDefaultAdapter()?{
          ??var?adapter;
          ??//?判斷當(dāng)前是否是?node?環(huán)境
          ??if?(typeof?process?!==?'undefined'?&&?Object.prototype.toString.call(process)?===?'[object?process]')?{
          ????//?如果是?node?環(huán)境,調(diào)用?node?專屬的?http?適配器
          ????adapter?=?require('./adapters/http');
          ??}?else?if?(typeof?XMLHttpRequest?!==?'undefined')?{
          ????//?如果是瀏覽器環(huán)境,調(diào)用基于?xhr?的適配器
          ????adapter?=?require('./adapters/xhr');
          ??}
          ??return?adapter;
          }

          //?http?adapter
          module.exports?=?function?httpAdapter(config)?{
          ??return?new?Promise(function?dispatchHttpRequest(resolvePromise,?rejectPromise)?{
          ????...
          ??}
          }
          //?xhr?adapter
          module.exports?=?function?xhrAdapter(config)?{
          ??return?new?Promise(function?dispatchXhrRequest(resolve,?reject)?{
          ????...
          ??}
          }

          其目的就是保證 node 和瀏覽器環(huán)境的入?yún)?config 一致,出參 Promise 都是同一個。

          小結(jié): 不改變原有接口的情況下,統(tǒng)一接口、統(tǒng)一入?yún)ⅰ⒔y(tǒng)一出參、統(tǒng)一規(guī)則,符合開發(fā)封閉原則。

          使用場景 :擁抱變化,兼容代碼。

          代理模式

          代理模式就是為對象提供一個代理,用來控制對這個對象的訪問。在我們業(yè)務(wù)開發(fā)中最常見的有四種代理類型:事件代理,虛擬代理、緩存代理和保護(hù)代理。本文主要介紹虛擬代理和緩存代理兩類。 提到虛擬代理,其最具代表性的例子就是圖片預(yù)加載。預(yù)加載主要是為了避免網(wǎng)絡(luò)延遲、或者圖片太大引起頁面長時間留白的問題。通常的解決方案是先給 img 標(biāo)簽展示一個占位圖,然后創(chuàng)建一個 Image 實(shí)例,讓這個實(shí)例的 src 指向真實(shí)的目標(biāo)圖片地址,當(dāng)其真實(shí)圖片加載完成之后,再將 DOM 上的 img 標(biāo)簽的 src 屬性指向真實(shí)圖片地址。

          class?ProxyImg?{
          ?constructor?(imgELe)?{
          ???this.imgELe?=?imgELe;
          ????this.DEFAULT_URL?=?'xxx';
          ??}
          ??setUrl?(targetUrl)?{
          ???this.imgEle.src?=?this.DEFAULT_URL;
          ????const?image?=?new?Image();
          ????
          ????image.onload?=?()?=>?{
          ?????this.imgEle.src?=?targetUrl;
          ????}
          ????image.src?=?targetUrl;
          ??}
          }

          緩存代理常用于一些計(jì)算量較大的場景。當(dāng)計(jì)算的值已經(jīng)被出現(xiàn)過的時候,不需要進(jìn)行第二次重復(fù)計(jì)算。以傳參求和為例:

          const?countSum?=?(...arg)?=>?{
          ?console.log('count...');
          ??let?result?=?0;
          ??arg.forEach(v?=>?result?+=?v);
          ??return?result;
          }

          const?proxyCountSum?=?(()?=>?{
          ?const?cache?=?{};
          ??return?(...arg)?=>?{
          ???const?args?=?arg.join(',');
          ????if?(args?in?cache)?return?cache[args];
          ????return?cache[args]?=?countSum(...arg);
          ??};
          })()

          proxyCountSum(1,2,3,4);?//?count...??10
          proxyCountSum(1,2,3,4);?//?10

          小結(jié): 通過修改代理類來增加功能,符合開放封閉模式。

          使用場景: 圖片預(yù)加載、緩存服務(wù)器、處理跨域以及攔截器等。

          行為型

          策略模式

          介紹策略模式之前,簡單實(shí)現(xiàn)一個常見的促銷活動規(guī)則:

          • 預(yù)售活動,全場 9.5 折
          • 大促活動,全場 9 折
          • 返場優(yōu)惠,全場 8.5 折
          • 限時優(yōu)惠,全場 8 折

          人人喊打的 if-else

          const?activity?=?(type,?price)?=>?{
          ?if?(type?===?'pre')?{
          ???return?price?*?0.95;
          ??}?else?if?(type?===?'onSale')?{
          ???return?price?*?0.9;
          ??}?else?if?(type?===?'back')?{
          ???return?price?*?0.85;
          ??}?else?if?(type?===?'limit')?{
          ???return?price?*?0.8;
          ??}
          }

          以上代碼存在肉眼可見的問題:大量 if-else、可擴(kuò)展性差、違背開放封閉原則等。 我們再使用策略模式優(yōu)化:

          const?activity?=?new?Map([
          ?['pre',?(price)?=>?price?*?0.95],
          ??['onSale',?(price)?=>?price?*?0.9],
          ??['back',?(price)?=>?price?*?0.85],
          ??['limit',?(price)?=>?price?*?0.8]
          ]);

          const?getActivityPrice?=?(type,?price)?=>?activity.get(type)(price);

          //?新增新手活動
          activity.set('newcomer',?(price)?=>?price?*?0.7);

          小結(jié): 定義一系列算法,將其一一封裝起來,并且使它們可相互替換。符合開放封閉原則。

          使用場景: 表單驗(yàn)證、存在大量 if-else 場景、各種重構(gòu)等。

          觀察者模式

          觀察者模式又叫發(fā)布-訂閱模式,其用來定義對象之間的一對多依賴關(guān)系,以便當(dāng)一個對象更改狀態(tài)時,將通知其所有依賴關(guān)系。通過“別名”可以知道,觀察者模式具備兩個角色,即“發(fā)布者”和“訂閱者”。正如我們工作中的產(chǎn)品經(jīng)理就是一個“發(fā)布者”,而前后端、測試可以理解為“訂閱者”。以產(chǎn)品經(jīng)理建需求溝通群為例:

          //?定義發(fā)布者類
          class?Publisher?{
          ??constructor?()?{
          ????this.observers?=?[];
          ????this.prdState?=?null;
          ??}
          ??//?增加訂閱者
          ??add?(observer)?{
          ????this.observers.push(observer);
          ??}
          ??//?通知所有訂閱者
          ??notify?()?{
          ????this.observers.forEach((observer)?=>?{
          ??????observer.update(this);
          ????})
          ??}
          ??//?該方法用于獲取當(dāng)前的?prdState
          ??getState?()?{
          ????return?this.prdState;
          ??}

          ??//?該方法用于改變?prdState?的值
          ??setState?(state)?{
          ????//?prd?的值發(fā)生改變
          ????this.prdState?=?state;
          ????//?需求文檔變更,立刻通知所有開發(fā)者
          ????this.notify();
          ??}
          }

          //?定義訂閱者類
          class?Observer?{
          ??constructor?()?{
          ??this.prdState?=?{};
          ??}
          ??update?(publisher)?{
          ????//?更新需求文檔
          ????this.prdState?=?publisher.getState();
          ????//?調(diào)用工作函數(shù)
          ????this.work();
          ??}
          ??//?work?方法,一個專門搬磚的方法
          ??work?()?{
          ????//?獲取需求文檔
          ????const?prd?=?this.prdState;
          ????console.log(prd);
          ??}
          }

          //?創(chuàng)建訂閱者:前端開發(fā)小王
          const?wang?=?new?Observer();
          //?創(chuàng)建訂閱者:后端開發(fā)小張
          const?zhang?=?new?Observer();
          //?創(chuàng)建發(fā)布者:產(chǎn)品經(jīng)理小曾
          const?zeng?=?new?Publisher();
          //?需求文檔
          const?prd?=?{
          ??url:?'xxxxxxx'
          };
          //?小曾開始拉人入群
          zeng.add(wang);
          zeng.add(zhang);
          //?小曾發(fā)布需求文檔并通知所有人
          zeng.setState(prd);

          經(jīng)常使用 Event Bus(Vue) 和 Event Emitter(node)會發(fā)現(xiàn),發(fā)布-訂閱模式和觀察者模式還是存在著細(xì)微差別,即所有事件的發(fā)布/訂閱都不能由發(fā)布者和訂閱者“私下聯(lián)系”,需要委托事件中心處理。以 Vue Event Bus 為例:

          import?Vue?from?'vue';

          const?EventBus?=?new?Vue();
          Vue.prototype.$bus?=?EventBus;

          //?訂閱事件
          this.$bus.$on('testEvent',?func);
          //?發(fā)布/觸發(fā)事件
          this.$bus.$emit('testEvent',?params);

          整個過程都是 this.$bus 這個“事件中心”在處理。

          小結(jié): 為解耦而生,為事件而生,符合開放封閉原則。

          使用場景: 跨層級通信、事件綁定等。

          迭代器模式

          迭代器模式號稱“遍歷專家”,它提供一種方法順序訪問一個聚合對象中的各個元素,且不暴露該對象的內(nèi)部表示。迭代器又分內(nèi)部迭代器(jquery.each/for...of)和外部迭代器(es6 yield)。 在 es6 之前,直接通過 forEach 遍歷 DOM NodeList 和函數(shù)的 arguments 對象,都會直接報錯,其原因都是因?yàn)樗麄兌际穷悢?shù)組對象。對此 jquery 很好的兼容了這一點(diǎn)。 在 es6 中,它約定只要數(shù)據(jù)類型具備 Symbol.iterator 屬性,就可以被 for...of 循環(huán)和迭代器的 next 方法遍歷。

          (function?(a,?b,?c)?{
          ?const?arg?=?arguments;
          ??const?iterator?=?arg[Symbol.iterator]();
          ??
          ??console.log(iterator.next());?//?{value:?1,?done:?false}
          ??console.log(iterator.next());?//?{value:?2,?done:?false}
          ??console.log(iterator.next());?//?{value:?3,?done:?false}
          ??console.log(iterator.next());?//?{value:?undefined,?done:?true}
          })(1,?2,?3)

          通過 es6 內(nèi)置生成器 Generator 實(shí)現(xiàn)迭代器并沒什么難度,這里重點(diǎn)通 es5 實(shí)現(xiàn)迭代器:

          function?iteratorGenerator?(list)?{
          ??var?index?=?0;
          ??//?len?記錄傳入集合的長度
          ??var?len?=?list.length;
          ??return?{
          ????//?自定義?next?方法
          ????next:?funciton?()?{
          ??????//?如果索引還沒有超出集合長度,done?為?false
          ??????var?done?=?index?>=?len;
          ??????//?如果?done?為?false,則可以繼續(xù)取值
          ??????var?value?=?!done???list[index++]?:?undefined;

          ??????//?將當(dāng)前值與遍歷是否完畢(done)返回
          ??????return?{
          ????????done:?done,
          ????????value:?value
          ??????};
          ????}
          ??}
          }

          var?iterator?=?iteratorGenerator([1,?2,?3]);
          console.log(iterator.next());?//?{value:?1,?done:?false}
          console.log(iterator.next());?//?{value:?2,?done:?false}
          console.log(iterator.next());?//?{value:?3,?done:?false}
          console.log(iterator.next());?//?{value:?undefined,?done:?true}

          小結(jié): 實(shí)現(xiàn)統(tǒng)一遍歷接口,符合單一功能和開放封閉原則。

          使用場景: 有遍歷的地方就有迭代器。

          寫到最后

          設(shè)計(jì)模式的難,在于它的抽象和分散。抽象在于每一設(shè)計(jì)模式看例子都很好理解,真正使用起來卻不知所措;分散則是出現(xiàn)一個場景發(fā)現(xiàn)好幾種設(shè)計(jì)模式都能實(shí)現(xiàn)。而解決抽象的最好辦法就是動手實(shí)踐,在業(yè)務(wù)開發(fā)中探索使用它們的可能性。本文大致介紹了前端領(lǐng)域常見的 9 種設(shè)計(jì)模式,相信大家在理解的同時也不難發(fā)現(xiàn),設(shè)計(jì)模式始終圍繞著“封裝變化”來提供代碼的可讀性、擴(kuò)展性、易維護(hù)性。所以當(dāng)我們工作生活中,始終保持“封裝變化”的思想的時候,就已經(jīng)開始體會到設(shè)計(jì)模式精髓了。

          - END -


          分享前端好文,點(diǎn)亮?在看

          瀏覽 54
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  91久久综合 | 中文字幕有码在线播放 | 国产精品77777 | 大鸡巴操我骚逼 | 动漫摸无码视频 |