iT邦幫忙

2024 iThome 鐵人賽

DAY 6
0

https://ithelp.ithome.com.tw/upload/images/20240920/20168201fImniyOlhB.png

今天要介紹的是 Prototype 模式,這是 GoF 提出的模式之一。

情境

在軟體開發中,有些物件具有高相似度,或是使用的方法、功能類似,開發者需要一種方式來避免建立重複的方法,提高創建物件的彈性。

問題

如何快速且靈活的建立一個物件,配置已有物件的屬性和方法,而不是從頭開始建立一個全新物件?

權衡

  • 需避免與建立物件有關的成本或效能負擔
    • 在某些情況下,建立物件不僅是簡單的實例化,可能還涉及複雜的配置過程、資源分配,或是初始化步驟繁雜,這些過程都需花費時間,成本較高,舉例來說,如果建立一個物件需要先從遠端服務載入資料,或需要繁瑣運算來建立初始狀態,這些都會讓創建新實例的成本和效能消耗變高。以 JavaScript 來說明,當我們使用標準建構方法,如使用 new 關鍵字來建立新物件時,在建構過程中可能有複雜計算,而帶來額外效能負擔,尤其是若需要創建大量類似物件時,頻繁的 new 建構物件會增加運算成本
  • 建立物件時,可能包含特定狀態的初始化,這些可能是基於先前存在的物件狀態
    • 有時候,新建立的物件屬性可能與已存在物件有類似或相同的初始值,或是相同的方法,此時若能從一個已設定好的物件模板開始,再依據需求調整,會比重新、從頭開始建立並配置一個全新物件更節省效能成本

解決方案

Prototype 模式可使用 JavaScript 原生提供的 prototype 優勢,因 JavaScript 中的 prototype(原型)是原生就有的物件,我們可用原型來讓同類型物件間共享屬性和方法。
可用兩種方式來撰寫 JavaScript 的原型繼承,第一種是 ES6 推出的 class 建構子,小提醒,class 只是建構函式的語法糖,JavaScript 中沒有 class 的概念,在 JavaScript 寫 class 本質上還是在用傳統的建構函式(function constructor)來建立實例,還不清楚 class 和建構函式的可參考 PJ 大大的筆記

ES6 class

假設在前端應用中,我們想設計一個圖形編輯器,會有正方形、圓形等圖形,他們會有類似的屬性和行為,我們可用 class 來定義圖形的原型。

// 定義 Shape 建構子來讓不同圖形共用顏色屬性和繪製方法(這裡先假設繪製行為是相同的)
class Shape {
    constructor(color) {
        this.color = color;
    }
    
    draw() {
        console.log(`Drawing a ${this.color} shape`);
    }
}

接下來我們可依據這個形狀的基本屬性和方法,延伸定義矩形和圓形建構子。

// 矩形 class
class Rectangle extends Shape {
    constructor(width, height, color) {
        super(color); // 用 super 繼承 Shape 的屬性
        this.width = width;
        this.height = height;
    }

    draw() {
        super.draw(); // 執行 Shape 的 draw 方法
        console.log(`This is a rectangle with width ${this.width} and height ${this.height}`); // 接著再執行新定義的邏輯,這樣可同時執行共用方法內的邏輯,又可擴展矩形自己想執行的行為
    }
    
    getArea() { // 定義矩形自己的方法
        return this.width * this.height; 
    }
}

// 圓形 class
class Circle extends Shape {
    constructor(radius, color) {
        super(color); // 用 super 繼承 Shape 的屬性
        this.radius = radius;
    }

    draw() {
        super.draw(); // 執行 Shape 的 draw 方法
        console.log(`This is a circle with radius ${this.radius}`); // 接著再執行新定義的邏輯
    }
}

都定義完後,可用 new 關鍵字來建立實例,並呼叫方法。

const myRectangle = new Rectangle(10, 5, 'blue');
myRectangle.draw();  // 會印出兩段文字,分別是 'Drawing a blue shape',以及 'This is a rectangle with width 10 and height 5'
console.log(`myRectangleArea: ${myRectangle.getArea()}`); // 印出 'myRectangleArea: 50'

const myCircle = new Circle(7, 'red'); 
myCircle.draw(); // 會印出兩段文字,分別是 'Drawing a red shape',以及 'This is a circle with radius 7'

可看出我們並沒有在 RectangleCircle 建構子中明確定義 draw 方法要印出這是在繪製哪個顏色的圖形,只有說要繼承 Shape 的方法,因 JavaScript 原型繼承的關係,當我們呼叫 myRectangle.draw() 時就會向上層追到 Shape 原型定義的方法,這就是 prototype 的用意,可讓物件使用共通的方法,而不需要重複定義一樣的邏輯,共用方法也因而能節省記憶體的使用。

Object.create

Object.create可讓我們將其他物件作為原型使用,並依此創建新物件。使用方式是 Object.create(prototype, propertiesObject),第一個參數 prototype 是指定要創建物件的原型物件,第二個參數 propertiesObject 是選填,可指定要創建的物件的初始化屬性。
舉例來說,如果我們有一個 myCar 物件如下:

const myCar = {
    name: 'FooBar',
    
    drive(){
        console.log('Weee. I am driving!')
    }
};

我們可以用 Object.create 來建立新物件,並指定要以 myCar 物件作為原型:

const newCar = Object.create(myCar);

console.log(newCar.name); // 因為 newCar 會繼承 myCar 屬性,所以 name 會印出 FooBar

// 如果要更改 newCar 的 name,再指派一個值給 newCar.name 即可
newCar.name = 'Midnight';
console.log(newCar.name); // Midnight

優點

以 Prototype 作為解決方案優點如下:

  • 記憶體節省:物件會共享原型中的方法,因此不需要在每個物件中都重複儲存這些方法,可節省記憶體
  • 動態擴展:物件和原型可以在 runtime 動態修改或擴展,提高靈活度。例如,可隨時向原型增加新方法或屬性,並立即反映在所有繼承自該原型的物件中
  • 效率:建立物件時,使用原型可避免呼叫建構函式,可在創建大量相似物件時提高效率

缺點

以 Prototype 作為解決方案缺點如下:

  • 屬性混淆:原型定義的屬性和方法可能和該物件本身的屬性混淆,尤其是物件和原型有相同的屬性名稱時
  • 原型污染:因為原型是共享的,對原型的修改會影響到所有繼承該原型的物件,可能會難以追蹤錯誤
  • 繼承複雜性:原型繼承模式需要花時間心力理解,可能需要更多時間設計和管理

應用情境 & 其他補充

JavaScript 的原型鏈設計可讓我們使用許多已定義好的共用方法,例如 forEach 是陣列原型的方法,所以我們在建立陣列時,不需要為新陣列重新定義 forEach 方法,就可以透過原型鏈的方式往上呼叫,可看出平常實務應用很常使用到原型的概念~

Reference


上一篇
[Day 05] Singleton 模式
下一篇
[Day 07] Factory 模式
系列文
30天的 JavaScript 設計模式之旅12
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言