JavaScript 面向对象之单例模式
前言
虽然单例模式看着最 low,但是我们工作中,最最最常用的,就是单例模式,单例模式也是后面高级一些模式的基础,要牢固的掌握。
描述一个独一无二的事务
假设我想在 JS 代码里描述我自己,那么我们可以下面的方式
let name = 'anbang';
let age = 18;
let sex = 'male';
console.log(`anbang age : ${age}`)
描述完我自己以后,我再描述我的一名哥们,可以按照同样的思路写
let name = 'trump';
let age = 70;
let sex = 'male';
console.log(`trump age : ${age}`)
这时候我们会发现一个问题,我和我的哥们如果在同一个代码文件里,就会冲突,比如下面这种。
//anbang
let name = 'anbang';
let age = 18;
let sex = 'male';
console.log(`anbang age : ${age}`)
//trump
let name = 'trump';
let age = 70;
let sex = 'male';
console.log(`trump age : ${age}`)
即使我们使用var
来描述,虽然不报错,但是后面会覆盖之前的;所以我们只能下面这样子写
//anbang
let anbangName = 'anbang';
let anbangAge = 18;
let anbangSex = 'male';
console.log(`anbang age : ${anbangAge}`)
//trump
let trumpName = 'trump';
let trumpAge = 70;
let trumpSex = 'male';
console.log(`trump age : ${trumpAge}`)
但是如果都这么写,我们会发现每次都是写同样的的逻辑,这些需要重复做的事情,肯定有好的解决方案,否则这门编程语言就要被淘汰了!
程序的特点:无需重复劳动,提高效率!
总结:上面的 anbangName/anbangAge/anbangSex
, 前面都是含有 anbang,都是先指定安邦以后再描述他的属性。
那么这时候我们就引入了对象的数据类型,可以通过对象来包起来;
对象描述
我们可以使用下面这种方式来描述,这也就会显得很优雅啦。
//anbang
let anbang = {
name : 'anbang',
age : 18,
sex : 'male'
}
// 使用的时候,先指定anbang,然后再指定age,这也就很符合我们的初衷了
console.log(`anbang age : ${anbang.age}`)
//trump
let trump = {
name : 'trump',
age : 70,
sex : 'male'
}
// 使用的时候,先指定我的哥们,然后再指定我哥们的age
console.log(`trump age : ${trump.age}`)
这里的anbang
不单单是变量,因为它开辟了一个新的储存空间,这种形式叫做"命名空间"。
验证变量anbang
开辟了新的储存空间:可以直接使用age
是无法条用的会发现找不到信息,必须要通过anbang.age
先找到 anbang,然后再找 age 才可以找到正确的值;这种验证就说明变量anbang
开辟了新的储存空间。
每个命名空间都是独立的,互不冲突,我在anbang
里可以写name
, 我在我的哥们trump
里也可以写name
;
扩展:维基百科上对命名空间的解释:https://zh.wikipedia.org/zh-hans/
我们这个时候,就已经写出单例模式啦,这种用一个变量接收一个对象实例的方式,就属于单例模式;
单例设计模式
下面这种就是单例模式
//anbang
let anbang = {
name : 'anbang',
age : 18,
sex : 'male'
}
console.log(`anbang age : ${anbang.age}`)
//trump
let trump = {
name : 'trump',
age : 70,
sex : 'male'
}
console.log(`trump age : ${trump.age}`)
因为anbang
和 trump
这两个对象都属于Object
这个类的实例,所以叫"单例模式",全称叫"单个实例的模式";
注意:实例和实例内的事务是不相同的!,就像我和特朗普都属于人类,我们都属于人类下的两个实例,我有一个鼻子两只手,特朗普也有一个鼻子两只手,但是你不能说我的手就是特朗普的手,虽然我们我们都有手,但是我们是不同的。
表现形式
表现形式:声明一个对象,并写入合适的值。
let anbang = {
name : 'anbang',
age : 18,
sex : 'male'
}
作用
- 描述一个独一无二的事务,并且可以对一个事务进行归类存储;
- 避免了全局变量的冲突,让使用更加优雅,也更加语义化;
真实项目中的使用
第一种方式:普通变量(用于声明)
第一种方式,就是我们目前的用法
let anbang = {
name : 'anbang',
age : 18,
sex : 'male'
}
第二种方式:函数(用于调用)
第二种就是常见的用法,用来分组和归类的,这样显得逻辑非常清晰。
let utility = {
main: function() {
console.log('执行 utility.main');
this.getInfoBaseAjax(1);
this.bindEvent();
},
bindEvent: function() {
console.log('执行 utility.bindEvent');
},
getInfoBaseAjax: function(id) {
console.log('执行 utility.getInfoBaseAjax');
},
other: function() {
console.log('执行 utility.other');
}
}
utility.main();
我见过好多人写代码是下面这种写的
垃圾代码的写法
function bindEvent() {
console.log('执行 bindEvent');
};
function getInfoBaseAjax(id) {
console.log('执行 getInfoBaseAjax');
}
function other() {
console.log('执行 other');
}
// 通过AJAX获取值
getInfoBaseAjax();
// 绑定对象
bindEvent();
这种写法的人,你问他面向对象,创建的方式之类的,也都懂,甚至基于闭包的单例模式都懂,但是真实写代码却并不会灵活运用;这是非常可耻的!!
我们要学的稳,学的踏实,还要学以致用!!!
只要稍微有点编程思想的人,看到上面两种方法,肯定会觉得单例模式的当时更佳优雅!
第三种方式:基于闭包的单例模式(常用于代码封装)
let anbang = (() => {
var fatherAssets = '有十座旷厂';
function code() {
return '安邦会写代码';
}
return {
name: 'anbang',
age: 18,
sex: 'male',
code: code
}
})()
console.log(anbang);
console.log(anbang.age);
console.log(anbang.code());
运行上面会发现,可以有
{ name: 'anbang', age: 18, sex: 'male', code: [Function: code] }
18
安邦会写代码
但是我老爹的资产fatherAssets
,那十座旷厂没有 return 出来,老爹没有告诉我,所以我也并不知道;只有里面 retrun 出来什么,我才知道有什么;
在上面的代码里,我老爹的那十座旷目前属于隐性资产;所以我现在一直在等待,也许有一天,我老爹告诉我"这些年都是在磨练我,其实我是一个富二代,家里有十座矿",哈哈。
回到主题!上面这种,通过函数运行,函数内数据与函数外没有直接关系,函数外不能直接访问函数内数据的机制就是闭包;
这种闭包内返回一个对象实例的方式就叫"基于闭包的单例模式",也有叫高级单例模式,反正就是一个称呼,知道咋回事就可以了,在封装库的时候经常用到。
单例模式的应用
let Utility = {
formatDate: function() {
console.log('formatDate');
},
formatThousands: function() {
console.log('formatThousands');
},
formatTest: function() {
console.log('formatTest');
}
}
面试题自测一下
关于基于闭包的单例模式,一定要对预解释了解的比较深入,如果你不知道自己了解的是否深入;
可以参考我以前学习 JS 笔记中的一道题目 基于闭包的单利模式-预解释面试题
题目如下,可以自己想一想,尝试能不能理解其中的原理。
var num = 20;
var obj = {
num: 30,
fn: (function (num) {
this.num *= 3;
num += 15;
var num = 45;
return function () {
this.num *= 4;
num += 20;
console.log(num);
}
})(num)
};
var fn = obj.fn;
fn();
obj.fn();
console.log(window.num, obj.num);
单利模式的擅长领域(优点)
我们通过单例模式,一般用于高度定制化,没有规律的场景,比如下面的变量。
let anbang = {
local:"Hangzhou",
github:"https://github.com/anbang",
website:"https://www.axihe.com/",
bilibili:"https://space.bilibili.com/59312814",
wechat:"yaolushan"
}
let myPhone = {
brand:"apple",
model:"iPhon Xs Max",
color :"black"
}
单利模式不适应的领域(缺点)
本文举的例子中,可以观察到,下面其实是有规律的。
//anbang
let anbang = {
name : 'anbang',
age : 18,
sex : 'male'
}
// 使用的时候,先指定anbang,然后再指定age,这也就很符合我们的初衷了
console.log(`anbang age : ${anbang.age}`)
//trump
let trump = {
name : 'trump',
age : 70,
sex : 'male'
}
// 使用的时候,先指定我的哥们,然后再指定我哥们的age
console.log(`trump age : ${trump.age}`)
如果我们要描述 10 个人,按照单利模式的思想,我们需要写很多重复代码,上面这种形式,要写 40 行的代码;
如果我们真这么写了,就肯定会被同时骂死的,这种代码属于懒婆娘的裹脚布 - 又臭又长!
程序的特点:无需重复劳动,提高效率!
联想到生活:如果个人想要找个人借钱,需要借条,直接手写一张,双方签字;假设个人向银行借钱,银行不会每次和你手写一个借条,而且做一套机制,按照机制走,就可以了;
假设这套机制是一套合同模板 + 公章的机制,那么银行打印一张空合同,填写信息,然后盖公章,个人签字,这样就可以了;
这样的机制下,无论是 10 个人借,还是 1W 个人借,效率都会很高。
这就引出了,面向对象的 - 工厂模式,下一节介绍面向对象的工厂模式