JS设计模式单例模式

76次阅读

共计 3771 个字符,预计需要花费 10 分钟才能阅读完成。

单例模式

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。
———来自维基百科

一个很典型的应用是在点击登录按钮,弹出登录浮窗,不论点击多少次登录按钮,页面始终只会弹出一个登录浮窗。

实现单例模式

思路很简单,用一个变量记录是否已经为某个类创建过对象,如果没有则返回新建的对象,反之则返回之前创建的对象。

在构造器里记录实例:

var Singleton = function(name) {
  this.name = name;
  this.instance = null;
};

Singleton.prototype.getName = function() {console.log(this.name);
};

Singleton.getInstance = function(name) {if (!this.instance) {this.instance = new Singleton(name);
  }
  return this.instance;
};

var a = Singleton.getInstance('sean1');
var b = Singleton.getInstance('sean2');
console.log(a === b); // true

使用闭包记录实例:

var Singleton = function(name) {this.name = name;};

Singleton.prototype.getName = function() {console.log(this.name);
};

Singleton.getInstance = (function() {
  var instance = null;
  return function(name) {if (!instance) {instance = new Singleton(name);
    }
    return instance;
  };
})();
var a = Singleton.getInstance('sean1');
var b = Singleton.getInstance('sean2');
console.log(a === b); // true

以上方法相对简单,但是 Singleton 类的使用者必须知道这是一个单例类,需要调用 getInstance() 函数(而不是 new 的方式)来获取对象,这也就是增加了“不透明性”。

透明的单例模式

利用一个 IIFE 形成一个闭包,在里边通过变量 instance 来记录实例,并返回构造函数。

var CreateDiv = (function() {
  var instance;

  var 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('sean1');
var b = new CreateDiv('sean2');
console.log(a === b); // true

上面完成了一个透明的单例类的编写,但还是有缺点,增加了一些程序的复杂度,且阅读性差。还有重要的一点是违反了“单一职责原则”,接下来再改进一下。

用代理实现单例模式

其实就是将实际的业务代码与负责管理单例的代码分离,管理单例的类就是代理类。

var CreateDiv = function(html) {
  this.html = html;
  this.init();};
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 a1 = new ProxySingletonCreateDiv('sean1');
var b1 = new ProxySingletonCreateDiv('sean2');
console.log(a1 === b1); // true

// 如果想要创建多个 div 就直接调用 CreateDiv 咯

var a2 = new CreateDiv('sean1');
var b2 = new CreateDiv('sean2');
console.log(a2 === b2); // false

JavaScript 中的单例模式

前面提到的几种实现,更多的是接近传统面向对象语言中的实现,但 JavaScript 是一门无类(class-free)语言,我们只要记住单例模式但核心是 确保只有一个实例,并提供全局访问

全局变量不是单例模式,但它却满足单例的条件,所以我们经常会把全局变量当成单例来使用:var a = {};。但是我们都知道全局变量很容易造成命名空间污染、容易被不小心覆盖等问题。我们有必要尽量减少全局变量的使用,即使要,也要将污染降到最低:

  • 使用命名空间

    var namespace1 = {a: function() {return 1;},
      b: function() {return 2;}
    };
    
    // 或者动态创建命名空间:var MyApp = {};
    MyApp.namespace = function(name) {var parts = name.split('.');
      var current = MyApp;
      for (var item of parts) {if (!current[item]) {current[item] = {};}
        current = current[item];
      }
    };
    MyApp.namespace('event');
    MyApp.namespace('dom.style');
    
    console.dir(MyApp);
  • 使用闭包封装私有变量

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

惰性单例

所谓惰性,就是只有在需要的时候才会去做。前面提到的也有满足惰性的实现,不过是基于“类”的。下面来看一下文章开头提到的那个典型的应用实现:

<html>
  <body>
    <button id="loginBtn"> 登录 </button>
    <script>
      var createLoginLayer = (function() {
        var div;
        return function() {if (!div) {div = document.createElement('div');
            div.innerHTML = '我是登录浮窗';
            div.style.display = 'none';
            document.body.appendChild(div);
          }
          return div;
        };
      })();
      document.getElementById('loginBtn').onclick = function() {var loginLayer = createLoginLayer();
        loginLayer.style.display = 'block';
      };
    </script>
  </body>
</html>

上面的代码完成了惰性单例的实现,但仍然违反了单一职责原则,如果我们下次不是创建一个登录浮窗而是别的元素呢,请往后看通用的惰性单例。

通用的惰性单例

// 定义一个管理单例的函数: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';
  document.body.appendChild(div);
  return div;
}

// 获取单例的浮窗
var createSingleLoginLayer = getSingle(createLoginLayer);

document.getElementById('btn').onclick = function() {var loginLayer = createSingleLoginLayer();
  loginLayer.style.display = 'block';
};

参考

曾探. JavaScript 设计模式与开发实践 (图灵原创) (Chinese Edition)

正文完
 0

js设计模式–单例模式

77次阅读

共计 4745 个字符,预计需要花费 12 分钟才能阅读完成。

概念
单例模式的定义是: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
UML 类图

场景
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的 window 对象等。
在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。
优缺点
优点:创建对象和管理单例的职责被分布在两个不同的方法中
实现
1. 我们的第一个单例

var instance = null
var getInstance = function(arg) {
if (!instance) {
instance = arg
}
return instance
}

var a = getInstance(‘a’)
var b = getInstance(‘b’)
console.log(a===b)
这种定义一个全局变量的方式非常不优雅,也不好复用代码
2. 利用闭包实现单例

var Singleton = function(name){
this.name = name;
};

Singleton.getInstance = (function(){
var instance = null;
return function(name){
if (!instance){
instance = new Singleton(name);
}
return instance;
}
})();
var a = Singleton.getInstance(‘a’)
var b = Singleton.getInstance(‘b’)
console.log(a===b)
有些同学可能对闭包不大理解,下面用函数实现一下
3. 利用函数实现单例

function Singleton(name) {
this.name = name
this.instance = null
}

Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}

