JavaScript设计模式系列三之单例模式(附案例源码)

文章初衷

设计模式其实旨在解决语言本身存在的缺陷

目前javaScript一些新的语法特性已经集成了一些设计模式的实现,

大家在写代码的时候,没必要为了用设计模式而去用设计模式,

那么我这边为什么还写设计模式的文章呢,

一方面是自己的一个整理,然后记录出来,结合自己的理解,

一方面就是虽然语言特性本身已经实现这些模式,有了自己的语法,

但是我们何尝不能去了解一下它是通过什么样的思路去实现了

在我看来设计模式更多的是让我对于思考问题,有了一些更好的思路和想法

文章实现更多的表现为用一些简单的案例,帮助大家去理解这样的一种思路,

会存在故意把设计模式的实现往简单的案例靠拢,

大家在真实项目中不要刻意去用设计模式实现相同的代码

设计模式在平时的一些代码中都会有所体现,大家也许经常用到,

耐心看文章,也许你会发现自己平时的代码就不断在设计模式中体现

JavaScript设计模式系列

JavaScript设计模式系列,讲述大概20-30种设计模式在JavaScript中的运用

后面对应的篇幅会陆续更新,欢迎大家提出建议

这是设计模式系列第三篇,讲述单例模式

上篇文章讲述了建造者设计模式,有兴趣可以查看

注意

JavaScript设计模式系列github地址

深入系列文章部分是有先后顺序的,按照目录结构顺序阅读效果最好。

勘误及提问

如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误。

单例模式

概念:

单例模式主要目的是确保系统中某个类只存在唯一一个实例,

也就是说对于这个类的重复实例的创建始终只返回同一个实例

在JavaScript里,单例可以作为一个命名空间提供者,

从全局命名空间里提供一个唯一的访问点来访问该对象

需要解决的问题

1.就拿我们常玩的超级玛丽游戏来说,游戏中小怪会不断出现,但是超级玛丽只有一个,如果游戏中出现多个玛丽实例的话,那就会出现问题,这时候就会用到我们所说的单例模式

2.再比如说我们页面窗口的弹窗,弹窗大部分情况下我们只需要一个,如果同时出现多个错误弹窗的话,网页就会显示的很奇怪,这时候弹窗的创建也会用到我们所说的单例模式

案例体现

那我们下面就来看一下单例模式在代码中的体现

对于 Java 之类的静态语言而言,实现单例模式常见的方法就是

将构造函数私有化,对外提供一个比如名为getInstance方法

静态接口来访问初始化好的私有静态的类自身实例。

但对于 JavaScript这样的动态语言而言,

单例模式的实现其实可以很灵活,

因为 JavaScript 语言中并不存在严格意义上的类的概念,只有对象。

每次创建出的新对象都和其他的对象在逻辑上不相等,

即使它们是具有完全相同的成员属性的同构造器创造出的对象。

所以,在 JavaScript 中,最常见的单例模式莫过于全局变量了

这边就拿上面提到的超级玛丽游戏举一个简单的🌰

// 游戏人物
var Player = {
    hp: 100,
    mp: 200
}复制代码

可见,全局变量就是一种最简单最常见的单例模式了。在全局的其他地方要获得这个单例的对象,其实就是获得这个唯一的全局变量,就可以保证访问的是同一实例

之前提过在JavaScript里,单例可以作为一个命名空间提供者,
从全局命名空间里提供一个唯一的访问点来访问该对象

那么我们下面来看一下具体实现

命名空间概念:

命名空间就是所谓的namespace,它解决这么一类问题:

为了让代码更易懂,人们常常用单词或者拼音定义变量或者方法,

但是因为可用的单词或者拼音有限,不同人定义的变量可能重复,

此时就需要用命名空间来约束每个人定义的变量来解决这类问题.

看一段代码

/**
 * 命名空间
 */

var Main = {
    run: function() {
        console.log('run');
    },
    jump: function() {
        console.log('jump');
    }
}
Main.run();复制代码

这边定义了一个Main的命名空间,那么所有的属性还有方法都会

放到Main这个命名空间下面,自然就可以保证变量名的唯一性.

jQuery这边也是用的同样的方式,定义了一个jQuery的命名空间,

所以的jQuery方法还有属性都放到这个命名空间,

这样就不会跟别的库的代码冲突

上面的命名空间案例仅仅是一个简单的对象,

但很多时候对象可能还涉及到初始化的工作,

可能需要实现按需加载(懒加载),对象中还会存在内部私有成员,

对外需以门面模式(Facade)提供可访问的接口。

所以我们还可以把这个再扩充一下:

之前有提过单例模式在Java中的实现方式,复习一下

