背景
写前端代码时,应该都用过单例模式,今天来详细聊一下,例如一个很简单的单例模式:
// 关键代码如下
let instance;
class Myclass {
constructor() {
// dosomething
}
static getInstance() {
if(!instance) {
instance = new MyClass()
}
return instance
}
// other method
}
export Myclass.getInstance()
测试了下,只实例化了一次,不管哪个模块调用都是调用同一个实例化对象。难道变量被 esmodule 缓存下来了?
测试(esmodule && commonjs && AMD)
为了验证这个问题,我使用 esmodule 和 commonjs 以及 require.js做了测试。esmodule 测试代码如下:
// entry.js
import lru from './lru.js'
import myFn from './module.js'
lru.push({phone: 134000000001, word: '11111111'})
let _currentUser = lru.getByIndex(0)
console.log(_currentUser, 'check')
myFn()
// module.js
import lru from './lru.js'
function myFn() {
setTimeout(() => {
lru.push({phone: 134000000004, word: '11111111'})
let _currentUser = lru.getByIndex(0)
console.log(_currentUser, 'check')
},2000)
}
export default myFn
// lru.js
// class width singleton
let LRUCacheInstance
class LRUCache {
constructor() {
console.log('实例化多少次')
this.store = []
if(!this.store) {
this.store = []
}
this.capacity = 10
}
static getInstance() {
if(LRUCacheInstance) {
return LRUCacheInstance
} else {
return new LRUCache()
}
}
push(obj) {
// 判断是否已存
// 若已存在,则将值提到最前面
// 若不存在,则插入数据,并检查长度
let _curr = this.get(obj.phone)
if(_curr) {
let _currIndex = this.store.findIndex(item => item.phone == obj.phone)
this.store.splice(_currIndex, 1)
}
this.store.unshift(obj)
if(this.store.length > this.capacity) {
this.store.pop()
}
this.saveStore(this.store)
}
get(key) {
let _val = this.store.filter(item => item.phone === key)
if(_val && _val.length) {
return _val[0]
}
return false
}
getByIndex(index) {
return this.store[index]
}
saveStore(arr) {
console.log(1)
}
}
export default LRUCache.getInstance()
测试结果都是一致的,模块内的变量都被缓存了下来,多个不同模块调用的都是第一个实例化对象。
模块化与变量缓存
目前应该只有commonjs可以直接获取到模块化的缓存值(通过require.cache,更多关于require.cache可以查看我之前关于hmr的文章),所以我们可以通过commonjs看下:
// index.ts
import utils from './utils'
utils.changePeriod()
console.log(require.cache)
// utils.ts
let consfig = {
period: 300
}
function sleep () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true)
}, consfig.period)
})
}
function changePeriod() {
consfig.period = 1000
}
export default {
sleep,
changePeriod
}
执行后可以发现 require.cache 中 utils 模块里面导出的 sleep 方法中有一个 scope 属性,里面缓存了 config 的引用。所以当 config 发生变化的时候, sleep 里仍然可以获取到变化后的值;
所以其实模块化之后,导出的函数/对象中若有引用外部变量的部分,会将外部变量的引用缓存起来;
结论
ECMAscript虽然已支持 class ,但相对于其他“面向对象”的语言支持度仍比较弱。按照目前模块化的开发方式,可以不再那么丑的通过污染全局变量的方式实现实例化,可以在实例模块中声明一个实例变量,通过该变量缓存实例并在其他模块调用即可。