發布日期: July 12, 2024
最後更新: August 2, 2024
閱讀時間: ~ 5 min
此篇文章介紹了單例模式在 JavaScript 中的實作方式,包含了一般的單例模式、惰性單例模式、ES6 Module 單例模式、用代理模式 (Proxy Pattern) 封裝單例模式等。
單例模式 (Singleton Pattern)
單例模式的定義為「保證一個類別僅有一個實例,並提供一個全局訪問點」(JavaScript 設計模式與開發實踐)。而單例模式在前端開發中最常見的場景就是彈跳提示框,當使用者在網頁上進行操作時,往往需要一些提示框來提醒使用者,而這種提示框通常只需要創建一次,之後就可以重複使用。
以下是一個簡單的單例模式的實作:
class Singleton {
// 用來保存唯一實例的靜態屬性,並且設置為私有
static #instance = new this();
// 透過此方法取得唯一實例
static getInstance() {
return this.#instance;
}
}
2
3
4
5
6
7
8
9
使用方法:
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
2
3
4
惰性單例模式 (Lazy Singleton Pattern)
上面的實作方式在剛載入時就會創建實例,除了在程式初始化時多了不必要的運算外,也佔用了記憶體空間。而惰性單例模式是一種更好的實作方式,只有在真正使用時才會創建實例。
以下是將上面的實作改為惰性單例模式 (使用方法相同):
class Singleton {
// 一開始不創建實例
static #instance;
static getInstance() {
// 當第一次使用時才創建實例
if (!this.#instance) {
this.#instance = new this();
}
return this.#instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
使用 getter
實作惰性單例
另外也可以用 getter
的方式來實作 (參考自 7 ways to create Singleton Pattern in JavaScript),這個方法與前一個沒什麼差別,看個人喜好:
class Singleton {
static #instance;
static get instance() {
if (!this.#instance) {
this.#instance = new this();
}
return this.#instance;
}
}
2
3
4
5
6
7
8
9
10
使用方法:
const instance1 = Singleton.instance;
const instance2 = Singleton.instance;
console.log(instance1 === instance2); // true
2
3
4
更好的實作方式?
上面的實作只要使用 new 方法就無法達到單例模式的效果,由於我們不能控制使用者如何創建實例,所以可以直接在 constructor 中實作單例的邏輯:
class Singleton {
static #instance;
constructor() {
if (!Singleton.#instance) {
Singleton.#instance = this;
}
return Singleton.#instance;
}
}
2
3
4
5
6
7
8
9
10
使用方法:
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
2
3
4
這是更好的嗎?
由於查到的資料中比較少用此方式來實作單例模式,所以這個可能不是我所說「更好」的方式,因此我才在標題後加上問號。但這個方式避免了使用者直接使用 new 來創建實例,而且也不需要再透過 getInstance
或 getter
來取得實例,因此我認為這個寫法更加的優雅,若是讀者有更好的實作方式,或是對於這個寫法有疑問,歡迎透過任何方式聯絡我。
ES6 Module 下的單例模式
在 ES6 Module 中,每個模組都是單例的 (ES Module 本身就是單例),因此我們可以直接在摸組中定義類別,並且再創建一個實例後導出,這樣就可以達到單例模式的效果。
class Singleton {
constructor() {
// ...
}
}
export const instance = new Singleton();
2
3
4
5
6
7
使用方法:
import { instance } from './Singleton.js';
這是惰性單例嗎?
沒錯,這種方式也是惰性單例模式,因為只有在真正引入模組時才會創建實例,而引入模組表示我們需要使用這個實例。
用代理模式 (Proxy Pattern) 來封裝單例模式
上面的方式都需要在類別中實作單例模式,若每個需要單例模式的類別都要實作一次,會讓程式碼重複性增加,因此可以使用代理模式,將單例模式的邏輯抽離出來,讓所有需要單例的類別都可以使用。
以下是用代理模式實作單例模式 (此程式碼參考 How can I implement a singleton in JavaScript?):
const singletonProxy = (className) => {
// 利用閉包保存私有實例
let instance;
// 透過 Proxy 來管理類別創建的過程
return new Proxy(className.prototype.constructor, {
// 當使用 new 關鍵字時,會觸發此方法
construct: (target, arguments) => {
if (!instance) {
instance = new target(...arguments);
}
return instance;
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
使用方法:
class MyClass {
constructor() {
// ...
}
}
const SingletonMyClass = singletonProxy(MyClass);
const instance1 = new SingletonMyClass();
const instance2 = new SingletonMyClass();
console.log(instance1 === instance2); // true
2
3
4
5
6
7
8
9
10
11
12
結論
此篇文章提供了幾種單例模式在 JavaScript 中的實作方式,單例模式的實作方式有很多種,推薦有興趣的讀者可以多去查資料,例如 7 ways to create Singleton Pattern in JavaScript 這篇文章就提供了 7 種不同的實作方式,並且介紹了每種方式的優缺點,非常推薦讀者閱讀。