前端单例模式理解和应用

Posted by Melissa Zhou on 2018-07-19

单例模式理解和应用

写在前面:前段时间在看Rx js时候看到观察者模式,联想到单例模式,然后自己学习了下。不看不知道,原来自己在平时写代码的过程中用的最多的就是单例模式。在项目中也有很多应用。这里做下总结。

什么是单例模式?

什么是单例模式?顾名思义,就是只有一个实例。即使多次实例化一个类,也只返回第一次的实例。这样说可能比较抽象,看看实际中最简单的单例模式:

1
2
3
4
5
6
let hmacsha256 = {
name: '哈希加密',
encrypt: function() {},
decrypt: function() {}
}
hmacsha256.name // '哈希加密'

上面字面量形式的创建对象,这个对象hmacsha256有两个方法一个变量。可以通过hmacsha256.encrypt()来调用方法。这是我们最常见的单例模式。但是这样写有个特点,就是hmacsha256的所有方法和变量都是公共的,但是如果有一些内部的辅助函数我们不希望暴露出去的话,这样的单例就无法满足我们的需求。

有私有变量的单例模式

如果像上面说的那样,不想要把所有的方法和变量都暴露出去,以免有的方法被修改,那我们可以只返回自己想要暴露的方法和变量,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var hmacsha256 = function () {

/* 私有变量和方法 */
var name = '哈希加密';
function showPrivate() {
console.log(name);
}

/* 公有变量和方法(可以访问私有变量和方法) */
return {
getValue: function () {
showPrivate();
},
encrypt: function() {},
decrypt: function() {}
};
};

var single = hmacsha256();
single.getValue(); // '哈希加密'

这样我们对外暴露了3个方法,而name值不在能通过hmacsha256.name 拿到,所以name已经变成了类的私有变量,只有通过single.getValue() 才能拿到,如果,不提供修改这个值的方法,外部就无法修改这个变量。这其实这是一个闭包的典型应用。

通过这个修改以后发我们发现,这个类在应用这个js的时候初始化一次,但是如果这个js里面的方法一直没有被用到的话,那就等于浪费了一些开销,因为一直没有用到。于是我们希望在引入的时候也不实例化,而是在真正使用的时候在实例化,这就引入了一个懒性单例的概念

懒性单例

怎么样能做到引入的时候不实例化呢?我们借助自执行函数来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 懒性单例写法一 */
var hmacsha256 = (function () {
var instance;
/* 私有变量和方法 */
var name = '哈希加密';
function showPrivate() {
console.log(name);
}
function init(option) {
/*这里定义单例代码*/
return {
getValue: function () {
showPrivate();
},
encrypt: function() {},
decrypt: function() {}
};
}

return {
getInstance: function (option) {
if (!instance) {
instance = init(option);
}
return instance;
}
};
})();

/*调用公有的方法来获取实例:*/
hmacsha256.getInstance();

自执行函数,我的理解是一种巧妙的方法,使得我们可以将匿名函数以函数表达式的方式进行创建,并返回匿名函数对象的引用。在结尾加上一对括号,可以调用匿名函数对象的引用,让函数立即被执行。

这里需要说明一下,() 的作用,() 的作用是迫使js解析器在解析的时候强制将括号内的表达式(expression)转化为对象,而不是作为语句(statement)来执行 。也就是说(function () {}) 这个括号中虽然有function关键字,但是由于有括号,所有解析器并没有把他当做一个function,而是强制把里面的内容转成了一个对象,并返回指向这个对象的指针。

() 在这里的作用与用Eval把json格式字符串转换为json对象 时的作用一样,这就是为啥eval("(" + testJson + ")"); 一定要多加一个括号的原因。

1
2
alert(eval("{}");  // return undefined
alert(eval("({})");// return object[Object]

事实上,上面的代码和下面的写法的效果一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 懒性单例写法二 */
function hmacsha256 () {
var instance;
/* 私有变量和方法 */
var name = '哈希加密';
function showPrivate() {
console.log(name);
}
function init(option) {
/*这里定义单例代码*/
return {
getValue: function () {
showPrivate();
},
encrypt: function() {},
decrypt: function() {}
};
}

return {
getInstance: function (option) {
if (!instance) {
instance = init(option);
}
return instance;
}
};
}

/*调用公有的方法来获取实例:*/
hmacsha256().getInstance();

但是为什么要使用自执行函数呢?答案就是隔离作用域 。第二种写法虽然功能可以实现,但是function hmacsha256这个方法随时候有可能被人改写。第一种写法中,就算你hmacsha256返回上千种方法,里面有再多的私有变量,都不影响其他的作用域。他只管hmacsha256这个变量下的东西,就像有一个命名空间一样。

单例模式解决了什么问题?

单例模式只有一个实例,节约了系统的开销。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。比如,工具类,登录框,导航,这些都是系统中单例模式的绝佳使用场景。除了能解决这种业务场景的问题,隔离作用域和模块的分割也是我们使用的最多的姿势。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var hmacsha256 = {
encrypt: function () {
},
decrypt: function () {
}
}

var getAuthorization = function (token) {
return authorization;
}

export {
hmacsha256,
getAuthorization
}

单例模式在项目实战应用

项目中有个需求是提供一个sdk,初始化以后可以生成一个顶部和右侧的导航栏。这个导航栏真个项目中只有一个,只需要一个实例,这就是典型的单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  window.mySDK = (function () {
var instance;
function initSDK (option) {
/*
判断是否登录,获取账号信息
初始化顶栏和侧边栏
调用外部传进来的init方法
*/
}
function showSd (option) {
/*控制侧边栏展开还是收起*/
}
function resetSd (option) {
/*传入新的filter方法并重置左侧导航*/
}
function constructor (option) {

initSDK(option);

return {
head: option.head,/*初始化顶部的div的id*/
side: option.side,/*初始左侧导航栏div的id*/
sdHide: option.sdHide,
showSd: showSd,
resetSd: resetSd,
init: function () {
typeof(option.init)==='function' ? option.init() : undefined
},
signOut: function () {
typeof(option.signOut)==='function' ? option.signOut() : undefined
}
}
}
return {
getInstance: function (option) {
if (!instance) {
instance = constructor(option)
}
return instance;
}
}
})()


var options = {
sdHide: false,
resetSd: function () {},
init: function () {}
}

mySDK = window.mySDK.getInstance({
head: 'top',
side: 'left',
resetSd: options.filterSd,
sdHide: options.sdHide,
init: options.init
})

这里把实现的具体的内容省去,其实在 initSDK 中做了很多的工作。但这是有关业务的内容,我们需要根据不同的业务员场景自行实现。但运用单例模式,可以保证导航栏只有一个实例。

其他参考文章:

https://zhuanlan.zhihu.com/p/34754447