《JavaScript设计模式与开发实践》一篇文章带你读懂
《JavaScript设计模式与开发实践》是由曾探所著的一本经典技术书籍。
该书详细介绍了JavaScript中常用的设计模式,并结合实际项目开发经验给出了实践示例和最佳实践建议。这本书首先介绍了设计模式的基本概念和作用,以及为什么要在JavaScript中使用设计模式。接着,书中详细介绍了23种常见的设计模式,如单例模式、工厂模式、适配器模式、观察者模式等。每种设计模式都详细解释了其定义、结构、应用场景和优缺点,并给出了实际的代码示例和案例分析。除了介绍设计模式的理论知识,该书还提供了大量的实践经验和开发技巧。其中包括如何组织和管理JavaScript代码、如何优化性能、如何进行模块化开发、如何进行异步编程等方面的内容。这些实践经验有助于读者更好地理解和应用设计模式,提高JavaScript项目的质量和可维护性。
《JavaScript设计模式与开发实践》这本书语言简洁明了,通俗易懂,适合初学者入门和有一定经验的开发者进阶学习。通过学习这本书,读者可以深入理解JavaScript设计模式的原理和思想,并能够灵活运用到实际项目中,提高自己的编程水平和软件开发能力。
一、前言:
⭕前置问题:
- 什么是模式?
- 设计模式的适用性是什么?
📜从一个故事开始
- 足球运动包含很多的策略,有一个叫做“下底传中”——用一个名词来去描述一种“战术”,这就是一种“模式”。
- 模式:通过一个特有的名词,来描述一类问题对应的解决方案
✍🏻设计模式的适用性
- 不要有了锤子,看什么都像钉子
- 模式只有放到具体的环境下才有意义
二、 基础知识
1.面向对象的 JavaScript
(1)程序语言设计风格
- 命令式语言(过程化语言)
- 结构化语言
- 面向对象语言 (OOP)
- 函数式语言
- 脚本语言
Java 就是典型的面向对象编程语言。它具备以下三个特点:
- 封装
- 继承
- 多态
(2)JavaScript 的语言特性
通过(脚本)原型来实现面向对象的开发(不是标准的面向对象的编程语言)
动态类型语言
概念:变量类型由值确定
let t = 'str' // string 类型
t = 007 // number 类型
🌟鸭子类型:
“从前在JavaScript王国里,有一个国王,他觉得世界上最美妙的声音就是鸭子的叫声,于是国王召集大臣,要组建一个1000只鸭子组成的合唱团。大臣们找遍了全国,终于找到999只鸭子,但是始终还差一只,最后大臣发现有一只非常特别的鸡,它的叫声跟鸭子一模一样,于是这只鸡就成为了合唱团的最后一员。”
var duck = {duckSinging: function(){console.log('嘎嘎嘎');}
};
var chicken = {duckSinging: function(){console.log('嘎嘎嘎')}
var choir =[]; //合唱团var joinChoir = function(animal ){if ( animal && typeof animal.duckSinging === 'function' ){choir.push( animal );console.log('恭喜加入合唱团' );console.1og('合唱团已有成员数量:' + choir.length );}
};joinChoir( duck );// 恭喜加入合唱团
joinChoir( chicken );// 恭喜加入合唱团
如果有一个动物,走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子。
(3)JavaScript 的多态性
⭕动态类型语言天生具备多态性。
多态:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。
代码:
var makeSound =function( animal ){if ( animal instanceof Duck ){console.log('嘎嘎嘎');}else if ( animal instanceof Chicken ){console.log('咯咯咯');}
};
var Ducka = function(){};
var Chicken = function()[};makeSound(new Duck() ); //嘎嘎嘎
makeSound(new Chicken() );//咯咯咯
多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与“可能改变的事物”分离开来
2.this、call 和 apply
(1)this 指向问题
- 构造函数:指向实例对象
- 普通函数:指向函数调用方
- 箭头函数:不修改 this 指向,沿用上层作用域的 this
- call、apply、bind:仅作用于普通函数,this 指向第一个参数
(2)this“丢失”
var obj = {myName: 'sven',getName: function(){return this.myName;}
};console.log( obj.getName() ); // 输出: sven
//getName是obj下面的方法,this 会指向obj 本身var getName2 = obj.getName;
//vr声明了一个新的变量的时候 被绑定到windows下面的
console.log( getName2() ); //相对应 window.getName2
//输出:undefined
普通函数调用方式,this 是指向全局window 的
3.闭包和高阶函数
(1)闭包
定义:能够访问其他函数作用域中变量的函数
变量的生存周期:var 声明的变量在非函数作用域下是全局变量
12345
闭包解决生存周期问题:
for( var i = 0,len = nodes.length; i < len; i++ ){(function( i ){nodes[ i ].onclick = function(){console.log(i);}})( i )
};
⭕注意:ES6之后 let 也可以解决这个问题 (块级作用域)
(2)高阶函数
满足两个条件:
- 函数可以作为参数被传递
- 函数可以作为返回值输出
三、设计模式
1.单例模式
(1)定义:
保证一个类仅有一个实例
(2)基于场景的单例模式:
1️⃣场景:
2️⃣常见代码:
❌多次点击会生成多个登录框
3️⃣单例模式:
// 单例生成器
var getSingle = function (fn) {var result;// 闭包return function () {// 逻辑中断,利用 apply 改变 fn 下的 this 指向return result || (result = fn.apply(this, arguments));}
};
// 处理登录窗口的业务逻辑
var createLoginLayer = function () {var div = document.createElement('div');div.innerHTML ='我是登录浮窗';div.style.display = 'none';document.body.appendChild(div);return div;
};
// 利用单例生成器,得到一个闭包函数,触发该闭包函数可以得到 div
var createSingleLoginLayer = getSingle(createLoginLayer);
// 监听按钮点击行为
document.getElementById( 'loginBtn').onclick = function () (// 得到 div 实例 (单例的)var loginLayer = createSingleLoginLayer();// 展示loginLayer.style.display = 'block';
};
2.策略模式
(1)定义:
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
(2)基于场景的策略模式:
1️⃣场景:奖金计算
- 绩效为 S 的人年终奖有4 倍工资
- 绩效为 A 的人年终奖有了倍工资
- 而绩效为 B 的人年终奖是 2 倍工资
2️⃣常见代码:
var calculateBonus = function( performanceLevel, salary ){if ( performanceLevel === 'S' ){return salary * 4;}if ( performanceLevel === 'A' ){return salary * 3;}if ( performanceLevel === 'B' ){return salary * 2;}};calculateBonus( 'B', 20000 ); // 输出:40000calculateBonus( 'S', 6000 ); // 输出:24000
3️⃣策略模式:
//策略对象var strategies = {"S": function( salary ){return salary * 4;},"A": function( salary ){return salary * 3;},"B": function( salary ){return salary * 2;}};//计算方法var calculateBonus = function( level, salary ){return strategies[ level ]( salary );};//执行console.log( calculateBonus( 'S', 20000 ) ); // 输出:80000console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000
3. 代理模式
(1)定义:
为其他对象提供一种代理以控制对这个对象的访问
(2)基于场景的代理模式:
场景:图片预加载 —— 创建img 标签,在图片加载之前给 img 一个占位图
常见代码:
var MyImage = (function(){var imgNode = document.createElement('img');document.body.appendchild( imgNode );var img = new Image;img.onload = function(){imgNode.src = img.src;};return{setSrc: function( src ){imgNode.src =''imq.src = src;}}
})();MyImage.setSrc('');
代理模式:
// 自执行函数,创建 img 标签。
// myImage 为返回的闭包函数,可以用来设置 src
var myImage = (function () {var imgNode = document.createElement( ' img' );document.body.appendChild( imgNode);return function (src) {imgNode.src = src;}
})();// 代理,自执行函数,创建 Image 图片加载实例
// proxyImage 为闭包函数,可以为 img 标签设置 src 和进行图片预加载
var proxyImage = (function () {var img = new Image;img.onload = function () {myImage(this.src);}return function (src) {myImage( ' ' ) ;img.src = src;}
})();proxyImage('');
单一职责原则: 就一个类 (通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。
4.迭代器模式
(1)定义:
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示
(2)基于场景的迭代器模式:
场景:根据数据创建 div,并添加到 body 中
常见代码:
var appendDiv = function( data ){for ( var i = 0,l = data.length; i < l; i++ ){var div = document.createElement('div')_;div.innerHTMI = data[ i ];document.body.appendchild( div );}
};appendDiv( [ 1,2,3,4,5,6 ] );
迭代器模式:
var each = function( obj,callback ){var value,i=0.length = obj.length,isArray = isArraylike( obj );// isArraylike函数未实现,可以翻阅jQuery源代码if(isArray ){ // 迭代类数组 for (; i < length; i++ ) {callback.call( obj[ i ], i, obj[ i ] );} }else (for(i in obj ){ // 迭代object对象 value = callback.call( obj[ i ],i,obj[ i ] );}}return obj;
};var appendDiv = function( data ){each( data, function( i,n ){var div = document.createElement( 'div' );div.innerHTML = n;document.body.appendchild( div );});
};
appendDiv([ 1,2,3,4,5,6] );
appendDiv({a:1,b:2,c:3,d:4});
JS中已经内置了迭代器:深入理解现代JavaScript
5.发布-订阅模式
(1)定义:
发布-订阅模式(观察者模式):对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
(2)基于场景的观察者模式:
1️⃣场景:一个网站页面,包含:header、nav、消息列表、购物车等模块
2️⃣对比
①常见代码:
login.succ(function(data){header.setAvatar( data.avatar); //设置header模块的头像nav.setAvatar( data.avatar ); //设置导航模块的头像message.refresh(); //刷新消息列表cart.refresh); //刷新购物车列表
});
②观察者模式:
发布消息:
$.ajax( 'http:// xxx.com?login',function(data){ //登录成功login.trigger('loginSucc', data); // 发布登录成功的消息
});
接收消息:
var header = (function(){// header模块login.listen('loginSucc', function( data)(header.setAvatar( data.avatar );});return {setAvatar: funtion( data ){console.log( '设置header模块的头像' );}}
})();var nav = (funtion(){ // nav模块login.listen( 'loginSucc', funtion( data ){nav.setAvatar( data.avatar );});return {setAvatar: funtion( avatar ){console.log( '设置nav模块的头像' );}
6.命令模式
7.组合模式
8.模板方法模式
(1)定义:
类似于继承。公用的部分统一封装(模板),不同的部分单独处理。
(2)Coffee or Tea:
1️⃣咖啡:
- 把水煮沸
- 用沸水冲泡咖啡
- 把咖啡倒进杯子
2️⃣茶:
(1) 把水煮沸
(2) 用沸水浸泡茶叶
(3) 把茶水倒进杯子
9.享元模式
10.职责链模式
11.中介者模式
(1)定义:
定义一个对象(中介者),该对象封装了系统中对象间的交互方式。
(2)基于场景的中介者模式:
1️⃣场景:SKU



⭕代码的实现需要分别监听颜色以及数量的变化。
2️⃣常见代码实现:
①颜色切换
②数量切换
numberInput.oninput = function(){var color = colorSelect.value, // 颜色number = this.value, //数量stock = goods[ color ]; // 该颜色手机对应的当前库存numberInfo.innerHTML = number;if ( !color ){nextBtn.disabled = true;nextBtn.innerHTML = '请选择手机颜色';return;}if ((( number - 0) | 0)!== number - 0){ // 输入购买数量是否为正整数nextBtn.disabled = true;nextBtn.innerHTML = '请输入正确的购买数量';return;}if (number > stock ){//当前选择数量没有超过库存量nextBtn.disabled = true;nextBtn.innerHTML = '库存不足';return ;}nextBtn.disabled = false;nextBtn.innerHTML = '放入购物车 ;
};
3️⃣中介者模式:
var goods = {"red | 32G":3,"red | 16G":0,"blue |32G":1,"blue | 16G":6
};
var mediator = (function(){var colorSelect = document.getElementById( 'colorSelect'),var memorySelect = document.getElementById('memorySelect'),numberInput = document.getElementById('numberInput'),colorInfo = document.getElementById('colorInfo'),memoryInfo = document.getElementById('memoryInfo'),numberInfo = document.getElementById('numberInfo'),nextBtn = document.getElementById('nextBtn');return{changed: function( obj ){var color = colorSelect.value, //颜色memory = memorySelect.value,// 内存number = numberInput.value,//数量stock = goods[ color + '|'+ memory ]; //颜色和内存对应的手机库存数量if ( obj === colorSelect ){ //如果改变的是选择颜色下拉框colorInfo.innerHTMI = color;}else if ( obj === memorySelect ){memoryInfo.innerHTML = memory;}else if ( obj === numberInput ){numberInfo.innerHTML = number;}if ( !color){nextBtn.disabled = true;nextBtn.innerHTML = '请选择手机颜色';return;}if(!memory){nextBtn.disabled = true;nextBtn.innerHTML = '请选择内存大小';return;}if(((number-0) |0 )!== number - 0){ // 输入购买数量是否为正整数nextBtn.disabled = true;nextBtn.innerHTML = '请输入正确的购买数量';return;}nextBtn.disabled = false;nextBtn.innerHTM = '放入购物车';}}
})();//事件函数:
colorSelect.onchange = function(){mediator.changed( this );
};
tememorySelect.onchange = function(){mediator.changed( this );
}
numberInput.oninput = function(){mediator.changed(this );
};
12.装饰者模式
(1)定义:
让类或者对象一开始只具有一些基础的职责,更多的职责在代码运行时被动态装饰到对象上面
(2)基于场景的装饰者模式:
- 场景
- 常见代码
- 装饰者模式
13.状态模式
14.适配器模式
四、设计原则和编程技巧
1.单一职责原则
(1)定义:
一个对象(方法)应该只做一件事情
📜案例:小明买酱油
1️⃣旧实现:
function shoping() {console.log('小明用钥匙打开门')console.log('小明去超市')console.log('小明买酱油')console.log('小明回家')
}
❌修改现有函数的实现则意味着需要重新测试整个的流程
2️⃣单一职责原则:
function shoping() {open()goSupermarket()buy()goHome()
}function open() {console.Log('小明用钥匙打开门')
}function goSupermarket(){console.log('小明去超市')
}function buy() {console.log('小明买酱油')
}function goHome(){console.log('小明回家')
}
修正如下:
function shoping(){
+ openPasswordDoor()
···
}function open() {console.log('小明用钥匙打开门')
}
+function openPasswordDoor({
+ console.log('小明用钥匙打开门')
+}
(2)应用:
- 单例模式
- 代理模式
- 迭代器模式
- 装饰者模式
(3)实际应用:违反原则并不奇怪
2.最少知识原则 (迪米特法则)
(1)定义:
一个对象应当尽可能少地与其他对象发生相互作用
(2)应用:
- 中介者模式
- 外观模式
3.开放封闭原则
(1)定义:
软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。
(2)应用:
1️⃣用多态来消除条件分支
初始代码:

不符合开发多态原则
多态写法:
2️⃣发布-订阅模式
3️⃣模板方法模式
4️⃣策略模式
5️⃣代理模式
6️⃣职责链模式
4.接口和面向接口编程
5.代码重构原则
(1)概念:
无论你进行了再多的努力,重构都是一个会在未来发生的事情,只不过是一个早晚的问题。
(2)尽量拖慢重构发生的时间的方法:
① 提炼函数:
var getUserInfo = function(){ajax( 'http:// xxx.com/userInfo', function( data ){console.log( 'userId: ' + data.userId );console.log( 'userName: ' + data.userName );console.log( 'nickName: ' + data.nickName );});};
改为:
var getUserInfo = function(){ajax( 'http:// xxx.com/userInfo', function( data ){printDetails( data );});
};var printDetails = function( data ){console.log( 'userId: ' + data.userId );console.log( 'userName: ' + data.userName );console.log( 'nickName: ' + data.nickName );
};
② 合并重复的代码片段
var paging = function( currPage ){if ( currPage <= 0 ){currPage = 0;jump( currPage ); // 跳转}else if ( currPage >= totalPage ){currPage = totalPage;jump( currPage ); // 跳转}else{jump( currPage ); // 跳转}};
改为:
var paging = function( currPage ){if ( currPage <= 0 ){currPage = 0;}else if ( currPage >= totalPage ){currPage = totalPage;}jump( currPage ); // 把jump 函数独立出来};
③ 把条件分支语句提炼成函数
var getPrice = function( price ){var date = new Date();if ( date.getMonth() >= 6 && date.getMonth() <= 9 ){ // 夏天return price * 0.8;}return price;};
改为:
var isSummer = function(){var date = new Date();return date.getMonth() >= 6 && date.getMonth() <= 9;};var getPrice = function( price ){if ( isSummer() ){ // 夏天return price * 0.8;}return price;};
④ 合理使用循环
var createXHR = function(){var xhr;try{xhr = new ActiveXObject( 'MSXML2.XMLHttp.6.0' );}catch(e){try{xhr = new ActiveXObject( 'MSXML2.XMLHttp.3.0' );}catch(e){xhr = new ActiveXObject( 'MSXML2.XMLHttp' );}}return xhr;};var xhr = createXHR();
改为:
var createXHR = function(){var versions= [ 'MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp' ];for ( var i = 0, version; version = versions[ i++ ]; ){try{return new ActiveXObject( version );}catch(e){}}};var xhr = createXHR();
⑤ 提前让函数退出代替嵌套条件分支
var del = function( obj ){var ret;if ( !obj.isReadOnly ){ // 不为只读的才能被删除if ( obj.isFolder ){ // 如果是文件夹ret = deleteFolder( obj );}else if ( obj.isFile ){ // 如果是文件ret = deleteFile( obj );}}return ret;};
改为以下代码减少嵌套:
var del = function( obj ){if ( obj.isReadOnly ){ // 反转if 表达式return;}if ( obj.isFolder ){return deleteFolder( obj );}if ( obj.isFile ){return deleteFile( obj );}};
⑥ 传递对象参数代替过长的参数列表

