JS 設計模式之原型模式(建立型)

2022-09-14 06:03:07 字數 3645 閱讀 5871

原型模式不僅是一種設計模式,它還是一種程式設計正規化(programming paradigm),是 j**ascript 物件導向系統實現的根基。

在原型模式下,當我們想要建立乙個物件時,會先找到乙個物件作為原型,然後通過轉殖原型的方式來建立出乙個與原型一樣(共享一套資料/方法)的物件。在 j**ascript 裡,object.create 方法就是原型模式的天然實現——準確地說,只要我們還在借助 prototype 來實現物件的建立和原型的繼承,那麼我們就是在應用原型模式。

有的設計模式資料中會強調,原型模式就是拷貝出乙個新物件,認為在 j**ascript 類裡實現了深拷貝方法才算是應用了原型模式。這是非常典型的對 j**a/c++ 設計模式的生搬硬套,更是對 j**ascript 原型模式的一種誤解。事實上,在 j**a 中,確實存在原型模式相關的轉殖介面規範。但在 j**ascript 中,我們使用原型模式,並不是為了得到乙個副本,而是為了得到與建構函式(類)相對應的型別的例項、實現資料/方法的共享。轉殖是實現這個目的的方法,但轉殖本身並不是我們的目的。

j**ascript 沒有除了 prototype 以外應用原型模式的選擇 —— 畢竟原型模式是 j**ascript 這門語言物件導向系統的根本。但在其它語言,比如 j**a 中,類才是它物件導向系統的根本。所以說在 j**a 中,我們可以選擇不使用原型模式 —— 這樣一來,所有的例項都必須要從類中來,當我們希望建立兩個一模一樣的例項時,就只能這樣做(假設例項從 dog 類中來,必傳引數為姓名、性別、年齡和品種):

dog dog = new dog('旺財', 'male', 3, '柴犬')

dog dog_copy = new dog('旺財', 'male', 3, '柴犬')

這裡我們不得不把一模一樣的引數傳兩遍,非常麻煩。而原型模式允許我們通過呼叫轉殖方法的方式達到同樣的目的,比較方便,所以 j**a 專門針對原型模式設計了一套介面和方法,在必要的場景下會通過原型方法來應用原型模式。當然,在更多的情況下,j**a 仍以「例項化類」這種方式來建立物件。

雖然說 es6 支援類,但 es6 的類其實是原型繼承的語法糖,類語法不會為 j**ascript 引入新的物件導向的繼承模型。

當我們嘗試用 class 去定義乙個 dog 類時:

class dog 

eat()

}

其實完全等價於寫了這麼乙個建構函式:

function dog(name, age) 

dog.prototype.eat = function ()

所以說 j**ascript 這門語言的根本就是原型模式。在 j**a 等強型別語言中,原型模式的出現是為了實現型別之間的解耦。而 j**ascript 本身型別就比較模糊,不存在型別耦合的問題,所以說平時不會刻意地去使用原型模式。因此不必強行把原型模式當作一種設計模式去理解,把它作為一種程式設計正規化來討論會更合適。

原型程式設計正規化的核心思想就是利用例項來描述物件,用例項作為定義物件和繼承的基礎。在 j**ascript 中,原型程式設計正規化的體現就是基於原型鏈的繼承。這其中,對原型、原型鏈的理解是關鍵。

在 j**ascript 中,每個建構函式都擁有乙個 prototype 屬性,它指向建構函式的原型物件,這個原型物件中有乙個 construtor 屬性指回建構函式;每個例項都有乙個__proto__屬性,當我們使用建構函式去建立例項時,例項的__proto__屬性就會指向建構函式的原型物件。

具體來說,當我們這樣使用建構函式建立乙個物件時:

// 建立乙個 dog 建構函式

function dog(name, age)

dog.prototype.eat = function ()

// 使用 dog 建構函式建立 dog 例項

const dog = new dog('旺財', 3)

這段**裡的幾個實體之間就存在著這樣的關係:

現在在上面那段**的基礎上,進行兩個方法呼叫:

// 輸出"肉骨頭真好吃"

dog.eat()

// 輸出"[object object]"

dog.tostring()

明明沒有在 dog 例項裡手動定義 eat 方法和 tostring 方法,它們還是被成功地呼叫了。這是因為訪問乙個 j**ascript 例項的屬性/方法時,它首先搜尋這個例項本身;當發現例項沒有定義對應的屬性/方法時,它會轉而去搜尋例項的原型物件;如果原型物件中也搜尋不到,它就去搜尋原型物件的原型物件,這個搜尋的軌跡,就叫做原型鏈。

以上面的 eat 方法和 tostring 方法的呼叫過程為例,它的搜尋過程就是這樣子的:

上面這些彼此相連的 prototype,就組成了乙個原型鏈。 幾乎所有 j**ascript 中的物件都是位於原型鏈頂端的 object 的例項,除了object.prototype(當然,如果手動用 object.create(null) 建立乙個沒有任何原型的物件,那它也不是 object 的例項)。

「模擬 j**a 中的轉殖介面」、「j**ascript 實現原型模式」 其實就是 「實現 js 中的深拷貝」

實現 j**ascript 中的深拷貝,有一種非常取巧的方式 —— json.stringify:

const lilei = 

const lileistr = json.stringify(lilei)

const lileicopy = json.parse(lileistr)

lileicopy.habits.splice(0, 1)

console.log('李雷副本的 habits 陣列是', lileicopy.habits)

console.log('李雷的 habits 陣列是', lilei.habits)

進控制台檢驗,可以發現引用型別也被成功拷貝了,副本和本體相互不干擾~

但是這個方法存在一些侷限性,比如無法處理 function、無法處理正則等等——只有當你的物件是乙個嚴格的 json 物件時,可以順利使用這個方法。

深拷貝沒有完美方案,每一種方案都有它的邊界 case,多數情況下涉及到遞迴。遞迴實現深拷貝的核心思路:

function deepclone(obj) 

// 定義結果物件

let copy = {}

// 如果物件是陣列,則定義結果陣列

if (obj.constructor === array)

// 遍歷物件的 key

for (let key in obj)

} return copy

}

呼叫深拷貝方法,若屬性為值型別,則直接返回;若屬性為引用型別,則遞迴遍歷。這就是遞迴實現深拷貝的核心方法。

拓展閱讀:

jquery 中的 extend 方法原始碼 (opens new window)

建立型設計模式之原型模式

如題,今天我要總結的是建立型設計模式中的原型模式及單例模式,let s go 原型模式 正如我前面那篇建立型導論部落格總結的那樣,原型模式和所有的建立型設計模式一樣都需要建立乙個新的物件,我們通過使用這個新物件來完成一些對物件的操作。原型模式和其他建立型模式的不同點在於它並不需要通過專門new 就可...

建立型設計模式 原型模式

總結 羊 program ade someproblem author cade franklin create 2019 12 22 22 12 public class sheep tostring setter getter program ade someproblem author cad...

設計模式(建立型) 原型模式

原型模式的定義 以乙個已經建立的例項作為原型,通過複製該物件來建立乙個和原型相同或相似的新物件。1.複製在效能上比直接 new 乙個物件更加優良。2.可以使用深拷貝方式儲存物件的狀態,使用原型模式將物件複製乙份,並將其狀態儲存起來,可輔助實現撤銷操作。1.需要為每乙個類配備乙個轉殖方法,對已有的類進...