专栏首页coding for loveJS常用设计模式解析01-单例模式

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 条评论
登录 后参与评论

相关文章

  • 在线商城项目06-商品列表页前端逻辑实现

    step1:价格过滤列表的字段显示。 这里,我们不做太复杂的逻辑,这些过滤字段不从后端请求,也不由用户输入,而是在前端写死。在GoodsList.vue中进行...

    love丁酥酥
  • CSS入门4-引入CSS

    (注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

    love丁酥酥
  • 在线商城项目02-展示商品列表页面并抽取公共组件

    step1:新增存放重构资源的目录 视频提供的重构有点问题,大家可以使用我上传在github上的重构资源。 在根目录下新建一个resources文件夹,用来...

    love丁酥酥
  • The Clean Architecture in PHP 读书笔记(三)The Clean Architecture in PHP 读书笔记(三)

    这5大原则最初是由Robert C. Martin提出,这些原则主要解决了下面两个问题:

    zhuanxu
  • 原 荐 七牛 JSSDK 配置+常见问题

    作者:汪娇娇 时间:2017年7月7日 一、铺垫 依靠七牛上传图片,其实有很多方法,先说说有哪些方法,以及这些方法各自的优缺点吧(移动端)。 way1:前端只负...

    jojo
  • 想要那种有提示声音的网站吗?我教你怎么做!

    熟悉不能再熟悉的声音了,今天小编就做了这样一个网站的提示新订单的功能,接下来就教你怎么玩转这个小功能!

    思梦php
  • JavaScript模式 读书笔记三

    函数是第一类对象 first-class object,可以作为带有属性和方法的值以及参数进行传递。

    lilugirl
  • python中使用import 和 from语法来实现其它语言中的include功能。

    示例: 文件c.py def fun(): print "A function." str="A String" 文件a.py from c i...

    战神伽罗
  • 第80天:jQuery插件使用

    jQuery其他补充 + 4.1 链式编程: end()补充 * 补充五角星 评论案例 * 第一步:鼠标移入,当前五角星和前面的五角星变实体。后...

    半指温柔乐
  • 快速入门numpy

    Numpy(Numeric Python)是一个用python实现的科学计算的扩展程序库。包括:1、一个强大的N维数组对象Array;2、比较成熟的(广播)函数...

    小小詹同学

扫码关注云+社区

领取腾讯云代金券