JavaScript设计模式之单例模式学习

156 阅读7分钟

JavaScript设计模式之单例模式

本文由我们团队[肖建朋]总结

学习资料主要来源于《JavaScript设计模式与开发实践》一书

JavaScript基础知识简单介绍

​ JavaScript 是一种动态类型语言,当我们对一个变量赋值时,不需要考虑它的类型。动态类型一个原则:【面向接口编程,而不是面向实现编程】

​ JavaScript 没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象
与对象之间的继承。JavaScript 也没有在语言层面提供对抽象类和接口的支持

this

this的指向

  1. 作为对象的方法调用。当函数作为对象的方法被调用时,this 指向该对象。

  2. 作为普通函数调用。当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的this 总是指向全局对象。

  3. 构造器调用。JavaScript 中没有类,但是可以从构造器中创建对象,同时也提供了new 运算符,使得构造
    器看起来更像一个类。当用new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的this 就指向返回的这个对象。

  4. Function.prototype.call 或Function.prototype.apply 调用。

    跟普通的函数调用相比,用Function.prototype.call 或Function.prototype.apply 可以动态地改变传入函数的this:

call和apply的区别

apply接受两个参数,第一个参数指定了函数体对象的指向,第二个参数为一个带下apply this 标的集合,这个集合可以为数组,也可以为类数组,方法把这个集合中的元素作为参数传apply递给被调用的函数

