淺談設計模式
一、JS設計模式
- 單例模式
- 工廠模式
- 建構函式模式
- 原型模式
- 建構函式 + 原型模式
- 觀察者模式與訂閱釋出模式
- 策略模式
- 代理模式
二、什麼是設計模式?
設計模式(Design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。
使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。 毫無疑問,設計模式於己於他人於系統都是多贏的;設計模式使程式碼編制真正工程化;設計模式是軟體工程的基石脈絡,如同大廈的結構一樣。
三、各設計模式簡述
1.單例模式
(用不同的名稱空間,把相同的屬性和方法分開) (單體模式/單子模式) ===> 常用來封裝, 公用的方法
- 1.最簡單的物件字面量 (存放資訊 )
let person1 = { name: 'vue', age: 23 }; let person2 = { name: 'react', age: 25 }; 複製程式碼
- 2.單例模式 + 閉包, 高階單例模式
let tools = (function(){ let x = 1; functoin Tabs() { // => 生成單例的建構函式的程式碼 } function fn() { } //return new Tabs(); return { Tabs, fn } })(); 複製程式碼
- 3封裝
第一種: let File = { delete() { return this; //==> 鏈式呼叫 }, update() { return this; }, addFile() { return this; } }; File.delete().upate().addFle(); 第二種: tools工具類的封裝 let tools = (function(){ let o = {}; o.toastOk = function() {} })();
2.工廠模式
工廠模式是軟體工程領域一種廣為人知的設計模式,而由於在ECMAScript5中無法建立類,因此用函式封裝以特定介面建立物件。其實現方法非常簡單,也就是在函式內建立一個物件,給物件賦予屬性及方法再將物件返回即可。
function creatper(name,age){
var per=new Object(); //原料
//加工
per.name=name;
per.age=age;
per.sayHi=function(){
console.log(per.name+''+per.age); }
return per; //出廠
}
var me=creatper('katherine',22);
me.sayHi();
console.log(me.name);
但是你會發現工廠模式是無法識別物件的型別,因為全都是object,於是建構函式模式出現了。
3.建構函式模式
ECMAScript中建構函式可以用來建立特定物件,類似於Array,Date等原生的js物件
function Student(name,age,classa){
this.name=name;
this.age=age;
this.classa=classa;
this.sayHello=function(){
console.log(this.name,this.age,this.classa);
}
}
var me=new Student("xiaoai",22,"大三");
console.log(me.classa);
me.sayHello();
console.log(me instanceof Student);//true
由程式碼可以看出,於工廠模式除了函式名不同以外,還要注意:建構函式名的首字母大寫(不過好像沒有嚴格規定)。建構函式也沒有顯示建立的物件,使用了this,直接把屬性和方法賦值給了this物件。沒有return語句,例項化的時候要使用new,而且它能夠識別物件(這正是建構函式模式勝於工廠模式的地方)。
建構函式雖然好用,但也有很大的缺點,就是每次建立例項的時候都要重新建立一次方法,實際應用中,每次建立物件的時候屬性值不同,而物件的方法卻是相同的,所以建立兩次完全相同的方法是沒有必要的
4.原型模式
js規定每一個建立的函式都有prototype(原型)屬性,這個屬性是指標,指向一個物件,而這個物件的用途是包含由特定型別的所有例項所共享的屬性和方法,使用原型物件就可以讓所有例項物件均包含這些屬性及方法。
function per(){}
per.prototype.name='xiaoai';
per.prototype.age=22;
per.prototype.course=['php','javascript','java','C#'];
per.prototype.say=function(){
console.log(this.name+this.age+this.course);
}
var per1=new per();
var per2=new per();
per1.name='katherine';
per1.course.push('Html5');
per1.say();
per2.say();
per2.course.pop();
關於原型模式的缺點,我想也是很明顯的,它省略了建構函式傳遞初始化引數這一環節,結果所有例項均在預設情況下取得了相同的屬性值,雖然你可以在後來做出更改,但一點都不方便,這樣還不是最大的問題,原型模式最大的問題是在於共享的本性所導致的,由於共享,因此一個例項修改了引用,另一個也隨之更改了屬性。因此一般不會單獨使用原型模式。
5.建構函式 + 原型模式
function per(name,age,course){
this.name=name;
this.age=age;
this.course=course;
}
per.prototype.say=function(){
console.log(this.name+this.age+this.course);
}
var per1=new per('katherine',22,['C#','java','PHP','Javascript']);
var per2=new per ('xiaoai',21,['oracle','mysql','nodejs','html5']);
per1.say();
per2.say();
per1.course.pop();
per1.say();
per2.say();
由程式碼可以看出混合模式的分工:建構函式用於定義例項的屬性,而原型模式用於定義方法和一些共享的屬性。每個例項都會有自己的屬性,但同時又共享著方法,最大限度的節省了記憶體。另外這種模式還支援傳遞初始引數。使用最廣泛。
6.觀察者模式與訂閱釋出模式
觀察者模式是軟體設計模式的一種。在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。此種模式通常被用來實時事件處理系統。
釋出/訂閱模式(Pub/Sub)是一種訊息模式,它有 兩個參與者 : 釋出者和訂閱者 。釋出者向 某個通道釋出一條訊息,訂閱者繫結這個通道,當有訊息釋出至通道時就會 接收到一個通知。最重要的一點是, 釋出者和訂閱者是完全解耦的,彼此並不知曉對方 的存在。兩者僅僅共享一個通道名稱。
從定義上可以看出,釋出訂閱模式裡雙方是完全解耦的,而在觀察者模式裡,目標物件管理這觀察者,雙方是耦合的,這是最主要的區別,而在釋出訂閱模式中多了一箇中間層通道。所以它們兩者並不相同,訂閱釋出模式是對觀察者模式的進一步解耦。vue雙向繫結的實現就使用了訂閱釋出模式。
我們從簡單的程式碼開始,以Dog物件的fire方法來呈現兩者的不同:
觀察者模式實現
function Dog(){
this.handles = {};//儲存所有觀察者方法的引用
}
Dog.prototype.addHandle = function(type,func){
if(!(this.handles[type] instanceof Array)){
this.handles[type] = [];
}
this.handles[type].push(func);
}
Dog.prototype.fire = function(type){
var typeFunc = this.handles[type];
var len = typeFunc.length;
for(var i = ;i < len; i++){
typeFunc[i](event);
}
}
Dog.prototype.removeHandle = function(type,func){
var typeFunc = this.handles[type];
if(!(typeFunc instanceof Array)){
return;
}
var len = typeFunc.length;
for(var i = ;i < len; i++){
if(typeFunc[i] == func){
typeFunc.splice(i,1);
}
}
}
釋出訂閱模式實現
function rockets(){//通道
this.subers = {};
}
rockets.prototype.addSub = function(type,func){
if(!(subers[type] instanceof Array)){
subers[type] = [];
}
subers[type].push(func);
}
rockets.prototype.removeSub = function(type,func){
var typeFunc = subers[type];
if(!(typeFunc instanceof Array)){
return;
}
var len = typeFunc.length;
for(var i = ;i < len; i++){
if(typeFunc[i] == func){
typeFunc.splice(i,1);
}
}
}
rockets.prototype.fire = function(type){
var typeFunc = subers[type];
var len = typeFunc.length;
for(var i = ;i < len; i++){
typeFunc[i](event);
}
}
function Dog(){
this.rockets = new rockets();
}
Dog.prototype.fire = function(type){
this.rockets.fire(type);
}
從中可以看出釋出訂閱模式相對觀察者模式多了一箇中間層從而把釋出和訂閱分開,如果從這點程式碼量看來好像釋出訂閱完全多此一舉,但是在大型專案中的團隊合作來說,完全解耦是十分有必要的。
7.策略模式
定義一系列方法,封裝起來使他們可以相互替換。也就是將策略封裝在策略類中,當發起請求時管理類將請求委託給對應策略類。概括而言,就是講對應不同情況的策略(即不同的處理方法)專門分裝在一個物件中,即策略物件裡面包含所有的策略。事件請求不直接請求該策略物件,而是有個管理物件來處理請求轉發給策略物件的不同介面,這裡其實也可以認為涉及到代理模式的概念, 主要目的是為了避免太多的邏輯判斷語句的出現,解耦業務邏輯和演算法策略。
使用場景:適用於業務邏輯中重複比較多依賴引數或者標識來區分不同的處理的場景。例如react中一個新建某個訂單的頁面中,存在很多的渲染input等。
/**
2 * 針對不同情況的策略演算法封裝在策略類fucs中,
3 * 從呼叫事件中去除繁瑣的if或者switch邏輯判斷。達到解耦的目的
4 * 加入後面再增加‘select’的選項增加對應的方法即可
5 */
6 var funcs = {
7 text:function(){
8 console.log('this is method for text')
9 },
10 radio:function(){
11 console.log('this is method for radio')
12 },
13 checkbox:function(){
14 console.log('this is method for checkbox')
15 },
16 default:function(){
17 console.log('this is method for default')
18 }
19 }
20
21 var renderDom = function(type){
22 /**
23 * 只需要根據不同的入參,自行匹配策略類中的介面即可。
24 */
25 return (funcs[type] || funcs['default'])()
26 }
27 renderDom('checkbox')
8.代理模式
代理模式是一種非常有意義的模式,在生活中可以找到很多代理模式的場景。比如,明星都有經紀人作為代理。如果想請明星來辦一場商業演出,只能聯絡他的經紀人。經紀人會把商業演出的細節和報酬都談好之後,再把合同交給明星籤。
代理模式的關鍵是,當客戶不方便直接訪問一個物件或者不滿足需要的時候,提供一個替身物件來控制對這個物件的訪問,客戶實際上訪問的是替身物件。替身物件對請求做出一些處理之後,再把請求轉交給本體物件。
下面用一個故事、一段程式碼簡單演示代理模式 原本:追慕者小明-->送花-->心上人A 代理:追慕者小明-->送花-->跟A相熟的代理B-->心上人A
// 代理模式 好處:
// 1、保護代理:代理B可以幫A過濾掉一些請求
// 2、虛擬代理:把一些開銷大的物件,延遲到真正需要它時才建立
class Flower {
constructor (name) {
this.name = name
}
}
// 追慕者小明
let xiaoming = {
name: '小明',
sendFlower (target) {
target.receiveFlower(this.name)
}
}
// 代理B
let B = {
receiveFlower (fromWho) {
// 當然要等A好心情時才送花,也在送花也才建立花
A.listenGoodMood(() => {
A.receiveFlower(new Flower(fromWho + '的花'))
})
}
}
// 心上人A
let A = {
receiveFlower (flower) {
console.log('收到花:' + flower.name)
},
listenGoodMood (fn) {
setTimeout(() => {fn()}, 1000)
}
}
xiaoming.sendFlower(B)
執行結果:收到花:小明的花