《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 声明的变量在非函数作用域下是全局变量
<html>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<script>
var nodes = document.getElementsByTagName( 'div' );
for( var i = 0,len = nodes.length; i < len; i++ ){
nodes[ i ].onclick = function(){
alert ( i );//永远输出5
}
};
</script>
</body>
</html>
闭包解决生存周期问题:
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️⃣常见代码:
<html>
<body>
<button id="loginBtn">登录</button>
</body>
<script>
var createLoginLayer = function(){
var div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
return div:
};
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
</script>
</html>
❌多次点击会生成多个登录框
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 ); // 输出:40000
calculateBonus( '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 ) ); // 输出:80000
console.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️⃣常见代码实现:
①颜色切换
<script>
var colorSelect = document.getElementById(' colorSelect'),
numberInput = document.getElementById( 'numberInput'),
colorInfo = document.getElementById('colorInfon'),
umberInfo = document.getElementByid(' numberInfo'),
nextBtn = document.getElementById('nextBtn');
var goods = {// 手机库存
"red":3
"blue":6
};
colorSelect.onchange = function(){
var color = this.value, //颜色
number = numberInput.value,
stock = goods[ color ]; // 该颜色手机对应的当前库存
colorInfo.innerHTMI = color;
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 = '放入购物车';
</script>
②数量切换
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 );
}
};
⑥ 传递对象参数代替过长的参数列表
<script type="text/javascript">
var setUserInfo = function( id, name, address, sex, mobile, qq ){
console.log( 'id= ' + id );
console.log( 'name= ' +name );
console.log( 'address= ' + address );
console.log( 'sex= ' + sex );
console.log( 'mobile= ' + mobile );
console.log( 'qq= ' + qq );
};
setUserInfo( 1314, 'sven', 'shenzhen', 'male', '137********', 377876679 );
var setUserInfo = function( obj ){
console.log( 'id= ' + obj.id );
console.log( 'name= ' + obj.name );
console.log( 'address= ' + obj.address );
console.log( 'sex= ' + obj.sex );
console.log( 'mobile= ' + obj.mobile );
console.log( 'qq= ' + obj.qq );
};
setUserInfo({
id: 1314,
name: 'sven',
address: 'shenzhen',
sex: 'male',
mobile: '137********',
qq: 377876679
});
⑦ 尽量减少参数数量
var draw = function( width, height, square ){};
var draw = function( width, height ){
var square = width * height;
};
⑧ 合理使用链式调用
var User = function(){
this.id = null;
this.name = null;
};
User.prototype.setId = function( id ){
this.id = id;
return this;
};
User.prototype.setName = function( name ){
this.name = name;
return this;
};
console.log( new User().setId( 1314 ).setName( 'sven' ) );
改为:
var User = {
id: null,
name: null,
setId: function( id ){
this.id = id;
return this;
},
setName: function( name ){
this.name = name;
return this;
}
};
console.log( User.setId( 1314 ).setName( 'sven' ) );