var a = Singleton.getInstance(‘a’)
var b = Singleton.getInstance(‘b’)
console.log(a===b)

2,3 这两种方式也有缺点,就是我们必须调用 getInstance 来创建对象,一般我们创建对象都是利用 new 操作符
4. 透明的单例模式
var Singleton = (function() {
var instance
Singleton = function(name) {
if (instance) return instance
this.name = name
return instance = this
}
return Singleton
})()

var a = new Singleton(‘a’)
var b = new Singleton(‘b’)
console.log(a===b)

这中方法也有点缺点:不符合单一职责原则,这个对象其实负责了两个功能:单例和创建对象
下面我们分离这两个职责
5. 利用代理实现单例
var People = function(name) {
this.name = name
}

var Singleton = (function() {
var instance
Singleton = function(name) {
if (instance) return instance
return instance = new People(name)
}
return Singleton
})()

var a = new Singleton(‘a’)
var b = new Singleton(‘b’)
console.log(a===b)

这中方法也有点缺点:代码不能复用。如果我们有另外一个对象也要利用单例模式,那我们不得不写重复的代码
6. 提供通用的单例

var People = function(name) {
this.name = name
}

var Singleton = function(Obj) {
var instance
Singleton = function() {
if (instance) return instance
return instance = new Obj(arguments)
}
return Singleton
}

var peopleSingleton = Singleton(People)

var a = new peopleSingleton(‘a’)
var b = new peopleSingleton(‘b’)
console.log(a===b)
到这里已经比较完美了,等等这只是 es5 的写法,下面我们用 es6 来实现一下
7. es6 单例模式
class People {
constructor(name) {
if (typeof People.instance === ‘object’) {
return People.instance;
}
People.instance = this;
this.name = name
return this;
}
}
var a = new People(‘a’)
var b = new People(‘b’)
console.log(a===b)
比较以上几种实现

用全局变量的第 1 种方法,应该摒弃
用闭包实现的第 2 种方式,instance 实例对象总是在我们调用 Singleton.getInstance 的时候才被创建,应该摒弃
其他方式都是惰性单例(在需要时才创建)

js 的特殊性
我们都知道:JavaScript 其实是一门无类 (class-free) 语言,,生搬单例模式的概念并无意义。
单例模式的核心是确保只有一个实例,并提供全局访问。
我们可以用一下几种方式来另类实现
1. 全局变量
比如 var a = {}, 这时全局就只有一个 a 对象但全局变量存在很多问题,它很容易造成命名空间污染,我们用以下两种方式解决
2. 使用命名空间
var namespace1 = {
a: function () {
alert(1);
},
b: function () {
alert(2);
}
};
另外我们还可以动态创建命名空间

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]];
}
};
MyApp.namespace(‘event’);
MyApp.namespace(‘dom.style’);
console.dir(MyApp);
// 上述代码等价于:
var MyApp = {
event: {},
dom: {
style: {}
}
};
3. 闭包
var user = (function () {
var __name = ‘sven’,
__age = 29;
return {
getUserInfo: function () {
return __name + ‘-‘ + __age;
}
}
})();
例子
登录框
下面我们来实现一个点击登录按钮弹出登录框的例子
粗糙的实现
<html>

<body>
<button id=”loginBtn”> 登录 </button>
</body>
<script>
var loginLayer = (function () {
var div = document.createElement(‘div’);
div.innerHTML = ‘ 我是登录浮窗 ’;
div.style.display = ‘none’;
document.body.appendChild(div);
return div;
})();
document.getElementById(‘loginBtn’).onclick = function () {

loginLayer.style.display = ‘block’;
};
</script>

</html>

上面这种方式如果用户没有点击登录按钮,也会在一开始就创建登录框
改进

<html>

<body>
<button id=”loginBtn”> 登录 </button>
</body>
<script>
var createLoginLayer = function () {
var div = document.createElement(‘div’);
div.innerHTML = ‘ 我是登录浮窗 ’;
div.style.display = ‘none’;
document.body.appendChild(div);
return div;
};
document.getElementById(‘loginBtn’).onclick = function () {
var loginLayer = createLoginLayer();
loginLayer.style.display = ‘block’;
};
</script>

</html>
这种方式每次点击按钮都会创建一个登录框
再改进

var createLoginLayer = (function () {
var div;
return function () {
if (!div) {
div = document.createElement(‘div’);
div.innerHTML = ‘ 我是登录浮窗 ’;
div.style.display = ‘none’;
document.body.appendChild(div);
}
return div;
}
})();

document.getElementById(‘loginBtn’).onclick = function () {
var loginLayer = createLoginLayer();
loginLayer.style.display = ‘block’;
};
这种方式不够通用,不符合单一职责原则
再再改进

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’;
document.body.appendChild(div);
return div;
};
var createSingleLoginLayer = getSingle(createLoginLayer);
document.getElementById(‘loginBtn’).onclick = function () {
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = ‘block’;
};

// 下面我们再试试创建唯一的 iframe 用于动态加载第三方页面:
var createSingleIframe = getSingle(function () {
var iframe = document.createElement(‘iframe’);
document.body.appendChild(iframe);
return iframe;
});
document.getElementById(‘loginBtn’).onclick = function () {
var loginLayer = createSingleIframe();
loginLayer.src = ‘http://baidu.com’;
};

至此已经完美

正文完
 0