var func = function( a, b, c ){
  alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.apply( null, [ 1, 2, 3 ] );

call传入的参数数量不固定,相同的是,第一个参数也是代表函数体内指向,call apply this
从第二个参数开始往后,每个参数被依次传入函数

var func = function( a, b, c ){
  alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.call( null, 1, 2, 3 );

闭包和高阶函数

变量的作用域

变量的作用域,就是指变量的有效范围。

我们最常谈到的是在函数中声明的变量作用域。当在函数中声明一个变量的时候,如果该变量前面没有带上关键字var,这个变量就会成为全局变量,这当然是一种容易造成命名冲突的做法。

另外一种情况是用var 关键字在函数中声明变量,这时候的变量即是局部变量,只有在该函数内部才能访问到这个变量,在函数外面是访问不到的

var a = 1;
var func1 = function(){
var b = 2;
var func2 = function(){
var c = 3;
alert ( b ); // 输出:2
alert ( a ); // 输出:1
}
func2();
alert ( c ); // 输出:Uncaught ReferenceError: c is not defined
};
func1();

变量的生存周期

对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。

而对于在函数内用var 关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了它们的价值,

它们都会随着函数调用的结束而被销毁:

代码示例:

var func = function(){
var a = 1; // 退出函数后局部变量a 将被销毁
alert ( a );
};
func();
var func = function(){
var a = 1;
return function(){
a++;
alert ( a );
}
};
var f = func();
f(); // 输出:2
f(); // 输出:3
f(); // 输出:4
f(); // 输出:5

使用闭包使for 循环中使用异步方法

<html>
<body>
<div style="width: 100; height: 100;" >1</div>
<div style="width: 100; height: 100;">2</div>
<div style="width: 100; height: 100;">3</div>
<div style="width: 100; height: 100;">4</div>
<div style="width: 100; height: 100;"> 5</div>
<script>
var nodes = document.getElementsByTagName( 'div' );
for ( var i = 0, len = nodes.length; i < len; i++ ){
((i) => {
nodes[ i ].onclick = function(){
alert ( i );
}
})(i)
}
</script>
</body>
</html>

闭包的作用

  1. 封装变量
  2. 延续局部变量的寿命

高阶函数

高阶函数至少满足以下一种条件:

  1. 函数可以作为参数传递
  2. 函数可以作为返回值输出

在后面的单例模式会有具体示例

单例模式

定义

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。要实现单例模式,要用一个变量来标志当前是否为某个类创建了对象,若是,则在下次获取该类的实例时,直接返回之前创建的对象。

类图

简单实现单例模式

<!DOCTYPE html>
<html>

<body>

  <p>
    简单的单例模式
  </p>

  <script>
    var Singleton = function (name) {
      this.name = name;
      this.instance = null;
    };
    Singleton.prototype.getName = function () {
      alert(this.name);
    };
    Singleton.getInstance = function (name) {
      if (!this.instance) {
        this.instance = new Singleton(name);
      }
      return this.instance;
    };
    var a = Singleton.getInstance('sven1');
    a.getName()
    var b = Singleton.getInstance('sven2');
    b.getName()
    alert(a === b); // true

  </script>

  <p>
  </p>

</body>

</html>

代理实现代理模式

在CreateDiv 构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建div 的类。

把负责管理单例的逻辑移到了代理类proxySingletonCreateDiv 中。

<!DOCTYPE html>
<html>

<body>

  <p>
    用代理实现单例模式
  </p>

  <script>
    // 创建Div的类
    var CreateDiv = function(html) {
      this.html = html
      this.init()
    }
    // CreateDiv的初始化方法
    CreateDiv.prototype.init = function() {
      var div = document.createElement('div')
      div.innerHTML = this.html
      document.body.appendChild(div)
    }

    // 引入代理类
    var ProxySingletonCreateDiv = (function() {
      // 这里使用了一个闭包结构
      var instance
      return function(html) {
        if(!instance) {
          instance = new CreateDiv(html)
        }
        return instance
      }
    })();
    var a = new ProxySingletonCreateDiv('sevnt1')
    console.log(a)
    var b = new ProxySingletonCreateDiv('sevnt2')
    console.log(b)
    alert(a===b)
    var c = new CreateDiv('seven3')
    console.log(c)
  </script>
</body>

</html>

###JavaScript 中的单例模式

在JavaScript 中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统的单例模式实现在JavaScript 中并不适用。单例模式的核心是确保只有一个实例,并提供全局访问。

全局变量不是单例模式,但在JavaScript中我们经常把全局变量当做单例模式来使用。例如:

var a = {} //加入a被声明在全局作用域下,则a可以在代码任何地方被使用,a就满足了单例模式的条件

但是全局变量容易造成命名污染,所以在JavaScript中要尽量少使用全局变量。

降低全局变量命名污染的方法:

  1. 使用命名空间 ,减少变量和全局作用域打交道的机会
var MyApp = {
event: {},
dom: {
style: {}
}
};

或动态创建命名空间

var MyApp = {};
MyApp.namespace = function( name ){
var parts = name.split( '.' );
var current = MyApp;
for ( var i in parts ){
if ( !current[ parts[ i ] ] ){
current[ parts[ i ] ] = {};
}
current = current[ parts[ i ] ];11
}
};
MyApp.namespace( 'event' );
MyApp.namespace( 'dom.style' );
console.dir( MyApp );
  1. 使用闭包封装变量,只暴露一些接口和外界通信。例如:

    用下划线来约定私有变量name 和age,它们被封装在闭包产生的作用域中,外部是访问不到这两个变量的,这就避免了对全局的命令污染。

    var user = (function(){
    var __name = 'sven',
    __age = 29;
    return {
    getUserInfo: function(){
    return __name + '-' + __age;
    }
    }
    })();
    

惰性单例

惰性单例指的是在需要的时候才创建对象实例。当点击按钮时显示“登录浮窗“

通用惰性单例时序图

以创建一个loginDiv为例,

第一次调用createSingleLoginLayer,依次调用getSingle、createLoginLayer成功创建loginDiv;

第二次调用createSingleLoginLayer,调用getSingle,getSingle中判断得知loginDiv已存在,于是返回已存在的loginDiv。

通用惰性单例实例 :

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Page Title</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
  <script src="main.js"></script>
</head>

<body>
  <button id="loginBtn">登录</button>
  <button id="iframeBtn">打开百度</button>
  <br>
</body>
<script>

  // 单例管理函数
  var getSingle = function(fn) {
    var result
    return function(){
      return result || (result = fn.apply(this, arguments));
    }
  }
  var createLoginLayer = function(){
    var div = document.createElement('div')
    div.innerHTML = "登录浮窗"
    div.style.display = 'none'
    div.style.backgroundColor = 'blue'
    div.style.width = '100px'
    div.style.height='100px'
    document.body.appendChild(div)
    return div
  }
  var createSingleLoginLayer = getSingle(createLoginLayer)

  var createSingleIframe = getSingle(function(){
    var iframe = document.createElement('iframe')
    iframe.style.width='100%'
    iframe.style.height='600px'
    document.body.appendChild(iframe)
    return iframe
  })

  document.getElementById( 'loginBtn' ).onclick = function(){
    var loginLayer = createSingleLoginLayer()
    loginLayer.style.display = 'block'
  }
  document.getElementById( 'iframeBtn' ).onclick = function(){
    var iframe = createSingleIframe()
    iframe.src='http://baidu.com'
  }
</script>

</html>

总结

单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。