簡介
設計模式的定義是:在物件導向軟體設計過程中針對特定問題的優雅而簡潔的解決方案。
就好比在足球比賽中我們把“邊後衛快速突破後向球門方向傳出高球,中路隊員接應頭球攻門”這種戰術稱作“下底傳中”一樣是針對一些常見問題設計的解決方案。
相信大家在程式開發中都會碰到類似的情況:感覺自己的程式碼寫的不優雅,但不知道從如何下手去優化,雖然暫時解決了問題,但是卻留下了很大的風險,一旦遇上了一些詭異的bug或者新的需求變動,就會發現自己都已經理不清程式碼邏輯了根本無從下手。通過學習設計模式可以幫助我們開拓思路,用更好的結構調整程式碼。
在學習設計模式之前有幾個基礎概念需要了解:
- this指向
- 閉包
- 高階函式
- 原型,原型鏈
(關於這幾個知識點網上已近有很多教程提及了。以下是我個人的一些總結希望用簡短的語言幫助你更好的理解他們)
基礎知識
- this指向
總結:“函式中的this基本都指向函式的呼叫者”
// 示例一
function demo1() {
console.log(this)
}
// 示例二
var demo2 = {
log() {
console.log(this)
}
}
// 示例三
var demo3= {
log() {
setTimeout(function print(){ // 給一個函式名方便理解
console.log(this)
},1000)
}
}
demo1() // window;
demo2.log() // {log: f};
demo3.log() // window;
複製程式碼
- demo1(): 可以理解成window.demo1() 所以呼叫者是window, this =window
- demo2.log(): log方法的呼叫者是demo2 所以this=demo2
- demo3.log(): 本示例中print方法作為引數傳給了setTimeout方法, 所以在呼叫demo3.log()時並未執行該方法,而是在1s的延時之後執行了print這是呼叫者又變成了window 所以this=window
既然有一般情況,那肯定也存在幾種特殊的情況:
// 第一種:new (作為建構函式呼叫)
function Demo() {
this.a = 1;
console.log(this)
}
new Demo() // {a: 1}
// 第二種:apply、call
function log() {
console.log(this, arguments)
}
var target = {a: 1}
log.apply(target, [1,2,3]) // {a:1} [1,2,3]
log.call(target, 1, 2, 3) // {a:1} [1,2,3]
// 第三種:bind、箭頭函式
log.bind(target)
log(1, 2, 3) // {a:1} [1,2,3]
// 使用之前的demo3做示例
var demo3= {
log() {
setTimeout((function print() {
console.log(this)
}).bind(this), 1000)
// 等同於 setTimeout(() => console.log(this), 1000)
}
}
demo3.log() // {log: f}
複製程式碼
-
使用new關鍵字呼叫函式時會先宣告一個新的物件然後將函式中的this指這個新的物件,所以例子中可以理解成先執行了this ={}然後再執行之後的內容
-
apply、call是兩個常用的改變函式this指向的方法,他們的第一個引數是替換this的物件可以理解成先執行了this =target,區別在於第二個引數。apply的第二個引數是一個陣列這個陣列會拆一個個引數傳入函式([1,2,3]會變成log(1,2,3)),而call會將第二個及之後的引數作為函式的引數傳入函式
-
bind和箭頭函式其實是一種方式(箭頭函式其實是一種語法糖簡化了bind寫法), 由於現在箭頭函式用的比較多所以放在一起說下。 bind方法的作用是繫結函式的作用域但是不會立即執行這個函式,這也更符我們的使用場景。我們改寫了demo3的實現,在宣告print方法的同時使用bin方法繫結了this。當我們呼叫demo3.log時,這時this指向呼叫者demo3,所bind中的this就是demo3,即使延時了1s執行this指向也不會因為呼叫者的改而改變
-
閉包
總結:“函式的內部變數被其內部函式暴露給外部物件使用”,在本質上,包就是將函式內部和函式外部連線起來的一座橋樑
作用:1. 持久儲存變數 2. 隔離作用域
// 示例一
function home() {
var computer = 'pc'
console.log(computer, 'at home')
return function online() {
console.log(computer, 'online')
return computer
}
}
var online = home() // pc at home
var computer = online() // pc online
console.log(computer) // pc
// 示例二
var funs = []
var i=0 // 這樣寫便於理解i是宣告在window下的
for(; i<5; i++) {
funs.push(function() {
console.log(i)
})
}
funs[0]() // 5
funs[1]() // 5
funs[2]() // 5
複製程式碼
- 通過示例一我們來舉個?:我家有一臺電腦,通常情況下我都在家裡用電,但有時候我不在家也想用電腦怎麼辦。我可以在第一次使用時把電腦連上網記下ip,以後出門在外也可以遠端訪問到這臺電腦。 這個例子中computer變數是home函式的內部變數,它被內部函式online引用當執行home函式時返回了online函式,繼續呼叫online函式我們就在外部得到了home的內部變數computer。
- 示例二展示了一個我們工作中常見的場景,我們預期的值應該是0,1,2..實際結果都輸出了最後一次迴圈的值。原因是這裡的i我們是在window下宣告可以,當我們呼叫funs陣列中的方法時,迴圈已經結束i的值已經變成了5。們可以用閉包來解決這個問題:
var funs = []
var i=0
for(; i<5; i++) {
(function parent(i){
funs.push(function child() {
console.log(i)
})
})(i)
}
funs[0]() // 0
funs[1]() // 1
funs[2]() // 2
複製程式碼
我們宣告瞭一個parent函式並立即呼叫了它,把i當做引數傳入作為parent的部變數,並在child函式中呼叫i形成了一個閉包。
更詳細的內容可以參考阮一峰老師寫的這篇文章
- 高階函式
總結:“將函式作為引數或者將函式作為返回值的函式”
高階函式的概念比較好理解,就是一類函式的代稱, 來看個簡單的例子
function delay (cb, time) {
return function() {
setTimeout(cb, time)
}
}
複製程式碼
- 原型,原型鏈
關於原型,原型鏈的知識點比較多如果是第一次接觸可以先參考[這篇文章]www.cnblogs.com/onepixel/p/…) 總結:“每個物件都有一個隱藏屬性__proto__指向他的建構函式(constructr)的原型物件(prototype),這種鏈式的關係稱作原型鏈”
function Person(){}
console.log(Person.prototype);
// Object {constructor:function Person(),__proto__:Object}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){
console.log(this.name);
};
var person = new Person();
person.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
console.log(person1.sayName == person2.sayName); //true
console.log(person1.__proto__); //{name,age,job,sayname}
複製程式碼
首先我們先來了解下上文提到的三個單詞
prototype:
當建立一個函式時,會根據一組特定的規則為該函式建立一個名為prototype的原型物件,這個原型物件包含一個constructor屬性。
constructor:
原型物件都預設會有一個constructor(建構函式)屬性,這個屬性包含一個向 prototype 屬性所在函式的指標。就拿前面的例子來說, Person.prototype.constructor 指向 Person 。
__proto__:
js中所有物件都預設包含一個指標[[Prototype]] (內部屬性),指向建構函式的原型物件。雖然在js中沒有標準的方式訪問 [[Prototype]],但 Firefox、Safari 和 Chrome在每個物件上都支援一個屬性__proto__。在上例中person1.__proto__指向建構函式的原型即Person.prototype,而Person.prototype.__proto__又指向了Object(原型物件是通過new Object建立的),這樣的鏈式結構稱之為原型鏈。
我們可以看一張圖來幫助理解
每當程式碼讀取某個物件的某個屬性時,都會執行一次搜尋,目標是具有給定字的屬性。搜尋首先從物件例項本身開始。如果在例項中找到了具有給定名的屬性,則返回該屬性的值;如果沒有找到,則繼續順著原型鏈往上找。
如在原型物件中找到了這個屬性,則返回該屬性的值。也就是說,在我們呼叫person1.sayName() 的時候,會先後執行兩次搜尋。首先,解析器會問:“例項 person1 有 sayName 屬性嗎?”答: “沒有。”然後,它繼續搜尋,再問: “ person1 的原型有 sayName 屬性嗎?”答:“有。 ”於是,它就讀取那個儲存在原型物件中的函式。當我們呼叫person2.sayName()時,將會重現相同的搜尋過程,得到相同的結果。而這正是多個物件例項共原型所儲存的屬性和方法的基本原理。
原型模式
簡介:原型模式是一種用於建立物件的模式,不同於用類建立,原型模式使用克隆物件的方式來建立一個新的物件。JavaScript本身就是一門基於原型的面嚮物件語言,它的物件系統就是使用原型模式搭建的。
原型模式的實現關鍵是語言本省是否提供了clone方法,ES5中提供了Object.create這個方法來clone物件
例:
var AM = function() {
this.HP = 1000
this.ATK = 100
this.DEF = 100
}
var am = new AM()
var amClone = Object.create(am)
console.log(amClone.HP) // 1000
console.log(amClone.ATK) // 100
console.log(amClone.DEF) // 100
複製程式碼
上述?中,我們通過Object.create方法來clone了一個“一模一樣”的物件,接下來我們通過自己實現一個create函式來理解js中是怎麼實現clone的
(結合之前提到的原型和原型鏈大家可以嘗試自己先實現一下這個create方法)
------------- 先思考,勿偷看 ---------------
------------- Think first ---------------
function create(obj) {
var F = function() {} // 宣告一個空方法
F.prototype = obj // 將該方法的原型物件設定為需要克隆的物件
return new F() // 返回這個物件的例項
}
var am = new AM()
var amClone = create(am)
console.log(amClone.HP) // 1000
console.log(amClone.ATK) // 100
console.log(amClone.DEF) // 100
複製程式碼
最後補充一句,JavaScript中除了undefined之外,一切都應該是物件(null是個特例感興趣的同學可以看看這裡)。Object.prototype是所有物件的根物件,它是一個空物件,所有其他物件都是從他克隆來的。
結語
本系列文章主要是我對《javascript設計模式與開發實踐》一書的學習總結,計劃分為上中下三章介紹書中提及的16種設計模式,希望幫助大家提升自己的程式碼質量的同時也能幫我們更好的和同事溝通(裝x)。
系列連結
- 16種JavaScript設計模式(上)
- 16種JavaScript設計模式(中)
- 16種JavaScript設計模式(下)還在計劃中。
本文主要參考了《javascript設計模式與開發實踐》一書