前端常见设计模式之单例模式
单例模式的定义是产生一个类的唯一实例,但 js 本身是一种“无类”语言。很多讲 js 设计模式的文章把{}当成一个单例来使用也勉强说得通。因为 js 生成对象的方式有很多种,我们来看下另一种更有意义的单例。 有这样一个常见的需求,点击某个按钮的时候需要在页面弹出一个遮罩层。比如 web.qq.com 点击登录的时候.
这个生成灰色背景遮罩层的代码是很好写的.
var createMask = function() {
return document.body.appendChild(document.createElement(div));
}
$('button').click(function() {
Var mask = createMask();
mask.show();
})
问题是, 这个遮罩层是全局唯一的, 那么每次调用 createMask 都会创建一个新的 div, 虽然可以在隐藏遮罩层的把它 remove 掉. 但显然这样做不合理. 再看下第二种方案, 在页面的一开始就创建好这个 div. 然后用一个变量引用它.
var mask = document.body.appendChild(document.createElement('div'))
$('button').click( function(){
mask.show();
})
这样确实在页面只会创建一个遮罩层 div, 但是另外一个问题随之而来, 也许我们永远都不需要这个遮罩层, 那又浪费掉一个 div, 对 dom 节点的任何操作都应该非常吝啬. 如果可以借助一个变量. 来判断是否已经创建过 div 呢?
var mask;
var createMask = function() {
if (mask) return mask;
else {
mask = document.body.appendChild(document.createElement(div));
return mask;
}
}
看起来不错, 到这里的确完成了一个产生单例对象的函数. 我们再仔细看这段代码有什么不妥. 首先这个函数是存在一定副作用的, 函数体内改变了外界变量 mask 的引用, 在多人协作的项目中, createMask 是个不安全的函数. 另一方面, mask 这个全局变量并不是非需不可. 再来改进一下.
var createMask = function() {
var mask;
return function() {
return mask || (mask = document.body.appendChild(document.createElement('div')))
}
}()
用了个简单的闭包把变量 mask 包起来, 至少对于 createMask 函数来讲, 它是封闭的.
再回来正题, 前面那个单例还是有缺点. 它只能用于创建遮罩层. 假如我又需要写一个函数, 用来创建一个唯一的 xhr 对象呢? 能不能找到一个通用的 singleton 包装器.js 中函数是第一公民, 意味着函数也可以当参数传递. 看看最终的代码.
var singleton = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments));
}
}
var createMask = singleton(function() {
return document.body.appendChild(document.createElement('div'));
})
用一个变量来保存第一次的返回值, 如果它已经被赋值过, 那么在以后的调用中优先返回该变量. 而真正创建遮罩层的代码是通过回调函数的方式传入到 singleton 包装器中的. 这种方式其实叫桥接模式. 关于桥接模式, 放在后面一点点来说. 然而 singleton 函数也不是完美的, 它始终还是需要一个变量 result 来寄存 div 的引用. 遗憾的是 js 的函数式特性还不足以完全的消除声明和语句.
将单例与音视频结合起来
function singletonAudio = (function () {
class Audio {
constructor(options) {
if (!options.src) throw new Error('播放地址不允许为空');
this.audioNode = document.createElement('audio');
this.audioNode.src = options.src;
}
play(playOptions) {
}
// 其他对单个音频的控制逻辑...
}
let audio;
const _static = {
getInstance(options) {
// 若 audio 实例还未被创建,则创建并返回
if (audio === undefined) {
audio = new Audio(options);
}
return audio;
}
};
return _static;
})();
我们才用了一个 iife 来构造闭包, 仅返回了一个 _static 对象, 该对象提供了 getinstance 方法, 封装和创建、获取的步骤.以便获取全局只存在一个 audio 实例. 我们可以注意到音频实例并没有直接暴露给使用者,而是通过一个公有方法 getInstance 让使用者创建、获取音频实例。这么做的目的是禁止使用者主动实例化 Audio,在公共组件的层面上保证全局只存在一个 audio 实例。
类仅允许有一个实例,且该实例在用户侧有一个访问点。
实例必须能通过子类的形式进行扩展,且用户侧能在不修改代码的前提下使用该扩展实例。
单例模式的确会返回具有单例性质的结构,但单例这一性质体现在这些结构上,单例模式本身完全可以返回多个具有单例性质的对象(这是结构的一种)
实现音轨这个功能, 我们定义 tracks 类
class Tracks {
constrcutor() {
this.tracks = {};
}
set(key, options) {
this.tracks[key] = singletonAudio.getInstance(key, options);
}
get(key) {
return this.tracks[key];
}
// 所有音轨音量调节
volumeUp(options) {
// 这里的 options 直接原样传入了,实际情况下可能会对 options 作额外的处理
// 例如,我们想调节所有音轨的整体音量,options 传入 overallVolume
// 综合考虑所有 audio 的音量,给每个 audio 的 volumeUp 方法传入合适的参数
Object.keys(this.tracks).forEach((key) => {
const audio = this.tracks[key];
audio.volumeUp(options);
});
}
}
纯单例模式
function Singleton(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("a");
var b = Singleton.getInstance("b");
// 进行比较
console.log(a === b);
或者采用闭包的方式
function Singleton(name) {
this.name = name;
}
Singleton.prototype.getName = function () {
console.log(this.name);
};
Singleton.getInstance = (function () {
var instance;
return function (name) {
if (!instance) {
this.instance = new Singleton(name);
}
return this.instance;
};
})();
// 获取对象1
var a = Singleton.getInstance("a");
// 获取对象2
var b = Singleton.getInstance("b");
// 进行比较
console.log(a === b);
以上两种方法都不太透明, 我们需要通过 Singleton.getInstance() 对象, 不知道的需要研究代码的实现 与我们常见的用 new 关键字来获取对象有出入,实际意义不大。
// 单例构造函数
function CreateSingleton(name) {
this.name = name;
this.getName();
}
// 获取实例的名字
CreateSingleton.prototype.getName = function () {
console.log(this.name);
};
// 单例对象
var Singleton = (function () {
var instance;
return function (name) {
if (!instance) {
instance = new CreateSingleton(name);
}
return instance;
};
})();
// 创建实例对象1
var a = new Singleton("a");
// 创建实例对象2
var b = new Singleton("b");
console.log(a === b);
我们通常是采用这种方式。
JavaScript 单例模式
而在开发中我们避免全局变量污染的通常做法如下:
全局命名空间
使用闭包
它们的共同点是都可以定义自己的成员、存储数据。区别是全局命名空间的所有方法和属性都是公共的,而闭包可以实现方法和属性的私有化。
惰性单例模式
而惰性单例模式的原理也是这样的,只有当触发创建实例对象时,实例对象才会被创建。这样的实例对象创建方式在开发中很有必要的。
var singleton = function (fn) {
var instance;
return function () {
return instance || (instance = fn.apply(this, arguments));
};
};
最后更新于
这有帮助吗?