修煉內功之JavaScript設計模式(二)

觀感度:🌟🌟🌟🌟🌟javascript

口味:話梅排骨css

烹飪時間:20min


html

有人問我堅持寫博客的緣由是什麼?前端

借用著名小說家斯蒂芬·金的一句話:「開始寫吧!年輕人。」vue

當你把消費級興趣升級爲生產型興趣時,你纔會漸漸發現之前沒有窺見的門道和妙處。java

咳咳,天涼了請喝雞湯~node

可能本系列文章中所講的設計模式你在工做中常常應用它們,可是並不知道它們的名字。jquery

一旦咱們將這些設計模式整理學習並融會貫通後,即可以大大加強咱們的編程功底,在遇到實際業務需求時,給咱們提供更好的解決問題的思路。webpack

畢竟咱們的口號是:不加班!es6

書接上文,本文給你們介紹的是結構型設計模式,包括外觀模式適配器模式代理模式裝飾者模式橋接模式組合模式以及享元模式

外觀模式 Facade

概念:能夠對複雜的子系統接口提供一個更高級的統一接口,對底層結構兼容性作統一封裝來簡化用戶使用。

這種模式比較簡單也比較容易理解,在平常的開發中你必定遇到過如下的場景。

那些年咱們對ie瀏覽器作過的兼容。。

這些常見的兼容方案屬於外觀模式。

var getEvent = function (event) {
  return event || window.event;
}

var getTarget = function (event) {
  var event = getEvent(event);
  return event.target || event.srcElement;
}

var preventDefault = function (event) {
  var event = getEvent(event);
  if (event.preventDefault) {
    event.preventDefault();
  } else {
    event.returnValue = false;
  }
}

var stopBubble = function (event) {
  var event = getEvent(event);
  if (event.stopPropagation) {
    event.stopPropagation();     
  } else {
    event.cancelBubble = true;
  }
}
複製代碼

在團隊開發中,爲了不添加點擊事件時出現的重名覆蓋問題,咱們會使用DOM2級事件處理程序綁定事件,並添加對瀏覽器的兼容。

function addEvent (dom, type, fn) {
  if (dom.addEventListener) {
    dom.addEventListener (type, fn, false);
  } else if (dom.attachEvent) {
    // 兼容IE(低於9)
    dom.attachEvent('on' + type, fn);
  } else {
    dom['on' + type] = fn;
  }
}
複製代碼

你固然也能夠經過外觀模式,來封裝多個功能來簡化底層操做方法。

var T = {
  g : function (id) {
    return document.getElementById(id);
  },
  css : function (id, key, value) {
    document.getElementById(id).style[key] = value;
  },
  attr : function (id, key, value) {
    document.getElementById(id)[key] = value;
  },
  html : function (id, html) {
    document.getElementById(id).innerHTML = html;
  },
  on : function (id, type, fn) {
    document.getElementById(id)['on' + type] = fn;
  }
};

T.css('box', 'background', 'red');
T.attr('box', 'className', 'box');
T.html('box', '這是新添加的內容');
T.on('box', 'click', function() {
  T.css('box', 'width', '500px');
})
複製代碼

外觀模式對接口進行了包裝,咱們使用時無須知道接口實現的具體細節,按照規則使用便可。

適配器模式 Adapter

概念:將一個類(對象)的接口(方法或者屬性)轉化成另一個接口,以知足用戶需求,使類(對象)之間接口的不兼容問題經過適配器得以解決。

適配器在咱們的平常生活中很常見,好比出國旅行時,有的國家只有三項的插座,這時候咱們須要三項轉兩項插頭電源適配器。再好比iPhone7之後耳機接口變成了lightning接口,爲了適配圓孔耳機蘋果爲咱們提供了適配器。

// 爲兩個代碼庫所寫的代碼兼容運行而書寫的額外代碼是適配器的一種。
window.A = A = jQuery;
複製代碼

jQuery中的適配器

jQuery.fn.css()
jQuery核心cssHook
// (爲了控制文數,此處不貼代碼,閱讀完後你們可自行去官網查看)
複製代碼

jQuery源碼地址

參數適配器

function doSomeThing(name, title, age, color, size, prize) {}
// 咱們記住這些參數的順序是很困難的,因此咱們常常是以一個參數對象方式來傳入
/** * obj.name : name * obj.title : title * obj.age : age * obj.color : color * obj.size : size * obj.prize : prize **/
// 當調用它的時候咱們要考慮到傳遞的參數是否完整的問題,若是有一些必須參數沒有傳入
// 一些參數是有默認值的等等。這時咱們能夠用適配器來適配傳入的參數對象
function doSomeThing (obj) {
  var _adapter = {
    name : '前端食堂',
    title : '設計模式',
    age : 24,
    color : 'blue',
    size : 100,
    prize : 99
  };
  for (var i in _adapter) {
    _adapter[i] = obj[i] || _adapter[i];
  }
  // 或者extend(_adapter, obj) 注: 此時可能會多添加屬性
  // do things
}
複製代碼

