为什么要封装代码?
咱们常常据说:“写代码要有良好的封装,要高内聚,低耦合”。那怎么才算良好的封装,咱们为什么要封装呢?其实封装有这样几个益处:
- 封装好的代码,外部变量不会净化内部。
- 能够作为一个模块给内部调用。内部调用者不须要晓得实现的细节,只须要依照约定的标准应用就行了。
- 对扩大凋谢,对批改敞开,即开闭准则。内部不能批改模块,既保证了模块外部的正确性,又能够留出扩大接口,应用灵便。
怎么封装代码?
JS生态曾经有很多模块了,有些模块封装得十分好,咱们应用起来很不便,比方jQuery,Vue等。如果咱们认真去看这些模块的源码,咱们会发现他们的封装都是有法则可循的。这些法则总结起来就是设计模式,用于代码封装的设计模式次要有工厂模式
,创建者模式
,单例模式
,原型模式
四种。上面咱们联合一些框架源码来看看这四种设计模式:
工厂模式
工厂模式的名字就很直白,封装的模块就像一个工厂一样批量的产出须要的对象。常见工厂模式的一个特色就是调用的时候不须要应用new
,而且传入的参数比较简单。然而调用次数可能比拟频繁,常常须要产出不同的对象,频繁调用时不必new
也不便很多。一个工厂模式的代码构造如下所示:
function factory(type) {
switch(type) {
case 'type1':
return new Type1();
case 'type2':
return new Type2();
case 'type3':
return new Type3();
}
}
上述代码中,咱们传入了type
,而后工厂依据不同的type
来创立不同的对象。
实例: 弹窗组件
上面来看看用工厂模式的例子,如果咱们有如下需要:
咱们我的项目须要一个弹窗,弹窗有几种:音讯型弹窗,确认型弹窗,勾销型弹窗,他们的色彩和内容可能是不一样的。
针对这几种弹窗,咱们先来别离建一个类:
function infoPopup(content, color) {}
function confirmPopup(content, color) {}
function cancelPopup(content, color) {}
如果咱们间接应用这几个类,就是这样的:
let infoPopup1 = new infoPopup(content, color);
let infoPopup2 = new infoPopup(content, color);
let confirmPopup1 = new confirmPopup(content, color);
...
每次用的时候都要去new
对应的弹窗类,咱们用工厂模式革新下,就是这样:
// 新加一个办法popup把这几个类都包装起来
function popup(type, content, color) {
switch(type) {
case 'infoPopup':
return new infoPopup(content, color);
case 'confirmPopup':
return new confirmPopup(content, color);
case 'cancelPopup':
return new cancelPopup(content, color);
}
}
而后咱们应用popup
就不必new
了,间接调用函数就行:
let infoPopup1 = popup('infoPopup', content, color);
革新成面向对象
上述代码尽管实现了工厂模式,然而switch
始终感觉不是很优雅。咱们应用面向对象革新下popup
,将它改为一个类,将不同类型的弹窗挂载在这个类上成为工厂办法:
function popup(type, content, color) {
// 如果是通过new调用的,返回对应类型的弹窗
if(this instanceof popup) {
return new this[type](content, color);
} else {
// 如果不是new调用的,应用new调用,会走到下面那行代码
return new popup(type, content, color);
}
}
// 各种类型的弹窗全副挂载在原型上成为实例办法
popup.prototype.infoPopup = function(content, color) {}
popup.prototype.confirmPopup = function(content, color) {}
popup.prototype.cancelPopup = function(content, color) {}
封装成模块
这个popup
不仅仅让咱们调用的时候少了一个new
,他其实还把相干的各种弹窗都封装在了外面,这个popup
能够间接作为模块export
进来给他人调用,也能够挂载在window
上作为一个模块给他人调用。因为popup
封装了弹窗的各种细节,即便当前popup
外部改了,或者新增了弹窗类型,或者弹窗类的名字变了,只有保障对外的接口参数不变,对里面都没有影响。挂载在window
上作为模块能够应用自执行函数:
(function(){
function popup(type, content, color) {
if(this instanceof popup) {
return new this[type](content, color);
} else {
return new popup(type, content, color);
}
}
popup.prototype.infoPopup = function(content, color) {}
popup.prototype.confirmPopup = function(content, color) {}
popup.prototype.cancelPopup = function(content, color) {}
window.popup = popup;
})()
// 里面就间接能够应用popup模块了
let infoPopup1 = popup('infoPopup', content, color);
jQuery的工厂模式
jQuery也是一个典型的工厂模式,你给他一个参数,他就给你返回合乎参数DOM对象。那jQuery这种不必new
的工厂模式是怎么实现的呢?其实就是jQuery外部帮你调用了new
而已,jQuery的调用流程简化了就是这样:
(function(){
var jQuery = function(selector) {
return new jQuery.fn.init(selector); // new一下init, init才是真正的构造函数
}
jQuery.fn = jQuery.prototype; // jQuery.fn就是jQuery.prototype的简写
jQuery.fn.init = function(selector) {
// 这外面实现真正的构造函数
}
// 让init和jQuery的原型指向同一个对象,便于挂载实例办法
jQuery.fn.init.prototype = jQuery.fn;
// 最初将jQuery挂载到window上
window.$ = window.jQuery = jQuery;
})();
上述代码构造来自于jQuery源码,从中能够看出,你调用时省略的new
在jQuery外面帮你调用了,目标是为了使大量调用更不便。然而这种构造须要借助一个init
办法,最初还要将jQuery
和init
的原型绑在一起,其实还有一种更加简便的办法能够实现这个需要:
var jQuery = function(selector) {
if(!(this instanceof jQuery)) {
return new jQuery(selector);
}
// 上面进行真正构造函数的执行
}
上述代码就简洁多了,也能够实现不必new
间接调用,这里利用的个性是this
在函数被new
调用时,指向的是new
进去的对象,new
进去的对象天然是类的instance
,这里的this instanceof jQuery
就是true
。如果是一般调用,他就是false
,咱们就帮他new
一下。
建造者模式
建造者模式是用于比较复杂的大对象的构建,比方Vue
,Vue
外部蕴含一个功能强大,逻辑简单的对象,在构建的时候也须要传很多参数进去。像这种须要创立的状况不多,创立的对象自身又很简单的时候就实用建造者模式。建造者模式的个别构造如下:
function Model1() {} // 模块1
function Model2() {} // 模块2
// 最终应用的类
function Final() {
this.model1 = new Model1();
this.model2 = new Model2();
}
// 应用时
var obj = new Final();
上述代码中咱们最终应用的是Final
,然而Final
外面的构造比较复杂,有很多个子模块,Final
就是将这些子模块组合起来实现性能,这种须要精细化结构的就实用于建造者模式。
实例:编辑器插件
假如咱们有这样一个需要:
写一个编辑器插件,初始化的时候须要配置大量参数,而且外部的性能很多很简单,能够扭转字体色彩和大小,也能够后退后退。
个别一个页面就只有一个编辑器,而且外面的性能可能很简单,可能须要调整色彩,字体等。也就是说这个插件外部可能还会调用其余类,而后将他们组合起来实现性能,这就适宜建造者模式。咱们来剖析下做这样一个编辑器须要哪些模块:
- 编辑器自身必定须要一个类,是给内部调用的接口
- 须要一个控制参数初始化和页面渲染的类
- 须要一个管制字体的类
- 须要一个状态治理的类
// 编辑器自身,对外裸露
function Editor() {
// 编辑器外面就是将各个模块组合起来实现性能
this.initer = new HtmlInit();
this.fontController = new FontController();
this.stateController = new StateController(this.fontController);
}
// 初始化参数,渲染页面
function HtmlInit() {
}
HtmlInit.prototype.initStyle = function() {} // 初始化款式
HtmlInit.prototype.renderDom = function() {} // 渲染DOM
// 字体控制器
function FontController() {
}
FontController.prototype.changeFontColor = function() {} // 扭转字体色彩
FontController.prototype.changeFontSize = function() {} // 扭转字体大小
// 状态控制器
function StateController(fontController) {
this.states = []; // 一个数组,存储所有状态
this.currentState = 0; // 一个指针,指向以后状态
this.fontController = fontController; // 将字体管理器注入,便于扭转状态的时候扭转字体
}
StateController.prototype.saveState = function() {} // 保留状态
StateController.prototype.backState = function() {} // 后退状态
StateController.prototype.forwardState = function() {} // 后退状态
下面的代码其实就将一个编辑器插件的架子搭起来了,具体实现性能就是往这些办法外面填入具体的内容就行了,其实就是各个模块的互相调用,比方咱们要实现后退状态的性能就能够这样写:
StateController.prototype.backState = function() {
var state = this.states[this.currentState - 1]; // 取出上一个状态
this.fontController.changeFontColor(state.color); // 改回上次色彩
this.fontController.changeFontSize(state.size); // 改回上次大小
}
单例模式
单例模式实用于全局只能有一个实例对象的场景,单例模式的个别构造如下:
function Singleton() {}
Singleton.getInstance = function() {
if(this.instance) {
return this.instance;
}
this.instance = new Singleton();
return this.instance;
}
上述代码中,Singleton
类挂载了一个静态方法getInstance
,如果要获取实例对象只能通过这个办法拿,这个办法会检测是不是有现存的实例对象,如果有就返回,没有就新建一个。
实例:全局数据存储对象
如果咱们当初有这样一个需要:
咱们须要对一个全局的数据对象进行治理,这个对象只能有一个,如果有多个会导致数据不同步。
这个需要要求全局只有一个数据存储对象,是典型的适宜单例模式的场景,咱们能够间接套用下面的代码模板,然而下面的代码模板获取instance
必须要调getInstance
才行,要是某个使用者间接调了Singleton()
或者new Singleton()
就会出问题,这次咱们换一种写法,让他可能兼容Singleton()
和new Singleton()
,应用起来更加傻瓜化:
function store() {
if(store.instance) {
return store.instance;
}
store.instance = this;
}
上述代码反对应用new store()
的形式调用,咱们应用了一个动态变量instance
来记录是否有进行过实例化,如果实例化了就返回这个实例,如果没有实例化阐明是第一次调用,那就把this
赋给这个这个动态变量,因为是应用new
调用,这时候的this
指向的就是实例化进去的对象,并且最初会隐式的返回this
。
如果咱们还想反对store()
间接调用,咱们能够用后面工厂模式用过的办法,检测this
是不是以后类的实例,如果不是就帮他用new
调用就行了:
function store() {
// 加一个instanceof检测
if(!(this instanceof store)) {
return new store();
}
// 上面跟后面一样的
if(store.instance) {
return store.instance;
}
store.instance = this;
}
而后咱们用两种形式调用来检测下:
实例:vue-router
vue-router
其实也用到了单例模式,因为如果一个页面有多个路由对象,可能造成状态的抵触,vue-router
的单例实现形式又有点不一样,下列代码来自vue-router
源码:
let _Vue;
function install(Vue) {
if (install.installed && _Vue === Vue) return;
install.installed = true
_Vue = Vue
}
每次咱们调用vue.use(vueRouter)
的时候其实都会去执行vue-router
模块的install
办法,如果用户不小心屡次调用了vue.use(vueRouter)
就会造成install
的屡次执行,从而产生不对的后果。vue-router
的install
在第一次执行时,将installed
属性写成了true
,并且记录了以后的Vue
,这样前面在同一个Vue
外面再次执行install
就会间接return
了,这也是一种单例模式。
能够看到咱们这里三种代码都是单例模式,他们尽管模式不一样,然而核心思想都是一样的,都是用一个变量来标记代码是否曾经执行过了,如果执行过了就返回上次的执行后果,这样就保障了屡次调用也会拿到一样的后果。
原型模式
原型模式最典型的利用就是JS自身啊,JS的原型链就是原型模式。JS中能够应用Object.create
指定一个对象作为原型来创建对象:
const obj = {
x: 1,
func: () => {}
}
// 以obj为原型创立一个新对象
const newObj = Object.create(obj);
console.log(newObj.__proto__ === obj); // true
console.log(newObj.x); // 1
上述代码咱们将obj
作为原型,而后用Object.create
创立的新对象都会领有这个对象上的属性和办法,这其实就算是一种原型模式。还有JS的面向对象其实更加是这种模式的体现,比方JS的继承能够这样写:
function Parent() {
this.parentAge = 50;
}
function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child; // 留神重置constructor
const obj = new Child();
console.log(obj.parentAge); // 50
这里的继承其实就是让子类Child.prototype.__proto__
的指向父类的prototype
,从而获取父类的办法和属性。JS中面向对象的内容较多,我这里不开展了,有一篇文章专门讲这个问题。
总结
- 很多用起来棘手的开源库都有良好的封装,封装能够将外部环境和外部环境隔离,内部用起来更棘手。
- 针对不同的场景能够有不同的封装计划。
- 须要大量产生相似实例的组件能够思考用工厂模式来封装。
- 外部逻辑较简单,内部应用时须要的实例也不多,能够思考用建造者模式来封装。
- 全局只能有一个实例的须要用单例模式来封装。
- 新老对象之间可能有继承关系的能够思考用原型模式来封装,JS自身就是一个典型的原型模式。
- 应用设计模式时不要生吞活剥代码模板,更重要的是把握思维,同一个模式在不同的场景能够有不同的实现计划。
感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。
本文次要素材来自于网易高级前端开发工程师微业余唐磊老师的设计模式视频课程。
作者博文GitHub我的项目地址: https://github.com/dennis-jiang/Front-End-Knowledges
发表回复