js设计模式
面向对象编程
面向对象是一种程序设计思想,与之相对的编程思想叫做面向过程。
比如我们想要用代码描绘这样一个场景,有一只叫做张三的猫,吃了一个苹果,又吃了一条鱼,然后又有一只叫做李四的猫,吃了一根香蕉。
- 面向过程
function zhangsanEatApple(){}
function zhangsanEatFish(){}
function lisiEatBanana(){}
zhangsanEatApple()
zhangsanEatFish()
lisiEatBanana()
- 面向对象
function Cat(name){
this.name = name
}
Cat.prototype.eat = function(something){}
var zhangsan = new Cat('zhangsan')
var lisi = new Cat('lisi')
zhangsan.eat('apple');
zhangsan.eat('fish');
lisi.eat('banana');
工厂模式
工厂模式是一种比较简单的设计模式,主要使用来创建对象用的
批量生产对象
function createPerson(name){
// 创建对象
var o = {};
o.name = name;
o.getName = function(){
console.log(this.name)
}
// 返回对象
return o;
}
把构造放在工厂的原型上
function Person(name){
this.name = name;
}
Person.prototype.getName = function(){
console.log(this.name)
}
function Car(model){
this.model = model;
}
Car.prototype.getModel = function(){
console.log(this.model)
}
function create(type, param){
// 后面的构造函数原型,是不是存在前面这个对象的原型链里
if(this instanceof create){
return new this[type](param);
}
// 直接调用create函数的情况
return new create(type, param)
}
create.prototype = {
person: Person,
car: Car
}
var person1 = new create('person', 'zhang san')
var car1 = create('car', 'Benz')
person1.getName();
建造者模式
- 建造者模式注重创建对象的细节,使用这种模式创建出的复杂对象或者复合对象结构会非常清晰。
比如说一家招聘中介公司,现在有一批纸质简历或者简历数据信息,为了做成统一的电子版简历,我们需要将这些信息进行整理,我们如何去做呢?
var data = [
{
name: 'zhang san',
age: 23,
work: 'engineer'
},
{
name: 'li si',
age: 33,
work: 'teacher'
},
{
name: 'wang wu',
age: 22,
work: 'xxx'
}
]
方法一:定义一个函数,对数据处理
缺点:可读性差,全部挤在一起,结构性差
function Candidate(param){
var _candidate = {}
_candidate.name = param.name
_candidate.age = param.age
_candidate.firstName = _candidate.name.split(' ')[0]
_candidate.secondName = _candidate.name.split(' ')[1]
_candidate.work = {}
switch(param.work){
case 'engineer':
_candidate.work.name = '工程师'
_candidate.work.description = '热爱编程'
break
case 'teacher':
_candidate.work.name = '老师'
_candidate.work.description = '灵魂工程师'
break
default:
_candidate.work.name = param.work
_candidate.work.description = '无'
}
// 修改工作名
_candidate.work.changeWork = function(newWork){
// work对象的name
this.name = newWork
}
// 修改工作描述
_candidate.work.changeDes = function(des){
this.description = res
}
return _candidate
}
// 调用函数
var candidateArr = []
for(let i = 0;i < data.length; i++){
candidateArr[i] = Candidate(data[i])
}
console.log(candidateArr[0])
方法二:建造者模式
function Candidate(param){
// 对类抽象和封装,代码更清晰
var _candidate = new Person(param)
_candidate.name = new Person(param)
_candidate.work = new CreateWork(param.work)
return _candidate
}
function Person(param){
this.name = param.name
this.age = param.age
}
function CreateName(name){
this.wholeName = name
this.firstName = name.split(' ')[0]
this.secondName = name.split(' ')[1]
}
function CreateWork(work){
switch(work){
case 'engineer':
this.name = '工程师'
this.description = '热爱编程'
break
case 'teacher':
this.name = '老师'
this.description = '灵魂工程师'
break
default:
this.name = param.work
this.description = '无'
}
}
CreateWork.prototype.changeWork = function(work){
this.name = work
}
CreateWork.prototype.changeDes = function(des){
this.description = des
}
// 使用
var candidateArr = []
for(let i = 0;i < data.length; i++){
candidateArr[i] = Candidate(data[i])
}
console.log(candidateArr[0])
单例模式
单例模式就是希望创建对象的时候,我们无论创建多少次对象都指向同一个
一、使用一个变量缓存创建的对象
二、闭包
装饰器模式
装饰器模式,希望在不改变原对象的基础上,通过对其拓展功能和属性来实现更复杂的逻辑
例:4s在卖一种车,价格为10万元,如果用户需要在此基础上加装一些配置则需要加钱,比如加热座椅需要2万元,电动后视镜需要0.8王元等等。
普通写法
装饰器写法,没有破坏原来的构造函数
组合模式
将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
应用
虚拟DOM
Vue、React用render函数渲染虚拟DOM。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
div{
border:1px solid red;
}
p{
border:1px solid blue;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
let vnode = {
type: 'div',
attrs:{
id: 'div2',
className: 'container'
},
children:[
{
type: 'p',
attrs: {},
children: ['123']
},
{
type: 'p',
attrs: {},
children: ['456']
}
]
}
function render(vnode, parent){
let element;
if(typeof vnode == 'object'){
let { type, attrs, children = [] } = vnode;
element = document.createElement(type);
for(let attr in attrs){
if(attr == 'className'){
element.setAttribute('class', attrs[attr]);
}else{
element.setAttribute(attr, attrs[attr]);
}
}
// 递归调用
for(let child of children){
render(child, element);
}
}else{
element = document.createTextNode(vnode);
}
// 将构造的元素追加到第二个参数里面
parent.appendChild(element);
}
render(vnode, document.getElementById('root'));
</script>
</body>
</html>
宏命令
宏命令对象包含了一组具体的子命令对象,不管是宏命令对象,还是子命令对象,都有一个execute方法负责执行命令。
let closeDoorCommand = {
execute: function(){
console.log( '关门' )
}
}
let openPcCommand = {
execute: function(){
console.log( '开电脑' )
}
}
let openQQCommand = {
execute: function(){
console.log( '登录QQ' )
}
}
let MacroCommand = function(){
return {
commandsList: [],
add: function( command ){
this.commandsList.push( command )
},
execute: function(){
for ( let i = 0, command; command = this.commandsList[ i++ ]; ){
command.execute()
}
}
}
}
let macroCommand = MacroCommand()
macroCommand.add( closeDoorCommand )
macroCommand.add( openPcCommand )
macroCommand.add( openQQCommand )
macroCommand.execute()
宏命令中包含了一组子命令,它们组成了一个树形结构,这里是一棵结构非常简单的树。
其中,marcoCommand被称为组合对象,closeDoorCommand、openPcCommand、openQQCommand都是子对象。在macroCommand的execute方法里,并不执行真正的操作,而是遍历它所包含的子对象,把真正的execute请求委托给这些子对象。macroCommand表现得像一个命令,但它实际上只是一组真正命令的“代理”。并非真正的代理,虽然结构上相似,但macroCommand只负责传递请求给子对象,它的目的不在于控制对子对象的访问。
组合模式将对象组合成树形结构,以表示“部分——整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
1、表示树形结构。组合模式的优点:提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象下面的子对象的execute方法。组合模式可以非常方便地描述对象部分——整体层次结构。
2、利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。
扫描文件夹
文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以包含其他文件夹,最终可能组合成一棵树,组合模式在文件夹的应用中有以下两层好处。
1. 组合模式让Ctrl+V、Ctrl+C成为了一个统一的操作。例如,在移动硬盘里找到了一些电子书,想把它们复制到F盘中的学习资料文件夹。在复制这些电子书的时候,并不需要考虑这批文件的类型,不管它们是单独的电子书还是被放在了文件夹中。
2. 用杀毒软件扫描该文件夹时,往往不会关心里面有多少文件和子文件夹,组合模式使得只需要操作最外层的文件夹进行扫描。
现在来编写代码,首先分别定义好文件夹Folder和文件File这两个类。见如下代码:
let Folder = function( name ){
this.name = name
this.files = []
}
Folder.prototype.add = function( file ){
this.files.push( file )
}
Folder.prototype.scan = function(){
console.log( '开始扫描文件夹: ' + this.name )
for ( let i = 0, file, files = this.files; file = files[ i++ ]; ){
file.scan()
}
}
let File = function( name ){
this.name = name
}
File.prototype.add = function(){
throw new Error( '文件下面不能再添加文件' )
}
File.prototype.scan = function(){
console.log( '开始扫描文件: ' + this.name )
}
接下来创建一些文件夹和文件对象,并且让它们组合成一棵树,这棵树就是F盘里的现有文件目录结构。
let folder = new Folder( '学习资料' )
let folder1 = new Folder( 'JavaScript' )
let folder2 = new Folder ( 'jQuery' )
let file1 = new File( 'JavaScript 设计模式与开发实践' )
let file2 = new File( '精通jQuery' )
let file3 = new File( '重构与模式' )
folder1.add( file1 )
folder2.add( file2 )
folder.add( folder1 )
folder.add( folder2 )
folder.add( file3 )
现在的需求是把移动硬盘里的文件和文件夹都复制到这棵树中,假设已经得到了这些文件对象:
let folder3 = new Folder( 'Nodejs' )
let file4 = new File( '深入浅出Node.js' )
folder3.add( file4 )
let file5 = new File( 'JavaScript 语言精髓与编程实践' )
接下来就是把这些文件都添加到原有的树中:
folder.add( folder3 )
folder.add( file5 )
通过这个例子,再次看到客户是如何同等对待
组合对象和基本对象的。在添加一批文件的操作过程中,客户不用分辨它们到底是文件还是文件夹。新增加的文件和文件夹能够很容易地添加到原来的树结构中,和树里已有的对象一起工作。改变了树的结构,增加了新的数据,却不用修改任何一句原有的代码,这是符合开放——封闭原则的
运用了组合模式之后,扫描整个文件夹的操作也是轻而易举的,只需要操作树的最顶端对象:
folder.scan()
总结
组合模式如果运用得当,可以大大简化客户的代码。一般来说,组合模式适用于以下这两种情况。
1. 表示对象的部分——整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分——整体结构。特别是在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放——封闭原则。
2. 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和子对象的区别
,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是子对象,也就不用写一堆if、else语句来分别处理它们。组合对象和子对象会各自做自己正确的事情,这是组合模式最重要
的能力。
然而,组合模式并不是完美的,它可能会产生一个这样的系统:系统中的每个对象看起来都与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解。此外,如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。
此部分原文:https://www.jianshu.com/nb/32302644
观察者模式
观察者模式又叫发布订阅模式或者消息模式。是具体模式中非常著名也是非常重要的一种模式,这种模式一般会定义一个主体和众多个个体,这里主体可以想象为一个消息中心,里面有各种各样的消息,当未来消息中心发布某条消息的时候,订阅过他的个体就会得到通知。
1、创建发布订阅闭包
var msgCenter = (function(){
/** { carInfo: [person1的carInfo函数, person2的carInfo函数, person3的carInfo函数] }
{ personInfo: [person1的personInfo函数, person2的personInfo函数] }
*/
var _msg = {}
return {
// 用于订阅一个消息
// 参数:消息类型:'carInfo',订阅函数
register: function(type, fn){
if(_msg[type]){
_msg[type].push(fn)
}else{
_msg[type] = [fn]
}
},
// 用于发布消息
// 参数: 消息类型:'carInfo',消息内容:{ info: '新款汽车上市' }
fire: function(type, args){
if(!_msg[type]) return
var event = {
type,
args: args || {}
}
for(var i = 0; i < _msg[type].length; i++){
_msg[type][i](event)
}
},
// 用于取消订阅
cancel: function(type, fu){
if(!_msg[type]) return
for(var i = 0; i < _msg[type].length; i++){
if(_msg[type][i] === fn){
_msg[type].splice(i, 1)
break
}
}
}
}
})()
2、封装person发布订阅构造函数
function Person(){
// 查看已订阅消息(函数)
this.alreadyRegister = {}
}
Person.prototype.register = function(type, fn){
if(this.alreadyRegister[type]){
console.log('您已订阅此消息!不要重复订阅了')
}else{
// 注册订阅
msgCenter.register(type, fn)
this.alreadyRegister[type] = fn
}
}
Person.prototype.cancel = function(type){
// 取消订阅
msgCenter.cancel(type, this.alreadyRegister[type])
delete this.alreadyRegister[type]
}
测试
var person1 = new Person()
var person2 = new Person()
var person3 = new Person()
// 订阅
person1.register('carInfo', function(e){
console.log('person1得到了关于' + e.type + '的消息,消息内容是:' + e.args.info)
})
person1.register('personInfo', function(e){
console.log('person1得到了关于' + e.type + '的消息,消息内容是:' + e.args.info)
})
person2.register('personInfo', function(e){
console.log('person2得到了关于' + e.type + '的消息,消息内容是:' + e.args.info)
})
person2.register('carInfo', function(e){
console.log('person2得到了关于' + e.type + '的消息,消息内容是:' + e.args.info)
})
person3.register('carInfo', function(e){
console.log('person3得到了关于' + e.type + '的消息,消息内容是:' + e.args.info)
})
// 发布
msgCenter.fire('carInfo', { info: '新款汽车上市' })
msgCenter.fire('personInfo', { info: 'helloworld!' })
效果
策略模式
策略模式是一种相对简单的设计模式,主要用在有多种状态或者策略需要进行选择的时候,将所有选择封装在一起,只给外部暴露出必要的接口。
例:我们有一个表单,需要验证表单项,如果验证不通过就返回一个提示文案。这里我们就可以使用策略模式的思路进行封装。
测试
1
1
1
1
1
1
1
1
1
1