对于 Java 之类的静态语言而言,实现单例模式常见的方法就是将

构造函数私有化,对外提供一个比如名为getInstance方法

静态接口来访问初始化好的私有静态的类自身实例。

我们这边尝试一下Java中的单例模式在javascript中的实现,看🌰

/ 游戏人物
var Player = (function() {
    // 实例
    var instance = null;
    // 私有变量
    var hp = 100; // 生命值
    var mp = 200; // 魔法值
    // 初始化函数
    function init() {
        return {
            // 获取生命值
            getHp: function() {
                return hp;
            },
            // 获取魔法值
            getMp: function() {
                return mp;
            },
            // 行走方法
            run: function() {
                console.log('running');
            }
        }
    }
    return {
        // 初始化一个游戏英雄
        init: function() {
            // 未存在,则初始化
            if (!instance) {
                instance = init();
            }
            return instance;
        }
    }
})();

// 创建一个游戏英雄
var person1 = Player.init();
console.log(person1.getHp());
console.log(person1.getMp());
// 死亡复活
var person2 = Player.init();
// 创建的还是之前的对象
console.log(person1 === person2); // true复制代码

这边创建一个Player类,利用立刻执行函数,

然后创建一个空的实例变量instance用来记录是否已经实例化,

创建2个私有变量hp,mp,创建一个初始化函数init,

利用闭包的方式,暴露对外访问的接口,

细心的人可以看到代码里面输出的person1 完全等于person2

因为提供了公有的静态方法init,负责检验实例的存在性并实例化自己,

然后存储在实例对象instance中,以确保只有一个实例被创建

init: function() {
            // 未存在,则初始化
            if (!instance) {
                instance = init();
            }
            return instance;
        }复制代码

在jQuery中的应用

(function( window, undefined ) {

    var jQuery = (function() {
       // 构建 jQuery 对象
       var jQuery = function( selector, context ) {
           return new jQuery.fn.init( selector, context, rootjQuery );
       }

       // jQuery 对象原型
       jQuery.fn = jQuery.prototype = {
           constructor: jQuery,
           init: function( selector, context, rootjQuery ) {
              // selector 有以下 7 种分支情况:
              // DOM 元素
              // body(优化)
              // 字符串:HTML 标签、HTML 字符串、#id、选择器表达式
              // 函数(作为 ready 回调函数)
              // 最后返回伪数组
           }
       };

       // 猜猜这句是干什么呢?
       jQuery.fn.init.prototype = jQuery.fn;

       // 合并内容到第一个参数中,后续大部分功能都通过该函数扩展
       // 通过 jQuery.fn.extend 扩展的函数,大部分都会调用通过 jQuery.extend 扩展的同名函数
       jQuery.extend = jQuery.fn.extend = function() {};

       // 在 jQuery 上扩展静态方法
       jQuery.extend({
           // ready bindReady
           // isPlainObject isEmptyObject
           // parseJSON parseXML
           // globalEval
           // each makeArray inArray merge grep map
           // proxy
           // access
           // uaMatch
           // sub
           // browser
       });

        // 到这里,jQuery 对象构造完成,后边的代码都是对 jQuery 或 jQuery 对象的扩展
       return jQuery;

    })();

    window.jQuery = window.$ = jQuery;
})(window);复制代码

jQuery这边也采用了上面的方式创建了jQuery命名空间,
然后对应的属性还有方法都在这个命名空间下面扩展,
jQuery这边也把构造函数私有化,在调用的时候完成实例化,
不同点是jQuery每次都返回新的对象

单例模式的优点

  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。

  • 命名空间的管理方式,可以有效避免变量的重复,尽量避免使用全局变量

单例模式的缺点

  • 单例模式实现使用了闭包,过多使用闭包会导致内存泄露

  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。

单例模式适用情况

  • 系统只需要一个实例对象,或者需要考虑资源消耗太大而只允许创建一个对象。

  • 在javascript中,把功能封装起来,形成一个小型代码库,就可以使用命名空间的方式去实现,按照模块化划分

单例模式总结

  • 在javascript中,建议采用命名空间的方式去实现单例,
    在命名空间下面扩展对应的属性还有方法,
    以模块化的方式去完成对应的功能模块

  • 避免滥用单例模式,因为使用了闭包,过渡使用会导致内存泄露

注意

JavaScript设计模式系列github地址

深入系列文章部分是有先后顺序的,按照目录结构顺序阅读效果最好。

关于设计模式,更多的是结合我自己的一些理解,把他总结出来分享给大家,如果大家发现有什么不正确的地方,希望大家一定得提出来,避免我这边误人子弟,感谢大家!!!

    原文作者:算法小白
    原文地址: https://juejin.im/post/59c474c15188256c4b725557
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