ECMAscript单例模式和模块化

585 阅读2分钟

背景

写前端代码时,应该都用过单例模式,今天来详细聊一下,例如一个很简单的单例模式:

// 关键代码如下
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 ,但相对于其他“面向对象”的语言支持度仍比较弱。按照目前模块化的开发方式,可以不再那么丑的通过污染全局变量的方式实现实例化,可以在实例模块中声明一个实例变量,通过该变量缓存实例并在其他模块调用即可。