公司项目因为不是一手项目,近期正好有页面和需求的改版,所以想着优化一些页面。看到很多的if...else...语句,所以思考准备如何用设计模式优化下。
前言
Javascript和其他语言一样,同样拥有着很多种设计模式,比如单例模式、代理模式、观察者模式,策略模式等,熟练运用设计模式可以使我们的代码逻辑更加清晰,并且更加易于维护和重构。
本文主要介绍JS的单例模式,涉及知识点较多,包括闭包,作用域链,立即执行函数等(如果有误欢迎指出)
正文
单例模式定义
单例模式也叫单体模式,核心思想是确保一个类只对应一个实例。虽然JS是弱类型的语言,但是同样也有类的概念。在ES5中通过构造函数模式、原型模式、组合模式等创建类,ES6可以通过Class关键字创建类。
实现一个单例模式
实现单例模式不简略说下JS的数值类型分为基本类型(Undefined、Null、Boolean、Number、String、)和引用类型(Object、Fucntion、Array)。也就是说基本类型存储在栈内存中,引用类型存储在堆内存中,并在栈中存储指向堆中内容的指针。
所以在JS中实现单例模式核心思路是:每次调用构造函数时,返回指向同一个对象的指针。 也就是说,我们只在第一次调用构造函数时创建新对象,之后调用返回时返回该对象即可。所以重点变成了如何缓存初次创建的变量对象。
当然全局变量首先被排除掉。因为全局变量容易被修改和污染,产生一些意料之外的情况。所以决定采用以下方法来缓存创建的对象。
1.构造函数
//ES5实现
function President(name) {
// 如果已存在对应的实例
if (typeof President.onlyPresident === 'object') {
return President.onlyPresident
}
//否则正常创建实例
this.name = name
// 缓存
President.onlyPresident = this
return this
}
var president1 = new President("奥巴马")
var president2 = new President("特朗普")
console.log(president1)
console.log(president2)
//ES6实现
class President {
constructor(name) {
if (!President.onlyPresident) {
this.name = name
President.onlyPresident = this
}
return President.onlyPresident
}
}
var president3 = new President("奥巴马")
var president4 = new President("特朗普")
console.log(president3)
console.log(president4)
可以看下两次打印的内容,在两个实例的构造函数中,都分别有一个onlyPresident的属性,打印出来都是第一次实例化对象所缓存的属性。
注意: 这种方法有一个缺点就是将静态属性暴露到了外面,可以被人为重写。
//ES6修改静态属性
class President {
constructor(name) {
if (!President.onlyPresident) {
this.name = name
President.onlyPresident = this
}
return President.onlyPresident
}
}
var president3 = new President("奥巴马")
var president4 = new President("特朗普")
president4.constructor.onlyPresident = "小布什"//在此修改构造函数的静态属性
console.log(president3)
console.log(president4)
可以看到打印的结果:第一次实例化应该缓存的是 ‘奥巴马’,但是打印出的缓存实例却是‘小布什’。
2.借助闭包函数
要使用闭包,首先要理解javascript的特殊的变量作用域。 变量的作用域无非就两种:全局变量和局部变量。 javascript语言的特别之处就在于:函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。 注意点:在函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明的是一个全局变量!
-
闭包函数的定义
在MDN的解释是:函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。
我的理解是:一个函数A返回函数B,函数B返回函数A中的变量,也可以说闭包是函数内部和函数外部连接起来的桥梁。
下面是个简单的闭包例子:
var Outer = function () { var countX = 0; return function () { console.log("这是闭包的块级作用域") return countX } } console.log(countX) //Uncaught ReferenceError: countX is not defined let x = Outer()() console.log(x) // 0
上面例子可以看出,匿名函数中变量countX在外部是无法访问的。但是通过匿名函数中嵌套一个函数并返回出来就可以在外面接收 这个变量。这样就可以起到函数内部和外面联通的桥梁的作用。也起到了隔离函数作用域,防止变量污染。
-
根据闭包实现一个单例
function President(name) {
this.name = name
President.onlyPresident = this
President = function () {
return President.onlyPresident
}
}
President.prototype.getName=function(){
console.log(this.name)
}
var president1 = new President("奥巴马")
var president2 = new President("特朗普")
var president3 = new President("小布什")
console.log(president1) //President {name: "奥巴马"}
president1.getName() // 奥巴马
console.log(president2) //President {}
president2.getName() // Uncaught TypeError: president2.getName is not a function
console.log(president3) //President {}
通过闭包的方式来实现的核心思路是,当对象第一次被创建以后,重写构造函数,在重写后的构造函数里面访问私有变量。 但是这么做有一个缺陷就是:在第二次new一个实例的时候,因为构造函数已经被重写,所以之前添加的getName方法就会丢失,当调用president2.getName()的时候就会报错。下面可以通过调试器看到参数的引用情况:
可以看到第一次重写构造函数的时候,president1的构造函数还包含了onlyPresident属性,当第二次实例化的时候,President构造函数实际已经变为了return President.onlyPresident这样的新函数。可以做个简单测测试 console.log(president1.constructor === President) //false
console.log(president2.constructor === President) //true
- 优化闭包单例
既然知道了上面的问题出现在哪里,那就可以优化解决问题,优化他们原型实例之间的关系
function President(name) {
this.name = name
var instance = this
President = function () {
// President.prototype = this
return instance
}
// 当第一次修改完构造函数后,重新更改构造函数的原型链
President.prototype = this
//第二种写法,直接指向旧的原型
President.prototype = this.constructor.prototype
instance = new President()
return instance
}
President.prototype.getName = function () {
console.log(this.name)
}
var president1 = new President("奥巴马")
var president2 = new President("特朗普")
var president3 = new President("小布什")
president1.getName() //奥巴马
president2.getName() //奥巴马
president3.getName() //奥巴马
这是第一种写法更改原型链后,在调试器中看到的构造函数的形式,可以发现President的原型函数包含整个Presidnet构造函数
这是第二种写法更改原型链后,在调试器中看到的构造函数的形式,可以发现President的原型函数就是旧的原型3.借助立即执行函数
通过闭包和立即执行函数结合,来保存私有变量。立即执行函数只是函数的一种调用方式,只是声明完之后立即执行,这类函数一般都只是调用一次(可用于单例对象上),调用完之后会立即销毁,不会占用内存。但是和闭包结合使用后,但因为被引用的内部变量不能被销毁,所以会常驻内存。每次访问的时候都要校验是否存在实例,防止重复创建实例。
下面是一个立即执行函数和闭包结合的单例的例子:
//第一步
function President(name) {
this.presidentName = name;
}
President.prototype.getName = function () {
return this.presidentName
};
//第二步
var SinglePrisident = (function () {
var instance = null;
return function (name) {
if (!instance) {
instance = new President(name);
}
return instance;
}
})();
第三步
var prisident1 = SinglePrisident('奥巴马').getName()
var prisident2 = SinglePrisident('特朗普').getName()
var prisident3 = SinglePrisident('小布什').getName()
//第四步
console.log(prisident1)
console.log(prisident2)
console.log(prisident3)
为了方便描述把代码分为四步走。 当代码第一次自上而下执行完第一步到第二步SinglePrisident,声明完函数分配完内存空间,会立即执行函数里面的内容,创建函数作用域,使变量manager为函数内部变量。可以看下调试器,在执行到第二步的时候各参数的情况。
可以看到,这时候立即执行函数已经在global中存在了。而且在它的[[Scopes]]中还包含了一个闭包。其他的参数还是undefined。
当执行完第三步中的var prisident1 = onlyPrisident('奥巴马').getName()的时候,这时候,instance还为null值,然后创建新的Prisident实例并被保存在闭包中,并通过return返回出来,被变量prisident1接收到。
但是这时候同时变量SinglePrisident也已经变为一个新的函数(暂时称为函数A)就是里面所return的函数,下面就是SinglePrisident的新函数
function (name) {
if (!instance) {
instance = new President(name);
}
return instance;
}
所以当运行第二次var prisident2 = SinglePrisident('特朗普').getName()的时候,在函数SinglePrisident内部,并没有声明新的var instance = null;而是通过新的返回函数(上文中的函数A)来取到闭包中instance的值,这时候闭包中instance已经缓存了第一次创建的实例。所以第二次仍返回第一次实例化的对象。
根据调试的情况可以看出,SinglePrisident已经传入的参数 '特朗普' ,但是判断到instance已经存在实例,所以仍是返回第一次所缓存的实例,所以prisident2的打印仍是**'奥巴马'**当然第三次运行结果和第二次一样,在这里不再叙述。有兴趣的小伙伴可以把代码复制下来,自己debugger下看看。
结语
也是近期偶然对设计模式感兴趣,本来是想深入了解策略模式的,但是当我看文章对单例模式都不熟悉的时候,所以在此深入了解了js单例模式的前因后果,以及创建的方法和原理。如果对你有帮助请记得点赞~如果有不对的地方,也请指正,共同探讨和进步。