00、基本概念
單例模式(Singleton Pattern),也稱單體模式,就是全局(或某一作用域範圍)唯一實例,大家共享、複用一個實例對象,也可減少內存開銷。單例模式應該是最基礎、也最常見的設計模式了。
✅常見場景:
- 全局狀態vuex,Jquery中的全局對象
$
,瀏覽器中的window、document 都算是單例。- 公共的服務、全局配置、緩存、登錄框等,全局複用一個對象。
所以實現單例模式的關鍵就是保障對象實例只創建一次,後續的引用都是同一個實例對象。相比於Java、C#等語言,JavaScript單線程,也沒有類,單例實現還是比較容易,基於JS語言特性,有多種實現思路。
實現方式 | 說明 |
---|---|
全局對象 | 全局環境下的var變量,或者直接掛載到全局對象window 上。使用簡單,但會存在全局污染,也不優雅,🚫不推薦! |
構造函數.靜態方法getInstance | 使用構造函數的靜態方法getInstance() 來獲取實例,唯一實例對象存儲在構造函數的instance 上。 |
雖有一定耦合,Class版本還是一種不錯的方式 | |
閉包-new | 利用JS的閉包(萬惡的閉包)來保存那個唯一對象實例,這樣就可以new 來獲取唯一實例對象了 |
ES6模塊Module | ES6的模塊其實就是單例模式,模塊中導出的對象就是單例的,多次導入其實是同一個引用。 |
01、全局對象(不推薦)
創建一個全局對象,瀏覽器中全局對象一般掛載在Window上,如JQuery、loadsh就是如此實現的。
- 在全局環境中用
var
字面量申明一個對象,利用了var的變量提升 + 全局屬性的特點(全局環境下的var變量會自動成爲全局屬性),所以慎用var
。 - 直接掛載到全局對象window上。
window.jQuery = window.$ = jQuery;
window._ = lodash;
// 全局環境下的var變量會自動成爲全局屬性
var singleUser = {
name: 'sam',
id: 1001
}
// 使用
console.log(singleUser.name) // sam
console.log(window.singleUser.name) // sam
02、構造函數.靜態方法getInstance
統一一個入口獲取對象實例,入口就是爲構造函數的靜態方法getInstance()
(當然命名隨意),在該函數中判斷(靜態)對象instance
是否初始化,沒有則創建,有則直接返回。所以實際上的唯一實例是作爲靜態屬性,保存在構造器的instance
屬性上,類似Math.PI
。
function GlobalUser(name) {
this.name = name
this.id = 1002
}
// 基於構造函數的靜態函數作爲統一入口,Constructor.getInstance()
GlobalUser.getInstance = function(name) {
// 注意這裏的this指向的是構造函數GlobalUser
if (this.instance) return this.instance
// 第一次沒有創建
return this.instance = new GlobalUser(name)
}
console.log(GlobalUser.getInstance('張三').name) // 張三
console.log(GlobalUser.getInstance('李四').name) // 張三,依然是張三,複用了第一次創建的實例
console.log(GlobalUser.getInstance() === GlobalUser.getInstance()) // true
ES6的Class 版本的,原理和上面一樣,因爲Class本質上也是基於原型的構造函數,但實現起來更優雅一些,推薦使用。
class GlobalUser {
constructor(name) {
this.name = name
this.id = 1002
}
static getInstance(name) {
//靜態方法屬於類本身,這裏的this也就指向類本身
if (!this.instance)
this.instance = new GlobalUser(name)
return this.instance;
}
}
console.log(GlobalUser.getInstance('張三').name) // 張三
console.log(GlobalUser.getInstance('李四').name) // 張三,依然是張三,複用了第一次創建的實例
console.log(GlobalUser.getInstance() === GlobalUser.getInstance()) // true
03、閉包-new
核心思路就是利用JS的閉包(萬惡的閉包)來保存那個唯一對象實例,這樣就可以new
來獲取唯一實例對象了!基於閉包的實現方式是比較多的,下面示例只是其中一種,但基本原理都是利用閉包來保存那個“唯一實例”。
let GlobalUser = (function() {
let instance // 閉包保存的唯一實例對象
return function(name) {
if (instance) return instance
// (首次)創建實例
instance = { name: '張三', id: 1003 }
return instance
}
})() // 立即執行,外層函數的價值就是他的閉包變量instance
console.log(new GlobalUser('張三').name) // 張三
console.log(new GlobalUser('李四').name) // 張三,依然是張三,複用了第一次創建的實例
console.log(new GlobalUser() === new GlobalUser()) // true
斷點輸出一下日誌可以看到GlobalUser
的構造函數閉包
閉包版本還可以繼續改進下,做成一個通用版本的單例工廠:把具體的對象示例構造器封裝一下。
// 一個通用單例工廠,參數爲構造器函數、Class類
let Singleton = function(Constructor) {
let instance
return function(...args) {
if (instance) return instance
// (首次)創建實例
instance = new Constructor(...args)
return instance
}
}
// 構造函數
function User(name) {
this.name = name
}
class Config {
constructor(title) {
this.title = title
}
}
// 使用
let SingleUser = Singleton(User)
let u1 = new SingleUser('sam')
let u2 = new SingleUser('zhangsan')
console.log(u1 === u2, u1, u2) //true User {name: 'sam'} User {name: 'sam'}
let GlobalConfig = Singleton(Config)
console.log(new GlobalConfig('設計') === new GlobalConfig('模式')) // true
04、ES6模塊Module
ES6的模塊其實就是單例模式,模塊中導出的對象就是單例的,多次導入其實是同一個引用。
回顧一下ESM:參考《ESModule模塊化》
- 📢 Singleton 模式:
import
模塊的代碼只會執行一次,同一個url文件只會第一次導入時執行代碼。後續任何地方import
都不會執行模塊代碼了,也就是說,import
語句是 Singleton 模式的。- 📢 只讀-共享:模塊導入的接口的是隻讀的,不能修改。當然引用對象的屬性值是可以修改的,不建議這麼幹,注意模塊是共享的,導出的是一個引用,修改後其他方也會生效。
因此用ESM實現單例就比較簡單了:
// 模塊申明 config.js
export default {
title: '設計模式'
}
// 使用
import config from './config.js'
console.log(config) // {title: '設計模式'}
config.title = '修改一下'
import config2 from './config.js'
console.log(config, config2) // {title: '修改一下'} {title: '修改一下'}
參考資料
- 《Head First 設計模式 中文版》
- JavaScript Patterns
©️版權申明:版權所有@安木夕,本文內容僅供學習,歡迎指正、交流,轉載請註明出處!原文編輯地址-語雀