[JS] JS單例模式的實現(xiàn)
JS 單例模式的實現(xiàn)
單例模式簡介
單例模式(Singleton Pattern)是最簡單的設計模式之一。這種類型的設計模式屬于創(chuàng)建型模式,提供了一種創(chuàng)建對象的最佳方式。
特點:
- 意圖:保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。
- 主要解決:一個全局使用的類頻繁地創(chuàng)建與銷毀。
- 何時使用:當您想控制實例數(shù)目,節(jié)省系統(tǒng)資源的時候。
- 如何解決:判斷系統(tǒng)是否已經(jīng)有這個單例,如果有則返回,如果沒有則創(chuàng)建。
首先對比一下平時不使用單例模式的情況:
在不適用單例模式的情況下,如下,會得到不同的多個實例:
video.js
class Video{
constructor() {
console.log("video created");
}
}
export { Video };
main.js
import { Video } from "./video.js";
const v1 = new Video();
const v2 = new Video();
console.log(v1 === v2);
控制臺輸出

方法1:提前構造實例
提前構造一個實例,只向外部暴露該實例的引用,從而實現(xiàn)單例。
但是缺點是需要提前構造實例,而無法做到在需要的時候創(chuàng)建實例。
class Video{
constructor() {
console.log("video created");
}
}
const v = new Video();
export { v };
方法2:構造方法私有化
video.js
class Video{
private constructor() {
console.log("video created");
}
static _ins = null;
static getInstance(){
if(!this._ins){
this._ins = new Video();
}
return this._ins;
}
}
export { Video };
main.js
import { Video } from "./video.js";
const v1 = Video.getInstance();
const v2 = Video.getInstance();
console.log(v1 === v2);
通過將構造方法私有化,外部無法通過new實例化對象,只能通過靜態(tài)方法getInstance獲取實例。
而在類的內部實現(xiàn)中,使用_ins確保只存在一個實例。
缺點:
原生JS不存在private,需要使用TS。
在JS中,如果剔除private,僅通過getInstance也可以實現(xiàn)單例模式。但是這種方法不嚴格,無法確保每一個調用者不會使用構造函數(shù)創(chuàng)建新的實例。
方法3:通用方法——將任意類轉為單例
singleton.js
export function singleton(className){
let ins;
return class{
constructor(...args) {
if(!ins){
ins = new className(...args);
}
return ins;
}
};
}
- 這個函數(shù)接收一個類,進行改造之后返回一個新的類;
- 使用閉包,存儲實例對象
ins; - 新的類的構造函數(shù)相當于攔截作用:
- 如果
ins不存在,則將傳入的參數(shù)轉交給原來的類的構造函數(shù),并創(chuàng)建一個實例; - 如果
ins存在,則直接返回存儲在閉包中的實例對象。
- 如果
video.js
import { singleton } from "./singleton.js";
class Video{
constructor() {
console.log("video created");
}
}
const newVideo = singleton(Video);
export { newVideo as Video };
main.js
import { Video } from "./video.js";
const v1 = new Video();
const v2 = new Video();
console.log(v1 === v2);
控制臺輸出

觀察到這種實現(xiàn)下,構造函數(shù)只被調用了一次,并且v1和v2指向同一個實例。
缺點:
main.js
Video.prototype.play = function(){
console.log("play");
}
v1.play(); // Uncaught TypeError: v1.play is not a function
在這個案例中,我們試圖在Video的原型上添加一個方法,并通過實例對象v1調用,但是v1所處的原型鏈上并不能找到這個方法。
再回過頭來觀察singleton.js的實現(xiàn):
export function singleton(className){
let ins;
return class{
constructor(...args) {
if(!ins){
ins = new className(...args);
}
return ins;
}
};
}
- 在
main.js中,我們使用的Video類來自于video.js的導出,實際上已經(jīng)是經(jīng)過singleton函數(shù)改造的類,也就是上面這段代碼中,return class {}這個匿名類。 - 而對于
v1或v2,它們來自于ins這個實例對象,它由上面這段代碼的new className()創(chuàng)建,也就是說它來自于最“簡單”的、沒有經(jīng)過單例化的那個Video類。 - 綜上,
v1并不是由那個匿名類創(chuàng)建的,所以它們不在同一原型鏈上。這也是這種單例模式實現(xiàn)方式的缺點,需要改進。
方法4:使用代理
這個方法是對方法3的改進,使用Proxy API對類進行代理,往新的類的原型上添加方法,也會被添加到原來的類的原型上,由此解決了方法3的缺點。
singleton.js
export function singleton(className){
let ins;
return new Proxy(className, {
construct(target, ...args){
if(!ins){
ins = new target(...args);
}
return ins;
}
});
}
MDN對于
Proxy中construct更詳細的介紹:handler.construct() - JavaScript | MDN (mozilla.org)
這里的constuct主要是攔截外部的new操作,函數(shù)參數(shù)target指向代理對象,也就是這里的className,即需要被單例化的類。
其它邏輯和方法3一致,使用閉包,通過ins存儲實例對象。
video.js
import { singleton } from "./singleton.js";
class Video{
constructor() {
console.log("video created");
}
}
const newVideo = singleton(Video);
export { newVideo as Video };
main.js
import { Video } from "./video.js";
const v1 = new Video();
const v2 = new Video();
console.log(v1 === v2);
Video.prototype.play = function(){
console.log("play");
}
v1.play();
控制臺輸出

觀察到構造函數(shù)只被調用了一次,并且在單例化的新類的原型上添加方法,實例對象v1也可以訪問到。
這是因為newVideo是對Video的代理(這里的命名以video.js為準),在newVideo對象上的操作會被應用在Video這個對象上。
至此,完成了JS中較為完善的單例模式實現(xiàn)。

浙公網(wǎng)安備 33010602011771號