1. 什么是单例模式?
单例模式是一种十分常用但却相对而言比较简单的单例模式它是指在一个类只能有一个实例, 即使多次实例化该类, 也只返回第一次实例化后的实例对象单例模式不仅能减少不必要的内存开销, 并且在减少全局的函数和变量冲突也具有重要的意义
1.1 最简单的单例模式
就算你对于单例模式的概念还比较模糊, 但是我相信你肯定已经使用过单例模式了我们来看一下下面的一段代码:
- let timeTool = {
- name: '处理时间工具库',
- getISODate: function() {},
- getUTCDate: function() {}
- }
以对象字面量创建对象的方式在 JS 开发中很常见上面的对象是一个处理时间的工具库, 以对象字面量的方式来封装了一些方法处理时间格式全局只暴露了一个 timeTool 对象, 在需要使用时, 只需要采用
timeTool.getISODate()
调用即可 timeTool 对象就是单例模式的体现在 JavaScript 创建对象的方式十分灵活, 可以直接通过对象字面量的方式实例化一个对象, 而其他面向对象的语言必须使用类进行实例化所以, 这里的 timeTool 就已经是一个实例, 且 ES6 中 let 和 const 不允许重复声明的特性, 确保了 timeTool 不能被重新覆盖
1.2 惰性单例
采用对象字面量创建单例只能适用于简单的应用场景, 一旦该对象十分复杂, 那么创建对象本身就需要一定的耗时, 且该对象可能需要有一些私有变量和私有方法此时使用对象字面创建单例就不再行得通了, 我们还是需要采用构造函数的方式实例化对象下面就是使用立即执行函数和构造函数的方式改造上面的 timeTool 工具库
- let timeTool = (function() {
- let _instance = null;
- function init() {
- // 私有变量
- let now = new Date();
- // 公用属性和方法
- this.name = '处理时间工具库',
- this.getISODate = function() {
- return now.toISOString();
- }
- this.getUTCDate = function() {
- return now.toUTCString();
- }
- }
- return function() {
- if(!_instance) {
- _instance = new init();
- }
- return _instance;
- }
- })()
上面的 timeTool 实际上是一个函数,_instance 作为实例对象最开始赋值为 null,init 函数是其构造函数, 用于实例化对象, 立即执行函数返回的是匿名函数用于判断实例是否创建, 只有当调用 timeTool() 时进行实例的实例化, 这就是惰性单例的应用, 不在 js 加载时就进行实例化创建, 而是在需要的时候再进行单例的创建 如果再次调用, 那么返回的永远是第一次实例化后的实例对象
- let instance1 = timeTool();
- let instance2 = timeTool();
- console.log(instance1 === instance2); //true
2. 单例模式的应用场景
2.1 命名空间
一个项目常常不只一个程序员进行开发和维护, 然后一个程序员很难去弄清楚另一个程序员暴露在的项目中的全局变量和方法如果将变量和方法都暴露在全局中, 变量冲突是在所难免的就想下面的故事一样:
- // 开发者 A 写了一大段 js 代码
- function addNumber () {}
- // 开发者 B 开始写 js 代码
- var addNumber = '';
- //A 重新维护该 js 代码
- addNumber(); //Uncaught TypeError: addNumber is not a function
命名空间就是用来解决全局变量冲突的问题, 我们完全可以只暴露一个对象名, 将变量作为该对象的属性, 将方法作为该对象的方法, 这样就能大大减少全局变量的个数
- // 开发者 A 写了一大段 js 代码
- let devA = {
- addNumber() { }
- }
- // 开发者 B 开始写 js 代码
- let devB = {
- add: ''
- }
- //A 重新维护该 js 代码
- devA.addNumber();
上面代码中, devA 和 devB 就是两个命名空间, 采用命名空间可以有效减少全局变量的数量, 以此解决变量冲突的发生
2.2 管理模块
上面说到的 timeTool 对象是一个只用来处理时间的工具库, 但是实际开发过程中的库可能会有多种多样的功能, 例如处理 ajax 请求, 操作 dom 或者处理事件这个时候单例模式还可以用来管理代码库中的各个模块, 例如下面的代码所示
- var devA = (function() {
- //ajax 模块
- var ajax = {
- get: function(api, obj) {
- console.log('ajax get 调用')
- },
- post: function(api, obj) {}
- }
- //dom 模块
- var dom = {
- get: function() {},
- create: function() {}
- }
- //event 模块
- var event = {
- add: function() {},
- remove: function() {}
- }
- return {
- ajax: ajax,
- dom: dom,
- event: event
- }
- })()
上面的代码库中有 ajax,dom 和 event 三个模块, 用同一个命名空间 devA 来管理在进行相应操作的时候, 只需要 devA.ajax.get() 进行调用即可这样可以让库的功能更加清晰
3. ES6 中的单例模式
3.1 ES6 创建对象
ES6 中创建对象时引入了 class 和 constructor 用来创建对象下面我们来使用 ES6 的语法实例化苹果公司
- class Apple {
- constructor(name, creator, products) {
- this.name = name;
- this.creator = creator;
- this.products = products;
- }
- }
- let appleCompany = new Apple('苹果公司', '乔布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
- let copyApple = new Apple('苹果公司', '阿辉', ['iPhone', 'iMac', 'iPad', 'iPod']);
3.2 ES6 中创建单例模式
苹果这么伟大的公司明显有且只有一个, 就是乔爷爷创建的那个, 哪能容别人进行复制? 所以 appleCompany 应该是一个单例, 现在我们使用 ES6 的语法将 constructor 改写为单例模式的构造器
- class SingletonApple {
- constructor(name, creator, products) {
- // 首次使用构造器实例
- if (!SingletonApple.instance) {
- this.name = name;
- this.creator = creator;
- this.products = products;
- // 将 this 挂载到 SingletonApple 这个类的 instance 属性上
- SingletonApple.instance = this;
- }
- return SingletonApple.instance;
- }
- }
- let appleCompany = new SingletonApple('苹果公司', '乔布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
- let copyApple = new SingletonApple('苹果公司', '阿辉', ['iPhone', 'iMac', 'iPad', 'iPod']);
- console.log(appleCompany === copyApple); //true
3.3 ES6 的静态方法优化代码
ES6 中提供了为 class 提供了 static 关键字定义静态方法, 我们可以将 constructor 中判断是否实例化的逻辑放入一个静态方法 getInstance 中, 调用该静态方法获取实例, constructor 中只包需含实例化所需的代码, 这样能增强代码的可读性结构更加优化
- class SingletonApple {
- constructor(name, creator, products) {
- this.name = name;
- this.creator = creator;
- this.products = products;
- }
- // 静态方法
- static getInstance(name, creator, products) {
- if(!this.instance) {
- this.instance = new SingletonApple(name, creator, products);
- }
- return this.instance;
- }
- }
- let appleCompany = SingletonApple.getInstance('苹果公司', '乔布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
- let copyApple = SingletonApple.getInstance('苹果公司', '阿辉', ['iPhone', 'iMac', 'iPad', 'iPod'])
- console.log(appleCompany === copyApple); //true
4. 单例模式的项目实战应用
4.1 实现登陆弹框
登陆弹框在项目中是一个比较经典的单例模式, 因为对于大部分网站不需要用户必须登陆才能浏览, 所以登陆操作的弹框可以在用户点击登陆按钮后再进行创建而且登陆框永远只有一个, 不会出现多个登陆弹框的情况, 也就意味着再次点击登陆按钮后返回的永远是一个登录框的实例
现在来梳理一下我登陆弹框的流程, 在来进行代码的实现:
给顶部导航模块的登陆按钮注册点击事件
登陆按钮点击后 JS 动态创建遮罩层和登陆弹框
遮罩层和登陆弹框插入到页面中
给登陆框中的关闭按钮注册事件, 用于关闭遮罩层和弹框
给登陆框中的输入框添加校验 (此步骤略)
给登陆框中的确定按钮添加事件, 用于 Ajax 请求 (此步骤略)
给登陆框中的清空按钮添加事件, 用于清空输入框 (此步骤略)
因为 5,6 是登陆框的实际项目逻辑, 和单例模式关系不大下面的项目实战代码只实现 1 - 4 步, 其余步骤读者可自行进行扩展练习完整的代码可在 CodePen 中进行查看
4.1.1 给页面添加顶部导航栏的 html 代码
- <nav class="top-bar">
- <div class="top-bar_left">
- LTH BLOG
- </div>
- <div class="top-bar_right">
- <div class="login-btn"> 登陆 </div>
- <div class="signin-btn"> 注册 </div>
- </div>
- </nav>
4.1.2 使用 ES6 的语法创建 Login 类
- class Login {
- // 构造器
- constructor() {
- this.init();
- }
- // 初始化方法
- init() {
- // 新建 div
- let mask = document.createElement('div');
- // 添加样式
- mask.classList.add('mask-layer');
- // 添加模板字符串
- mask.innerHTML =
- `
- <div class="login-wrapper">
- <div class="login-title">
- <div class="title-text"> 登录框 </div>
- <div class="close-btn">×</div>
- </div>
- <div class="username-input user-input">
- <span class="login-text"> 用户名:</span>
- <input type="text">
- </div>
- <div class="pwd-input user-input">
- <span class="login-text"> 密码:</span>
- <input type="password">
- </div>
- <div class="btn-wrapper">
- <button class="confrim-btn"> 确定 </button>
- <button class="clear-btn"> 清空 </button>
- </div>
- </div>
- `;
- // 插入元素
- document.body.insertBefore(mask, document.body.childNodes[0]);
- // 注册关闭登录框事件
- Login.addCloseLoginEvent();
- }
- // 静态方法: 获取元素
- static getLoginDom(cls) {
- return document.querySelector(cls);
- }
- // 静态方法: 注册关闭登录框事件
- static addCloseLoginEvent() {
- this.getLoginDom('.close-btn').addEventListener('click', () => {
- // 给遮罩层添加 style, 用于隐藏遮罩层
- this.getLoginDom('.mask-layer').style = "display: none";
- })
- }
- // 静态方法: 获取实例 (单例)
- static getInstance() {
- if(!this.instance) {
- this.instance = new Login();
- } else {
- // 移除遮罩层 style, 用于显示遮罩层
- this.getLoginDom('.mask-layer').removeAttribute('style');
- }
- return this.instance;
- }
- }
4.1.3 给登陆按钮添加注册点击事件
- // 注册点击事件
- Login.getLoginDom('.login-btn').addEventListener('click', () => {
- Login.getInstance();
- })
4.1.4 效果演示
单例效果展示. gif-36.8kB
完整的项目代码见: CodePen(单例模式案例登录框)
上面的登陆框的实现中, 我们只创建了一个 Login 的类, 但是却实现了一个并不简单的登陆功能在第一次点击登陆按钮的时候, 我们调用 Login.getInstance() 实例化了一个登陆框, 且在之后的点击中, 并没有重新创建新的登陆框, 只是移除掉了 "display: none" 这个样式来显示登陆框, 节省了内存开销
总结
单例模式虽然简单, 但是在项目中的应用场景却是相当多的, 单例模式的核心是确保只有一个实例, 并提供全局访问就像我们只需要一个浏览器的 window 对象, jQuery 的 $ 对象而不再需要第二个 由于 JavaScript 代码书写方式十分灵活, 这也导致了如果没有严格的规范的情况下, 大型的项目中 JavaScript 不利于多人协同开发, 使用单例模式进行命名空间, 管理模块是一个很好的开发习惯, 能够有效的解决协同开发变量冲突的问题灵活使用单例模式, 也能够减少不必要的内存开销, 提高用于体验
来源: http://www.jianshu.com/p/5386936acfec