JS常用设计模式解析01-单例模式

1.实例演进

考虑实现如下功能,点击一个按钮后出现一个遮罩层。 原始办法:我们只需要实现一个创建遮罩层的函数并将其作为按钮点击的回调事件即可。如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        function createMask() {
            var mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        }
        window.onload = function() {
            document.getElementById('button').addEventListener('click', function() {
                var mask = createMask();
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

这里我们来看看效果:

原始方法

可以看到,每次点击都会创建一个新的遮罩层。而且老的遮罩层也仍然存在。这会无限增大html的体积。

改进办法1:将每次点击遮罩层隐藏改为将其移除。即:

mask.addEventListener('click', function () {
    document.body.removeChild(this);
});

具体效果这里就不演示了。 但即使这样,我们每一次点击仍然会创建一个新的遮罩层,损耗性能。

改进办法2:在页面初始化时建立一个隐藏的遮罩,每次点击只是控制其display属性。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        function createMask() {
            var mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        }
        window.onload = function() {
            var mask = createMask();
            document.getElementById('button').addEventListener('click', function() {
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

这样的话就不用每次点击按钮都新创建一个遮罩层了,可是还有一个缺点,那就是,如果用户并没有点击按钮,这个遮罩层不是白白创建了吗。

改进办法3:点击按钮的时候,动态判断是否需要新建一个遮罩层

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        var mask;
        function createMask() {
            if (mask) {
                return mask;
            }
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        }
        window.onload = function() {
            document.getElementById('button').addEventListener('click', function() {
                mask = createMask();
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

这样看上去已经很不错了,可是问题还是有,那就是mask成为了一个全局变量。 改进办法4:将mask当做局部变量,createMask当做闭包来引用。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        var createMask = (function () {
            var mask;
            return function () {
                if (mask) {
                    return mask;
                }
                mask = document.createElement('div');
                mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
                mask.addEventListener('click', function () {
                    this.style.display = 'none';
                });
                document.body.appendChild(mask);
                return mask;
            }
        })();

        window.onload = function() {
            document.getElementById('button').addEventListener('click', function() {
                var mask = createMask();
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

到这里,我们的代码已经很不错了。然而,设想这样一个场景,你在不同的页面,需要使用不同背景颜色的mask。怎么办?一个简单的想法,就是像createMask里面传参。可是,你又有了新的需求,不同页面还需要不同的透明度,也简单,再增加一个参数。那么问题来了,第一,你不可能无限制地为函数增加参数,第二,你的两个页面需要创建的mask可能是根本不一样的,比如另一个mask是一张图片,和前一种mask的创建方法没有什么共同性。那么这里最好的办法其实就是定义不同的创建mask的方法,然后根据需要使用和不同的创建方法。 改进办法5:抽象成更通用的单例模式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        var maskMethod1 = function() {
            var mask;
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        };
        var maskMethod2 = function() {
            var mask;
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#abc;opacity:0.6;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        };
        var mask;
        var createMask = function (fn) {
            return mask || (mask = fn.apply(this,arguments));
        };

        window.onload = function() {
            document.getElementById('button').addEventListener('click', function() {
                var mask = createMask(maskMethod2);
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

但是这里,为了使用 createMask的时候可以动态传参,我引入了一个全局变量。不知道有没有同学知道这里该如何不引入全局变量且能支持传参呢?如果知道的同学,还请不吝赐教哈 (找到办法了,写这篇文章的时候我还没有看到《JavaScript设计模式与开发实践》这本书,看过以后,发现这一章和作者的思路还是挺接近的,但是作者的分析更加全面和精辟。而且,作者也没有通过引入全局变量来进行抽象,建议大家看一下这本书。真的很精辟。强烈推荐。) 改进办法6:利用闭包抽象成更通用的单例模式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        var maskMethod1 = function() {
            var mask;
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        };
        var maskMethod2 = function() {
            var mask;
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#abc;opacity:0.6;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        };

        var getMaskCreate = function (fn) {
            var mask;
            return function() {
                return mask || (mask = fn.apply(this,arguments));
            }
        };

        window.onload = function() {
            var createMask = getMaskCreate(maskMethod2);
            document.getElementById('button').addEventListener('click', function() {
                var mask = createMask();
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

2. 单例模式的思想与优点

由第1节的遮罩层例子,引出单例模式的设计思想,其实质就是:保证一个类仅有一个实例,并且提供一个访问它的全局访问点。 单体模式具有如下优点:

  • 可以用来划分命名空间,减少全局变量的数量。
  • 使用单体模式可以使代码组织的更为一致,使代码容易阅读和维护。
  • 可以被实例化,且实例化一次。

3. 单例模式的实现

单例模式的基本结构:

var Singleton = function(name){
    this.name = name;
    this.instance = null;
};
Singleton.prototype.getName = function(){
    return this.name;
}
/* *
 * 1.这里的this在非严格模式下指向全局变量
 * 2. 用this而不用window可以根据宿主指向全局变量,比如node是global
 * 3. 使用这种写法不能使用new直接调用
*/
function getInstance(name) {
    if(!this.instance) {
        this.instance = new Singleton(name);
    }
    return this.instance;
}
// 这里不能直接通过new来调用
var a = getInstance("a");
var b = getInstance("b");
// 证明该对象仅可被实例化一次
console.log(a === b);  // true
// 证明创建了一个额外的全局变量
console.log(window.instance); // Singleton {name: "a", instance: null}
console.log(a === window.instance);  // true

这种模式很好理解,但是额外创建了一个全局变量。

闭包实现单例模式

var Singleton = function(name){
    this.name = name;
};
Singleton.prototype.getName = function(){
    return this.name;
}
// 使用闭包,使instance不再暴露到全局
var getInstance = (function() {
    var instance = null;
    return function(name) {
        if(!instance) {
            instance = new Singleton(name);
        }
        return instance;
    }
})();
// 这里可以通过new来直接调用,也可以直接调用
var a = new getInstance("a");
var b = getInstance("b");
// 证明该对象仅可被实例化一次
console.log(a === b);  // true
// 证明并未创建一个额外的全局变量
console.log(window.instance); // undefined
console.log(a === window.instance);  // false

有些同学会想,既然这里只是不想额外创建一个单例对象的全局实例变量,那我干脆将整个逻辑都包裹起来,比如我们需要一个可以通过传入html内容动态创建div的单例对象,只需要写成如下形式:

var CreateDiv;
(function() {
    var instance;
    CreateDiv = function(html) {
        if (instance) {
            return instance;
        }
        this.html = html;
        this.init();
        return instance = this;
    };
    CreateDiv.prototype.init = function() {
        var div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.appendChild(div);
    }
    return CreateDiv;
})();

var a = new CreateDiv('html1');
var b = new CreateDiv('html2');
// 证明该对象仅可被实例化一次
console.log(a === b);  // true
// 证明并未创建一个额外的全局变量
console.log(window.instance); // undefined
console.log(a === window.instance);  // false

这样岂不是封装性更好?可事实上是,相比于前两种写法,这里的代码逻辑变得更加复杂。为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且在这个匿名函数中实现真正的Singleton构造方法和原型逻辑,这让代码的可维护性变差。

另外,CreateDiv的构造函数负责了两件事情。1.创建对像和执行初始化init方法,第二是保证只有一个对象。这违背了设计模式中的单一职责的原则。

所以,使用第二种方法,即避免了额外创建一个全局的实例变量,又能够很好地区分开函数的职责。这种方法又叫做代理模式比如上面通过传入html内容动态创建div的单例对象。

var CreateDiv = function(html ='default html') {
    this.html = html;
    this.init();
}
CreateDiv.prototype.init = function(){
    var div = document.createElement("div");
    div.innerHTML = this.html;
    document.body.appendChild(div);
};
// 使用代理
var ProxyMode = (function(){
    var instance;
    return function(html) {
        if(!instance) {
            instance = new CreateDiv(html );
        }
        return instance;
    } 
})();
var a = new ProxyMode("html1");
var b = new ProxyMode("html2");
console.log(a===b);// true
// 这里要注意由于只会实例化一次,所以只有第一次实例化时所传的参数才有效
console.log(b); // CreateDiv {html: "html1"}

参考

BOOK-《JavaScript设计模式与开发实践》 第4章 Javascript设计模式详解 【原】常用的javascript设计模式 js设计模式 [译] 你应了解的4种JS设计模式 深入理解javascript之设计模式 JavaScript实现单例模式 JavaScript设计模式----单例模式

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏梦魇小栈

面试分享:2018阿里巴巴前端面试总结(题目+答案)

最开始的思路是用定时器实现,最后没有想的太完整,面试官给出的答案是用requestAnimationFrame。

1243
来自专栏finleyMa

补充上一篇 实现基于最新chrome的动态按需加载组件

上面代码中,import函数的参数specifier,指定所要加载的模块的位置。import命令能够接受什么参数,import()函数就能接受什么参数,两者区别...

1165
来自专栏Alan's Lab

如何编写一个 jQuery 插件

https://github.com/zcfan/sket... 重写了本文的初步功能实现,支持一个页面多个画图板。但为简单起见,本文保持不变。

1324
来自专栏偏前端工程师的驿站

前端构建:Less入了个门

一、前言                                说到前端构建怎能缺少CSS预处理器呢!其实CSS的预处理器有很多啦,比较出名的有Scs...

1927
来自专栏从零开始学自动化测试

Selenium+python自动化82-只截某个元素的图

前言 selenium截取全图小伙伴们都知道,曾经去面试的时候,面试官问:如何截图某个元素的图?不要全部的,只要某个元素。。。小编一下子傻眼了, 苦心人,天不负...

4864
来自专栏偏前端工程师的驿站

前端构建:Less入了个门

一、前言                                说到前端构建怎能缺少CSS预处理器呢!其实CSS的预处理器有很多啦,比较出名的有Scs...

1917
来自专栏JetpropelledSnake

Vue学习笔记之Vue的使用

我们能发现,引入vue.js文件之后,Vue被注册为一个全局的变量,它是一个构造函数。

903
来自专栏程序员宝库

Chrome 调试技巧

想必大家都在用console.log在控制台输出点东西,其实console还有其它的方法:

3362
来自专栏非著名程序员

仿苹果数字键盘以及判断信用卡有效期的Editext

这次带来一个小小的信用卡有效期规则的Editext,额外赠送内置数字键盘的开发 首先来看下需求: 1) 月份数字: λ 数字输入0:后一位数字可输入...

2185
来自专栏抠抠空间

JavaScript之BOM

一、什么是BOM? BOM(Browser Object Model)是指浏览器对象模型,它使 JavaScript 有能力与浏览器进行“对话”。 二、Wind...

2995

扫码关注云+社区

领取腾讯云代金券