不少插件對於參數配置都是這麼作的。

數據適配

var arr = ['前端食堂', 'restaurant', '記得按時吃飯', '9月30日'];

// 咱們發現數組中每一個成員表明的意義不一樣,因此這種數據結構 語義很差,咱們將其適配成對象。

function arrToObjAdapter (arr) {
  return {
    name : arr[0],
    type : arr[1],
    title : arr[2],
    data : arr[3]
  }
  var adapterData = arrToObjAdapter(arr);
  console.log(adapterData);  
  // {name:"前端食堂", type:"restaurant", title:"記得按時吃飯",data:"9月30日"} 
}
複製代碼

服務端數據適配

如今主流的方案是用node.js寫中間層-BFF層(Backend for Frontend),獲取到後臺返回的數據後進行處理, 處理爲前端想要的數據,這樣也不失爲一種適配器模式。

與外觀模式的區別:

相比於外觀模式,適配器模式要了解適配對象的內部結構。

代理模式 Proxy

概念:因爲一個對象不能直接引用另外一個對象,因此須要經過代理對象在這兩個對象之間起到中介的做用。

現實生活中好比咱們租房子時回去自如,自如是房屋中介機構,也就是咱們的代理。

javaScript中使用最多的就是虛擬代理和緩存代理。

虛擬代理

圖片loading預加載

// 建立本體對象,生成img標籤,對外提供setSrc接口
var myImage = (function() {
  var imgNode = document.createElement('img');
  document.body.appendChild(imgNode);

  return {
    setSrc: function (src) {
      imgNode.src = src;
    }
  }
})();

 // 引入代理對象,圖片加載以前會有個loading.gif
 var proxyImage = (function () {
   var img = new Image();
   img.onload = function () {
     myImage.setSrc(this.src);
   }

   return {
     setSrc: function (src) {
       myImage.setSrc('loading.gif');
       img.src= src;
     }
   }
 })();
 // 經過代理獲得圖片
 proxyImg.setSrc('http://...');
複製代碼

緩存代理

能夠爲一些開銷很大的運算結果提供暫時的儲存,若下次傳遞的參數一致則直接返回以前的結果,大大提升效率和節省開銷。

var computedFn = function () {
  console.log('開始');
  var a = 1;
  for (var i = 0; l = arguments.length; i < l; i++) {
    a = a * arguments[i];
  }
  return a;
}

var proxyComputed = (function() {
  var cache = {};
  return function() {
    var args = Array.prototype.join.call(arguments, ',');
    if (args in cache) {
      return cache[args];
    }
    return cache[args] = computedFn.apply(this, arguments);
  }
})();

proxyComputed(1, 2, 3, 4); // 24
proxyComputed(1, 2, 3, 4); // 24
複製代碼

如上所示,代理模式能夠解決系統之間的耦合度以及系統資源開銷大的問題,

ES6中的Proxy

ES6所提供的Proxy構造函數可以讓咱們輕鬆的使用代理模式。

// target所要代理的對象
// handler設置對所代理的對象的行爲
var proxy = new Proxy(target, handler);
複製代碼

ES6中的Proxy

Vue3.0中的Proxy

vue3.0中的雙向數據綁定原理用了es6中的Proxy,並優雅的解決了Proxy細節上的一些問題,從而完美的實現雙向綁定,你們能夠去閱讀源碼,或是社區中的這篇文章,這裏不做展開。

裝飾者模式 Decorator

概念:在不改變原對象的基礎上,經過對其進行包裝拓展(添加屬性或者方法)使原有對象能夠知足用戶的更復雜需求。

TypeScript的裝飾器@

裝飾器是一項實驗性特性,在將來的版本中可能會發生改變。

具體使用方法請移步官方文檔

假設咱們在開發中有這樣一個需求,在不影響原有事件的前提下,新增事件實現業務,就能夠經過裝飾者來實現。

// 裝飾者
var decorator = function (input, fn) {
  // 獲取事件源
  var input = document.getElementById(input);
  // 若事件源已經綁定事件
  if (typeof input.onclick === 'function') {
    // 緩存事件源原有回調函數
    var oldClickFn = input.onclick;
    // 爲事件源定義新的事件
    input.onclick = function () {
      // 事件源原有回調函數
      oldClickFn();
      // 執行事件源新增回調函數
      fn();
    }
  } else {
    // 事件源未綁定事件,直接爲事件源添加新增回調函數
    input.onclick = fn;
  }
}
複製代碼

與適配者模式的區別:

