『面试的底气』—— 设计模式之JavaScript中的单例模式

860 阅读3分钟

前言

在上期的文章中详情介绍什么是单例模式及如何实现,具体可以看『面试的底气』—— 设计模式之单例模式|8月更文挑战

这期会着重介绍JavaScript中的单例模式,为什么会这么说呢?回顾一下单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

注意里面一句话:“保证一个类仅有一个实例”,实例是一个对象。对象从“类”中创建而来。在以类为中心的语言中,这是很自然的做法。比如在 Java 中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来的。而在JavaScript中创建一个对象,根本用不到类来创建,那么传统的单例模式实现在 JavaScript 中并不适用。所以本文专门来介绍一下JavaScript中的单例模式

全局变量不是单例模式

使用var创建一个全局变量对象a,对象a是在程序中是独一无二,而且可供全局访问对象a,按单例模式的定义,好像创建全局变量就是一个单例模式。但是全局变量可以被修改的,而用单例模式创建的类,类实例化生成的对象在第一次生成后是不能被修改的。所以全局变量不是单例模式。

不使用类来实现一个单例模式

举一个例子来说明,比如点击一个登录按钮,弹出一个登录框,那该怎么实现呢?

第一种实现方案是在页面加载完成的时候便创建好这个登录框,这个登录框一开始是隐藏状态的,当用户点击登录按钮的时候,它才开始显示:

<html>
<body>
    <button id="loginBtn">登录</button>
</body>
<script>
var loginModal = (function() {
    const element = document.createElement('div');
    element.innerHTML = '我是登录浮窗';
    element.style.display = 'none';
    document.body.appendChild(element);
    return element;
})();
document.getElementById('loginBtn').onclick = function() {
    loginModal.style.display = 'block';
};
</script>
</html>

这种实现方案有一个明显的问题,就是会延长页面首次加载时间。那换一种实现方法,用户点击登录按钮的时候,才创建登录框。

<!DOCTYPE html>
<html>
<body>
    <button id="loginBtn">登录</button>
</body>
<script>
const creatLoginModal = function() {
    const element = document.createElement('div');
    element.innerHTML = '我是登录浮窗';
    element.style.display = 'none';
    document.body.appendChild(element);
    return element;
};
document.getElementById('loginBtn').onclick = function() {
    const loginModal = creatLoginModal();
    loginModal.style.display = 'block';
};
</script>
</html>

现在可以在用户点击登录按钮时创建一个登录框并显示,但是发现了一个更为严重的问题,登录框会被重复创建。

针对上面问题,可以使用单例模式来解决它。

<html>
<body>
    <button id="loginBtn">登录</button>
</body>
<script>
    const creatLoginModal = (function() {
        let element;
        return function() {
            if (!element) {
                element = document.createElement('div');
                element.innerHTML = '我是登录浮窗';
                element.style.display = 'none';
                document.body.appendChild(element);
            }
            return element;
        }
    })();
    document.getElementById('loginBtn').onclick = function() {
        const loginModal = creatLoginModal();
        loginModal.style.display = 'block';
    };
</script>
</html>

在上述代码中,把creatLoginModal方法改成一个自执行的匿名函数,形成一个闭包,在闭包中创建一个变量element,其会被缓存在闭包中,会记录下上一次调用creatLoginModal方法创建登录框的DOM。然后使用变量element来判断登录框是否被创建过。

此时可以发现在JavaScript中不用类,也可以使用单例模式。可以说明JavaScript中的单例模式和类之间没有什么必要的联系。

惰性单例

可以把上面实现的单例模式称为惰性单例,惰性单例指的是在需要的时候才创建对象或实例对象。惰性单例是单例模式的重点,这种技术在实际开发中非常有用。下面来封装一个方法来生成一个惰性单例。

const getSingle = function( fn ){
    const result;
    return function(){
        return result || ( result = fn .apply(this, arguments ) );
    }
};

creatLoginModal方法中把实现单例的逻辑中不变的部分(判断是否已生成对象)抽取出来。其中result是创造对象或实例对象的函数fn生成的,而函数fn是变化的,故把函数fn当作参数传递进去。

应用惰性单例

利用上面封装的getSingle方法来创建惰性单例。

例如实现在一个会被重复渲染元素只绑定一次click事件:

const bindEvent = getSingle(function(){
   document.getElementById( 'div' ).onclick = function(){
      alert ( 'click' );
   }
   return true;
});
const render = function(){
   console.log( '开始渲染' );
   bindEvent();
};

例如创建唯一的iframe用于动态加载第三方页面:

const createSingleIframe = getSingle(function() {
   const iframe = document.createElement('iframe');
   document.body.appendChild(iframe);
   return iframe;
});
document.getElementById('btn').onclick = function() {
   const iframeLayer = createSingleIframe();
   iframeLayer.src = 'http://baidu.com';
};