在適配者模式中要了解原有方法實現的具體細節,而在裝飾者模式只有當咱們調用方法時纔會知道其內部細節,這是對原有功能完整性的一種保護。

橋接模式 Bridge

概念:在系統沿着多個維度變化的同時,又不增長其複雜度並已達到解耦。

var spans = document.getElementsByTagName('span');
// 爲用戶名綁定特效
spans[0].onmouseover = function () {
  this.style.color = 'red';
  this.style.background = '#ddd';
}
spans[0].onmouseout = function () {
  this.style.color = '#333';
  this.style.background = '#f5f5f5';
}
// 爲等級綁定特效
spans[1].onmouseover = function () {
  this.getELementsByTagName('strong')[0].style.color = 'red';
  this.getElementsByTagName('strong')[0].style.background = '#ddd';
}
spans[1].onmouseout = function () {
  this.getELementsByTagName('strong')[0].style.color = '#333';
  this.getElementsByTagName('strong')[0].style.background = '#f5f5f5';
}
複製代碼
// 提取共同點,解除與事件中的this的耦合
function changeColor (dom, color, bg) {
  dom.style.color = color;
  dom.style.background = bg;
}

// 使用匿名函數,事件與業務邏輯之間的橋樑
var spans = document.getElementsByTagName('span');
spans[0].onmouseover = function () {
  changeColor(this, 'red', '#ddd';)
}

// changeColor方法中的dom實質上是事件回調函數中的this,解除它們之間的耦合
// 咱們使用了一個橋接方法-匿名回調函數。經過這個匿名回調函數
// 咱們將獲取到的this傳遞到changeColor函數中,便可實現需求
spans[0].onmouseout = function () {
  changeColor(this, '#333', '#f5f5f5');
}
// 一樣的道理應用在用戶等級上
spans[1].onmouseover = function () {
  changeColor(this.getElementsByTagName('strong')[0], 'red', '#ddd');
}
spans[1].onmouseout = function () {
  changeColor(this.getElementsByTagName('strong')[0], '#333', '#f5f5f5');
}
// 若是需求再有變化,只須要修改changeColor的內容就能夠了
// 而沒必要去到每一個事件回調函數中去修改,以新增一個橋接函數爲代價
複製代碼

將實現層(如元素綁定的事件)與抽象層(如修飾頁面UI邏輯)解耦分離,使兩部分能夠獨立變化。

注意,有些時候,對於橋樑的添加,會形成開發成本增長,性能上也會受影響。

組合模式 Composite

概念:又稱部分-總體模式,將對象組合成樹形結構以表示「部分總體」的層次結構,組合模式使得用戶對單個對象和組合對象的使用具備一致性。

中餐廳:套餐服務

webpack構建項目的目錄結構

公司部門組織架構

組合模式可以給咱們提供一個清晰的組成結構。組合對象類經過繼承同一個父類使其具備統一的方法,這樣也方便了咱們統一管理和使用。

jQuery中addClass實現

// jQuery 3.4.1
addClass: function( value ) {
	var classes, elem, cur, curValue, clazz, j, finalValue,
	i = 0;

	if ( isFunction( value ) ) {
		return this.each( function( j ) {
			jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
		} );
	}

	classes = classesToArray( value );

	if ( classes.length ) {
		while ( ( elem = this[ i++ ] ) ) {
			curValue = getClass( elem );
			cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );

			if ( cur ) {
				j = 0;
				while ( ( clazz = classes[ j++ ] ) ) {
					if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
							cur += clazz + " ";
					}
				}

				// Only assign if different to avoid unneeded rendering.
				finalValue = stripAndCollapse( cur );
				if ( curValue !== finalValue ) {
					elem.setAttribute( "class", finalValue );
				}
			}
		}
	}

	return this;
}
複製代碼

咱們無須知道addClass的內部結構和實現細節,就可使用addClass來對標籤添加類。

享元模式 Flyweight

概念:運用共享技術有效地支持大量的細粒度的對象,避免對象間擁有相同內容形成多餘的開銷。

核心思想:共享細粒度對象

最終目標:儘可能減小共享對象的數量

現實生活中,LOL、農藥段位之分是享元模式的思想,咖啡廳的咖啡種類也是享元模式的思想。

在咱們熟知的原型鏈繼承中,當子類實例不少的時候,子類能夠經過原型來複用父類的方法和屬性來優化內存,這也是享元模式的思想。

除此以外,Node.js中的線程池、數據庫的鏈接池、HTTP鏈接池以及字符常量池都是享元模式或其升級版。

參考:

《JavaScript設計模式》張容銘


交流

歡迎關注個人我的公衆號,文章將同步發送,後臺回覆【福利】便可免費領取海量學習資料。

你的前端食堂,記得按時吃飯。

相關文章
相關標籤/搜索