JavaScript设计模式与开发实践书籍阅读记录
原 设计模式 阅读书籍读完大概需要271分钟
- 发布时间:2018-08-02 23:41 星期四
- 刘伟波
- 1294
- 更新于: 2018-09-25 22:38 星期二
关于阅读书籍部分,是个人在本书籍中收集的精华部分和实战部分,为了后续再次阅读节省时间
和方便在工作中的应用,后续会分享本书籍的电子版pdf在线下载。不过我还是建议读着去阅读原著。
--
第一章 面向对象的 JavaScript
1.1 动态类型语言和鸭子类型
静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。
动态类型语言的优点是编写的代码数量更少,看起来也更加简洁,程序员可以把精力更多地
放在业务逻辑上面,这一切都建立在鸭子类型
- 我们可以通过一个小故事来更深刻地了解鸭子类型。
从前在
JavaScript
王国里,有一个国王,他觉得世界上最美妙的声音就是鸭子的叫
声,于是国王召集大臣,要组建一个 1000 只鸭子组成的合唱团。大臣们找遍了全国,
终于找到 999 只鸭子,但是始终还差一只,最后大臣发现有一只非常特别的鸡,它的叫
声跟鸭子一模一样,于是这只鸡就成为了合唱团的最后一员。
这个故事告诉我们,国王要听的只是鸭子的叫声,这个声音的主人到底是鸡还是鸭并不重要。
鸭子类型指导我们只关注对象的行为,而不关注对象本身
1.2 多态
多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结
果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的
反馈。
用自己的话解释就是:一个函数干一件事,要符合开发封闭原则
1.2.5 JavaScript的多态
多态的思想实际上是把“做什么”和“谁去做”分离开来,要实现
这一点,归根结底先要消除类型之间的耦合关系。
eg:假设我们要编写一个地图应用,现在有两家可选的地图 API 提供商供我们接入自己的应用。
目前我们选择的是谷歌地图,谷歌地图的 API 中提供了 show
方法,负责在页面上展示整个地图。
示例代码如下:
var googleMap = {
show: function(){
console.log( '开始渲染谷歌地图' );
}
};
var renderMap = function(){
googleMap.show();
};
renderMap(); // 输出:开始渲染谷歌地图
后来因为某些原因,要把谷歌地图换成百度地图,为了让renderMap
函数保持一定的弹性,
我们用一些条件分支来让 renderMap
函数同时支持谷歌地图和百度地图:
var googleMap = {
show: function(){
console.log( '开始渲染谷歌地图' );
}
};
var baiduMap = {
show: function(){
console.log( '开始渲染百度地图' );
}
};
var renderMap = function( type ){
if ( type === 'google' ){
googleMap.show();
}else if ( type === 'baidu' ){
baiduMap.show();
}
};
renderMap( 'google' ); // 输出:开始渲染谷歌地图
renderMap( 'baidu' ); // 输出:开始渲染百度地图
可以看到,虽然 renderMap 函数目前保持了一定的弹性,但这种弹性是很脆弱的,一旦需要
替换成搜搜地图,那无疑必须得改动 renderMap 函数,继续往里面堆砌条件分支语句。
我们还是先把程序中相同的部分抽象出来,那就是显示某个地图:
var renderMap = function( map ){
if ( map.show instanceof Function ){
map.show();
}
};
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMap ); // 输出:开始渲染百度地图
对象的多态性提
示我们,“做什么”和“怎么去做”是可以分开的,即使以后增加了搜搜地图,renderMap 函数仍
然不需要做任何改变,如下所示:
var sosoMap = {
show: function(){
console.log( '开始渲染搜搜地图' );
}
};
renderMap( sosoMap ); // 输出:开始渲染搜搜地图
1.3 封装
封装的目的是将信息隐藏。
1.3.2 封装实现
从封装实现细节来讲,封装使得对象内部的变化对其他对象而言是透明的,也就是不可见的。
对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。封装使得对象之间的耦合变松散,对象之间只通过暴露的 API 接口来通信。当我们修改一个对象时,可以随意地修改它的
内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。
new 运算符从构造器中得到一个对象:
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var a = new Person( 'sven' )
console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出:true
在这里Person
并不是类,而是函数构造器,JavaScript
的函数既可以作为普通函数被调用,
也可以作为构造器被调用。当使用 new 运算符来调用函数时,此时的函数就是一个构造器。 用
new 运算符来创建对象的过程,实际上也只是先克隆 Object.prototype
对象,再进行一些其他额
外操作的过程。
在 Chrome 和 Firefox 等向外暴露了对象proto属性的浏览器下,我们可以通过下面这段代
码来理解 new 运算的过程:
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var objectFactory = function(){
var obj = new Object(), // 从 Object.prototype 上克隆一个空的对象
Constructor = [].shift.call( arguments ); // 取得外部传入的构造器,此例是 Person
obj.__proto__ = Constructor.prototype; // 指向正确的原型
var ret = Constructor.apply( obj, arguments ); // 借用外部传入的构造器给 obj 设置属性
return typeof ret === 'object' ? ret : obj; // 确保构造器总是会返回一个对象
};
var a = objectFactory( Person, 'sven' );
console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出:true
我们看到,分别调用下面两句代码产生了一样的结果:
var a = objectFactory( A, 'sven' );
var a = new A( 'sven' );
第二章 this、call 和 apply
2.1 this
2.1.1 this的指向
- 作为对象的方法调用。
- 作为普通函数调用。
- 构造器调用。
Function.prototype.call
或Function.prototype.apply
调用。
2.1.2 丢失的this
这是一个经常遇到的问题,我们先看下面的代码:
var obj = {
myName: 'sven',
getName: function(){
return this.myName;
}
};
console.log( obj.getName() ); // 输出:'sven'
var getName2 = obj.getName;
console.log( getName2() ); // 输出:undefined
当调用 obj.getName
时,getName
方法是作为 obj 对象的属性被调用的,根据 2.1.1 节提到的规
律,此时的 this
指向 obj
对象,所以obj.getName()
输出sven
.
当用另外一个变量 getName2 来引用 obj.getName,并且调用 getName2 时,根据 2.1.2 节提到的
规律,此时是普通函数调用方式,this
是指向全局window
的,所以程序的执行结果是 undefined
。
2.2 call 和 apply
2.2.1 call和apply的区别
call
是包装在 apply
上面的一颗语法糖,如果我们明确地知道函数接受多少个参数,而且想
一目了然地表达形参和实参的对应关系,那么也可以用 call 来传送参数。
当使用call
或者 apply
的时候,如果我们传入的第一个参数为null
,函数体内的 this 会指
向默认的宿主对象,在浏览器中则是 window:
var func = function( a, b, c ){
alert ( this === window ); // 输出 true
};
func.apply( null, [ 1, 2, 3 ] );
但如果是在严格模式下,函数体内的 this 还是为 null:
var func = function( a, b, c ){
"use strict";
alert ( this === null ); // 输出 true
}
func.apply( null, [ 1, 2, 3 ] );
第三章 闭包和高阶函数
3.1 闭包
3.1.2 变量的生存周期
除了变量的作用域之外,另外一个跟闭包有关的概念是变量的生存周期。
对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。
而对于在函数内用 var 关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了
它们的价值,它们都会随着函数调用的结束而被销毁:
们的价值,它们都会随着函数调用的结束而被销毁:
var func = function(){
var a = 1; // 退出函数后局部变量 a 将被销毁
alert ( a );
};
func();
3.1.3 闭包的更多作用
1.封装变量 mult 函数接受一些 number 类型的参数,并返回这些参数的乘积。现在我们觉得对于那些相同
的参数来说,每次都进行计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能:
var mult = (function(){
var cache = {};
var calculate = function(){ // 封闭 calculate 函数
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i ){
a = a * arguments[i];
}
return a;
};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if ( args in cache ){
return cache[ args ];
}
return cache[ args ] = calculate.apply( null, arguments );
}
})();
- 延续局部变量的寿命
img 对象经常用于进行数据上报,如下所示:
var report = function( src ){
var img = new Image();
img.src = src;
};
report( 'http://xxx.com/getUserInfo' );
但是通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在 bug,在这些浏览器
下使用 report 函数进行数据上报会丢失 30%左右的数据,也就是说,report 函数并不是每一次
都成功发起了 HTTP 请求。丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数的
调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求
就会丢失掉。
现在我们把 img 变量用闭包封闭起来,便能解决请求丢失的问题:
var report = (function(){
var imgs = [];
return function( src ){
var img = new Image();
imgs.push( img );
img.src = src;
}
})();
3.1.4 闭包和面向对象设计
下面来看看这段跟闭包相关的代码:
var extent = function(){
var value = 0;
return {
call: function(){
value ;
console.log( value );
}
}
};
var extent = extent();
extent.call(); // 输出:1
extent.call(); // 输出:2
extent.call(); // 输出:3
如果换成面向对象的写法,就是:
var extent = {
value: 0,
call: function(){
this.value ;
console.log( this.value );
}
};
extent.call(); // 输出:1
extent.call(); // 输出:2
extent.call(); // 输出:3
或者:
var Extent = function(){
this.value = 0;
};
Extent.prototype.call = function(){
this.value ;
console.log( this.value );
};
var extent = new Extent();
extent.call();
extent.call();
extent.call();
3.1.6 闭包与内存管理
闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成
内存泄露,所以要尽量减少闭包的使用。
在将来需要回收这些变量,我们可以手动把这些变量设为 null
。
跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作
用域链中保存着一些 DOM 节点,这时候就有可能造成内存泄露。但这本身并非闭包的问题,也
并非 JavaScript 的问题。在 IE 浏览器中,由于 BOM 和 DOM 中的对象是使用 C 以 COM 对象
的方式实现的,而 COM 对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用
造成的内存泄露在本质上也不是闭包造成的。
同样,如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为 null
即可。将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运
行时,就会删除这些值并回收它们占用的内存。
3.2 高阶函数
高阶函数是指至少满足下列条件之一的函数。
- 函数可以作为参数被传递;
- 函数可以作为返回值输出。
3.2.1 函数作为参数传递
- 回调函数
在 ajax 异步请求的应用中,回调函数的使用非常频繁。当我们想在 ajax 请求返回之后做一
些事情,但又并不知道请求返回的确切时间时,最常见的方案就是把 callback 函数当作参数传入
发起 ajax 请求的方法中,待请求完成之后执行 callback 函数:
var getUserInfo = function( userId, callback ){
$.ajax( 'http://xxx.com/getUserInfo?' userId, function( data ){
if ( typeof callback === 'function' ){
callback( data );
}
});
}
getUserInfo( 13157, function( data ){
alert ( data.userName );
});
3.2.2 函数作为返回值输出
- 判断数据的类型
var isString = function( obj ){
return Object.prototype.toString.call( obj ) === '[object String]';
};
var isArray = function( obj ){
return Object.prototype.toString.call( obj ) === '[object Array]';
};
var isNumber = function( obj ){
return Object.prototype.toString.call( obj ) === '[object Number]';
};
我们发现,这些函数的大部分实现都是相同的,不同的只是 Object.prototype.toString.
call( obj )
返回的字符串。为了避免多余的代码,我们尝试把这些字符串作为参数提前值入isType
函数。代码如下:
var isType = function( type ){
return function( obj ){
return Object.prototype.toString.call( obj ) === '[object ' type ']';
}
};
var isString = isType( 'String' );
var isArray = isType( 'Array' );
var isNumber = isType( 'Number' );
console.log( isArray( [ 1, 2, 3 ] ) ); // 输出:true
我们还可以用循环语句,来批量注册这些 isType
函数:
var Type = {};
for ( var i = 0, type; type = [ 'String', 'Array', 'Number' ][ i ]; ){
(function( type ){
Type[ 'is' type ] = function( obj ){
return Object.prototype.toString.call( obj ) === '[object ' type ']';
}
})( type )
};
Type.isArray( [] ); // 输出:true
Type.isString( "str" ); // 输出:true
3.2.3 高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些
跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,
再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。
本节我们通过扩展 Function.prototype 来做到这一点。代码如下:
Function.prototype.before = function( beforefn ){
var __self = this; // 保存原函数的引用
return function(){ // 返回包含了原函数和新函数的"代理"函数
beforefn.apply( this, arguments ); // 执行新函数,修正 this
return __self.apply( this, arguments ); // 执行原函数
}
};
Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};
var func = function(){
console.log( 2 );
};
func = func.before(function(){
console.log( 1 );
}).after(function(){
console.log( 3 );
});
func();
我们把负责打印数字 1 和打印数字 3 的两个函数通过 AOP 的方式动态植入 func 函数。通过
执行上面的代码,我们看到控制台顺利地返回了执行结果 1、2、3。
这种使用 AOP
的方式来给函数添加职责,也是 JavaScript 语言中一种非常特别和巧妙的装饰
者模式实现。这种装饰者模式在实际开发中非常有用,我们将在第 15 章进行详细的讲解。
3.2.4 高阶函数的其他应用
- currying 是函数柯里化
currying 又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,
该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保
存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
var monthlyCost = 0;
var cost = function( money ){
monthlyCost = money;
};
cost( 100 ); // 第 1 天开销
cost( 200 ); // 第 2 天开销
cost( 300 ); // 第 3 天开销
//cost( 700 ); // 第 30 天开销
alert ( monthlyCost ); // 输出:600
通过这段代码可以看到,每天结束后我们都会记录并计算到今天为止花掉的钱。但我们其实
并不太关心每天花掉了多少钱,而只想知道到月底的时候会花掉多少钱。也就是说,实际上只需
要在月底计算一次。
接下来我们编写一个通用的 function currying(){},function currying(){}接受一个参数,即
将要被 currying 的函数。在这个例子里,这个函数的作用遍历本月每天的开销并求出它们的总和。
代码如下:
var currying = function( fn ){
var args = [];
return function(){
if ( arguments.length === 0 ){
return fn.apply( this, args );
}else{
[].push.apply( args, arguments );
return arguments.callee;
}
}
};
var cost = (function(){
var money = 0;
return function(){
for ( var i = 0, l = arguments.length; i < l; i ){
money = arguments[ i ];
}
return money;
}
})();
var cost = currying( cost ); // 转化成 currying 函数
cost( 100 ); // 未真正求值
cost( 200 ); // 未真正求值
cost( 300 ); // 未真正求值
alert ( cost() ); // 求值并输出:600
- 函数节流
(1) 函数被频繁调用的场景
- window.onresize 事件
- mousemove 事件。
- 上传进度。打印窗口大小的工作 1 秒钟进行了 10 次。而我们实际上只需要 2 次或者 3 次。
(2) 函数节流的原理
我们整理上面提到的三个场景,发现它们面临的共同问题是函数被触发的频率太高。
比如我们在 window.onresize 事件中要打印当前的浏览器窗口大小,在我们通过拖曳来改变
窗口大小的时候,打印窗口大小的工作 1 秒钟进行了 10 次。而我们实际上只需要 2 次或者 3 次。
这就需要我们按时间段来忽略掉一些事件请求,比如确保在 500ms 内只打印一次。很显然,我们
可以借助 setTimeout 来完成这件事情。
(3) 函数节流的代码实现
var throttle = function ( fn, interval ) {
var __self = fn, // 保存需要被延迟执行的函数引用
timer, // 定时器
firstTime = true; // 是否是第一次调用
return function () {
var args = arguments,
__me = this;
if ( firstTime ) { // 如果是第一次调用,不需延迟执行
__self.apply(__me, args);
return firstTime = false;
}
if ( timer ) { // 如果定时器还在,说明前一次延迟执行还没有完成
return false;
}
timer = setTimeout(function () { // 延迟一段时间执行
clearTimeout(timer);
timer = null;
__self.apply(__me, args);
}, interval || 500 );
};
};
window.onresize = throttle(function(){
console.log( 1 );
}, 500 );
- 分时函数
一个例子是创建 WebQQ 的 QQ 好友列表。列表中通常会有成百上千个好友,如果一个好友
用一个节点来表示,当我们在页面中渲染这个列表的时候,可能要一次性往页面中创建成百上千
个节点。
在短时间内往页面中大量添加 DOM 节点显然也会让浏览器吃不消,我们看到的结果往往就
是浏览器的卡顿甚至假死。代码如下:
var ary = [];
for ( var i = 1; i <= 1000; i ){
ary.push( i ); // 假设 ary 装载了 1000 个好友的数据
};
var renderFriendList = function( data ){
for ( var i = 0, l = data.length; i < l; i ){
var div = document.createElement( 'div' );
div.innerHTML = i;
document.body.appendChild( div );
}
};
renderFriendList( ary );
这个问题的解决方案之一是下面的 timeChunk 函数,timeChunk 函数让创建节点的工作分批进
行,比如把 1 秒钟创建 1000 个节点,改为每隔 200 毫秒创建 8 个节点。
timeChunk 函数接受 3 个参数,第 1 个参数是创建节点时需要用到的数据,第 2 个参数是封装了创建节点逻辑的函数,第 3 个参数表示每一批创建的节点数量。代码如下:
var timeChunk = function( ary, fn, count ){
var obj,
t;
var len = ary.length;
var start = function(){
for ( var i = 0; i < Math.min( count || 1, ary.length ); i ){
var obj = ary.shift();
fn( obj );
}
};
return function(){
t = setInterval(function(){
if ( ary.length === 0 ){ // 如果全部节点都已经被创建好
return clearInterval( t );
}
start();
}, 200 ); // 分批执行的时间间隔,也可以用参数的形式传入
};
};
最后我们进行一些小测试,假设我们有 1000 个好友的数据,我们利用 timeChunk 函数,每一
批只往页面中创建 8 个节点:
var ary = [];
for ( var i = 1; i <= 1000; i ){
ary.push( i );
};
var renderFriendList = timeChunk( ary, function( n ){
var div = document.createElement( 'div' );
div.innerHTML = n;
document.body.appendChild( div );
}, 8 );
renderFriendList();
- 惰性加载函数
在 Web 开发中,因为浏览器之间的实现差异,一些嗅探工作总是不可避免。比如我们需要
一个在各个浏览器中能够通用的事件绑定函数 addEvent,常见的写法如下:
var addEvent = function( elem, type, handler ){
if ( window.addEventListener ){
return elem.addEventListener( type, handler, false );
}
if ( window.attachEvent ){
return elem.attachEvent( 'on' type, handler );
}
};
这个函数的缺点是,当它每次被调用的时候都会执行里面的 if 条件分支,虽然执行这些 if
分支的开销不算大,但也许有一些方法可以让程序避免这些重复的执行过程。
讨论的惰性载入函数方案。此时 addEvent 依然被声明为一个普通函
数,在函数里依然有一些分支判断。但是在第一次进入条件分支之后,在函数内部会重写这个函
数,重写之后的函数就是我们期望的 addEvent 函数,在下一次进入 addEvent 函数的时候,addEvent
函数里不再存在条件分支语句:
var addEvent = function( elem, type, handler ){
if ( window.addEventListener ){
addEvent = function( elem, type, handler ){
elem.addEventListener( type, handler, false );
}
}else if ( window.attachEvent ){
addEvent = function( elem, type, handler ){
elem.attachEvent( 'on' type, handler );
}
}
addEvent( elem, type, handler );
};
var div = document.getElementById( 'div1' );
addEvent( div, 'click', function(){
alert (1);
});
addEvent( div, 'click', function(){
alert (2);
});
第四章 单例模式
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏
览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我
们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少
次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。
4.1 实现单例模式
var Singleton = function( name ){
this.name = name;
};
Singleton.prototype.getName = function(){
alert ( this.name );
};
Singleton.getInstance = (function(){
var instance = null;
return function( name ){
if ( !instance ){
instance = new Singleton( name );
}
return instance;
}
})();
var a = Singleton.getInstance( 'sven1' );
var b = Singleton.getInstance( 'sven2' );
alert ( a === b ); // true
我们通过 Singleton.getInstance
来获取 Singleton 类的唯一对象,这种方式相对简单,但有
一个问题,就是增加了这个类的“不透明性”,Singleton 类的使用者必须知道这是一个单例类,
跟以往通过 new XXX
的方式来获取对象不同,这里偏要使用Singleton.getInstance
来获取对象。
虽然现在已经完成了一个单例模式的编写,但这段单例模式代码的意义并不大。从下一节开
始,我们将一步步编写出更好的单例模式。
4.2 透明的单例模式
下面的例子中,我们将使用 CreateDiv 单例类,它的作用是负责在页
面中创建唯一的 div 节点,代码如下:
var CreateDiv = (function(){
var instance;
var CreateDiv = function( html ){
if ( instance ){
return instance;
}
this.html = html;
this.init();
return instance = this;
};
CreateDiv.prototype.init = function(){
var div = document.createElement( 'div' );
div.innerHTML = this.html;
document.body.appendChild( div );
};
return CreateDiv;
})();
var a = new CreateDiv( 'sven1' );
var b = new CreateDiv( 'sven2' );
alert ( a === b ); // true
虽然现在完成了一个透明的单例类的编写,但它同样有一些缺点。
为了把 instance
封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回
真正的 Singleton
构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
在这段代码中,CreateDiv 的构造函数实际上负责了两件事情。第一是创建对象和执行初始
化 init 方法,第二是保证只有一个对象。
假设我们某天需要利用这个类,在页面中创建千千万万的 div,即要让这个类从单例类变成
一个普通的可产生多个实例的类,那我们必须得改写 CreateDiv 构造函数,把控制创建唯一对象
的那一段去掉,这种修改会给我们带来不必要的烦恼。
4.3 用代理实现单例模式
var CreateDiv = function( html ){
this.html = html;
this.init();
};
CreateDiv.prototype.init = function(){
var div = document.createElement( 'div' );
div.innerHTML = this.html;
document.body.appendChild( div );
};
接下来引入代理类 proxySingletonCreateDiv
:
var ProxySingletonCreateDiv = (function(){
var instance;
return function( html ){
if ( !instance ){
instance = new CreateDiv( html );
}
return instance;
}
})();
var a = new ProxySingletonCreateDiv( 'sven1' );
var b = new ProxySingletonCreateDiv( 'sven2' );
alert ( a === b );
通过引入代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们
把负责管理单例的逻辑移到了代理类 proxySingletonCreateDiv
中。这样一来,CreateDiv
就变成了
一个普通的类,它跟 proxySingletonCreateDiv
组合起来可以达到单例模式的效果。
本例是缓存代理的应用之一,在第 6 章中,我们将继续了解代理带来的好处。
4.4 JavaScript 中的单例模式
单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例模式,但在 JavaScript 开发中,我们经常会把全局变量当成单例来使用。
例如:
var a = {};
当用这种方式创建对象 a 时,对象 a 确实是独一无二的。如果 a 变量被声明在全局作用域下,
则我们可以在代码中的任何位置使用这个变量,全局变量提供给全局访问是理所当然的。这样就
满足了单例模式的两个条件。
但是全局变量存在很多问题,它很容易造成命名空间污染。
- 使用命名空间
- 使用闭包封装私有变量
4.5 惰性单例
惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实
际开发中非常有用,有用的程度可能超出了我们的想象。
假设我们是 WebQQ 的开发人员(网址是web.qq.com),当点击左边导航里 QQ 头像时,会弹
出一个登录浮窗,很明显这个浮窗在页面里总是唯一的,不可能出现同时存在
两个登录窗口的情况。
也许我们进入 WebQQ 只是玩玩游戏或者看看天气,根本不需要进行
登录操作,因为登录浮窗总是一开始就被创建好,那么很有可能将白白浪费一些 DOM 节点。
也许读者已经想到了,我们可以用一个变量来判断是否已经创建过登录浮窗,这也是本节第
一段代码中的做法:
var createLoginLayer = (function(){
var div;
return function(){
if ( !div ){
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';
};
4.6 通用的惰性单例
上一节我们完成了一个可用的惰性单例,但是我们发现它还有如下一些问题
如果我们下次需要创建页面中唯一的 iframe,或者 script 标签,用来跨域请求数据,就
必须得如法炮制,把 createLoginLayer 函数几乎照抄一遍:
var createIframe= (function(){
var iframe;
return function(){
if ( !iframe){
iframe= document.createElement( 'iframe' );
iframe.style.display = 'none';
document.body.appendChild( iframe);
}
return iframe;
}
})();
现在我们就把如何管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在 getSingle
函数内部,创建对象的方法 fn 被当成参数动态传入 getSingle 函数:
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
接下来将用于创建登录浮窗的方法用参数 fn
的形式传入 getSingle
,我们不仅可以传入createLoginLayer
,还能传入 createScript、createIframe、createXhr
等。之后再让 getSingle 返回
一个新的函数,并且用一个变量 result 来保存 fn 的计算结果。result 变量因为身在闭包中,它永远不会被销毁。在将来的请求中,如果 result 已经被赋值,那么它将返回这个值。代码如下:
var createLoginLayer = function(){
var div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
return div;
};
var createSingleLoginLayer = getSingle( createLoginLayer );
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
下面我们再试试创建唯一的 iframe 用于动态加载第三方页面:
var createSingleIframe = getSingle( function(){
var iframe = document.createElement ( 'iframe' );
document.body.appendChild( iframe );
return iframe;
});
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleIframe();
loginLayer.src = 'http://baidu.com';
};
在这个例子中,我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两
个方法可以独立变化而互不影响,当它们连接在一起的时候,就完成了创建唯一实例对象的功能,
看起来是一件挺奇妙的事情。
这种单例模式的用途远不止创建对象,比如我们通常渲染完页面中的一个列表之后,接下来
要给这个列表绑定 click 事件,如果是通过 ajax 动态往列表里追加数据,在使用事件代理的前提
下,click 事件实际上只需要在第一次渲染列表的时候被绑定一次,但是我们不想去判断当前是
否是第一次渲染列表,如果借助于 jQuery,我们通常选择给节点绑定 one 事件:
var bindEvent = function(){
$( 'div' ).one( 'click', function(){
alert ( 'click' );
});
};
var render = function(){
console.log( '开始渲染列表' );
bindEvent();
};
render();
render();
render();
如果利用getSingle
函数,也能达到一样的效果。代码如下:
var bindEvent = getSingle(function(){
document.getElementById( 'div1' ).onclick = function(){
alert ( 'click' );
}
return true;
});
var render = function(){
console.log( '开始渲染列表' );
bindEvent();
};
render();
render();
render();
可以看到,render 函数和 bindEvent 函数都分别执行了 3 次,但 div 实际上只被绑定了一个
事件。
4.7 小结
这一章还提到了代理模式和单一职责原则,
后面的章节会对它们进行更详细的讲解。
在 getSinge 函数中,实际上也提到了闭包和高阶函数的概念。单例模式是一种简单但非常实
用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个
第五章 策略模式
俗话说,条条大路通罗马。
同样,在现实中,很多时候也有多种途径到达同一个目的地。比如我们要去某个地方旅游,
可以根据具体的实际情况来选择出行的线路。
- 如果没有时间但是不在乎钱,可以选择坐飞机。
- 如果没有钱,可以选择坐大巴或者火车。
- 如果再穷一点,可以选择骑自行车。
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
5.1 使用策略模式计算奖金
很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为 S 的人年
终奖有 4 倍工资,绩效为 A 的人年终奖有 3 倍工资,而绩效为 B 的人年终奖是 2 倍工资。假设财
务部要求我们提供一段代码,来方便他们计算员工的年终奖。
var performanceS = function(){};
performanceS.prototype.calculate = function( salary ){
return salary * 4;
};
var performanceA = function(){};
performanceA.prototype.calculate = function( salary ){
return salary * 3;
};
var performanceB = function(){};
performanceB.prototype.calculate = function( salary ){
return salary * 2;
};
接下来定义奖金类 Bonus:
var Bonus = function(){
this.salary = null; // 原始工资
this.strategy = null; // 绩效等级对应的策略对象
};
Bonus.prototype.setSalary = function( salary ){
this.salary = salary; // 设置员工的原始工资
};
Bonus.prototype.setStrategy = function( strategy ){
this.strategy = strategy; // 设置员工绩效等级对应的策略对象
};
Bonus.prototype.getBonus = function(){ // 取得奖金数额
return this.strategy.calculate( this.salary ); // 把计算奖金的操作委托给对应的策略对象
};
在完成最终的代码之前,我们再来回顾一下策略模式的思想:
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换
比如员工的原始工资数额。接下来把某个计算奖金的策略对象也传入 bonus 对象
内部保存起来。当调用 bonus.getBonus()来计算奖金的时候,bonus 对象本身并没有能力进行计算,
而是把请求委托给了之前保存好的策略对象:
var bonus = new Bonus();
bonus.setSalary( 10000 );
bonus.setStrategy( new performanceS() ); // 设置策略对象
console.log( bonus.getBonus() ); // 输出:40000
bonus.setStrategy( new performanceA() ); // 设置策略对象
console.log( bonus.getBonus() ); // 输出:30000
刚刚我们用策略模式重构了这段计算年终奖的代码,可以看到通过策略模式重构之后,代码
变得更加清晰,各个类的职责更加鲜明。但这段代码是基于传统面向对象语言的模仿,下一节我
们将了解用 JavaScript 实现的策略模式。
5.2 JavaScript 版本的策略模式
实际上在 JavaScript 语言中,函数也是对象,所以更简单和直接的做法是把 strategy
直接定义为函数:
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
经过改造,代码的结构变得更加简洁
5.3 多态在策略模式中的体现
通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有
关的逻辑不再放在 Context 中,而是分布在各个策略对象中。Context 并没有计算奖金的能力,而
是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我
们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多
态性的体现,也是“它们可以相互替换”的目的。替换 Context 中当前保存的策略对象,便能执
行不同的算法来得到我们想要的结果。
5.4 使用策略模式实现缓动动画
动画在 Web 前端开发中的地位。一些别出心裁的
动画效果可以让网站增色不少。
如果我们明白了怎样让一个小球运动起来,那么离编写一个完整的游戏就不遥远了,剩下的
只是一些把逻辑组织起来的体力活。
5.4.1 实现动画效果的原理
JavaScript 实现动画效果的原理跟动画片的制作一样,动画片是把一些差距不大的原画以较快的帧数播放,来达到视觉上的动画效果。在 JavaScript 中,可以通过连续改变元素的某个 CSS
属性,比如 left、top、background-position 来实现动画效果。
……此处省略,因为都是写的动画
5.6 表单校验
var strategies = {
isNonEmpty: function( value, errorMsg ){ // 不为空
if ( value === '' ){
return errorMsg ;
}
},
minLength: function( value, length, errorMsg ){ // 限制最小长度
if ( value.length < length ){
return errorMsg;
}
},
isMobile: function( value, errorMsg ){ // 手机号码格式
if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
return errorMsg;
}
}
};
var validataFunc = function(){
var validator = new Validator(); // 创建一个 validator 对象
/***************添加一些校验规则****************/
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' );
validator.add( registerForm.password, 'minLength:6', '密码长度不能少于 6 位' );
validator.add( registerForm.phoneNumber, 'isMobile', '手机号码格式不正确' );
var errorMsg = validator.start(); // 获得校验结果
return errorMsg; // 返回校验结果
}
var registerForm = document.getElementById( 'registerForm' );
registerForm.onsubmit = function(){
var errorMsg = validataFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验
if ( errorMsg ){
alert ( errorMsg );
return false; // 阻止表单提交
}
};
var Validator = function(){
this.cache = []; // 保存校验规则
};
Validator.prototype.add = function( dom, rule, errorMsg ){
var ary = rule.split( ':' ); // 把 strategy 和参数分开
this.cache.push(function(){ // 把校验的步骤用空函数包装起来,并且放入 cache
var strategy = ary.shift(); // 用户挑选的 strategy
ary.unshift( dom.value ); // 把 input 的 value 添加进参数列表
ary.push( errorMsg ); // 把 errorMsg 添加进参数列表
return strategies[ strategy ].apply( dom, ary );
});
};
Validator.prototype.start = function(){
for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i ]; ){
var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
if ( msg ){ // 如果有确切的返回值,说明校验没有通过
return msg;
}
}
};
使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验,
这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地被移植到其他项
目中。
在修改某个校验规则的时候,只需要编写或者改写少量的代码。比如我们想将用户名输入框
的校验规则改成用户名不能少于 4 个字符。可以看到,这时候的修改是毫不费力的。代码如下:
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' );
// 改成:
validator.add( registerForm.userName, 'minLength:10', '用户名长度不能小于 10 位' );
5.6.3 给某个文本输入框添加多种校验规则
如果我们既想校验它是否为空,又想校验它输入文本的长度不小于 10 呢?我们期望以这样
的形式进行校验:
/***********************策略对象**************************/
var strategies = {
isNonEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};
/***********************Validator 类**************************/
var Validator = function () {
this.cache = [];
};
Validator.prototype.add = function (dom, rules) {
var self = this;
for (var i = 0, rule; rule = rules[i ];) {
(function (rule) {
var strategyAry = rule.strategy.split(':');
var errorMsg = rule.errorMsg;
self.cache.push(function () {
var strategy = strategyAry.shift();
strategyAry.unshift(dom.value);
strategyAry.push(errorMsg);
return strategies[strategy].apply(dom, strategyAry);
});
})(rule)
}
};
Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i ];) {
var errorMsg = validatorFunc();
if (errorMsg) {
return errorMsg;
}
}
};
/***********************客户调用代码**************************/
var registerForm = document.getElementById('registerForm');
var validataFunc = function () {
var validator = new Validator();
validator.add(registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于 10 位'
}]);
validator.add(registerForm.password, [{
strategy: 'minLength:6',
errorMsg: '密码长度不能小于 6 位'
}]);
validator.add(registerForm.phoneNumber, [{
strategy: 'isMobile',
errorMsg: '手机号码格式不正确'
}]);
var errorMsg = validator.start();
return errorMsg;
}
registerForm.onsubmit = function () {
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg);
return false;
}
};
5.7 策略模式的优缺点
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它
们易于切换,易于理解,易于扩展。 - 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻
便的替代方案。
当然,策略模式也有一些缺点,但这些缺点并不严重。
首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的
逻辑堆砌在 Context 中要好。
其次,要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,
这样才能选择一个合适的 strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选
择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的所有实现,这是违反最少
知识原则的。
第六章 代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
这里很多是概念,也不怎么重要了,就摘下6.9
6.9 用高阶函数动态创建代理
乘法、加
法、减法等创建缓存代理,代码如下:
/**************** 计算乘积 *****************/
var mult = function(){
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i ){
a = a * arguments[i];
}
return a;
};
/**************** 计算加和 *****************/
var plus = function(){
var a = 0;
for ( var i = 0, l = arguments.length; i < l; i ){
a = a arguments[i];
}
return a;
};
/**************** 创建缓存代理的工厂 *****************/
var createProxyFactory = function( fn ){
var cache = {};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if ( args in cache ){
return cache[ args ];
}
return cache[ args ] = fn.apply( this, arguments );
}
};
var proxyMult = createProxyFactory( mult ),
proxyPlus = createProxyFactory( plus );
alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24
alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24
alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10
alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10
第七章 迭代器模式
这里没什么重要的,就插个图吧。
第八章 发布—订阅模式
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状
态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型
来替代传统的发布—订阅模式。
现在看看如何一步步实现发布—订阅模式。
- 首先要指定好谁充当发布者(比如售楼处);
- 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册);
- 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函
数(遍历花名册,挨个发短信)。
另外,我们还可以往回调函数里填入一些参数,订阅者可以接收这些参数。这是很有必要的,
比如售楼处可以在发给订阅者的短信里加上房子的单价、面积、容积率等信息,订阅者接收到这
些信息之后可以进行各自的处理:
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = {}; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function( key, fn ){
if ( !this.clientList[ key ] ){ // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
this.clientList[ key ] = [];
}
this.clientList[ key ].push( fn ); // 订阅的消息添加进消息缓存列表
};
salesOffices.trigger = function(){ // 发布消息
var key = Array.prototype.shift.call( arguments ), // 取出消息类型
fns = this.clientList[ key ]; // 取出该消息对应的回调函数集合
if ( !fns || fns.length === 0 ){ // 如果没有订阅该消息,则返回
return false;
}
for( var i = 0, fn; fn = fns[ i ]; ){
fn.apply( this, arguments ); // (2) // arguments 是发布消息时附送的参数
}
};
salesOffices.listen( 'squareMeter88', function( price ){ // 小明订阅 88 平方米房子的消息
console.log( '价格= ' price ); // 输出: 2000000
});
salesOffices.listen( 'squareMeter110', function( price ){ // 小红订阅 110 平方米房子的消息
console.log( '价格= ' price ); // 输出: 3000000
});
salesOffices.trigger( 'squareMeter88', 2000000 ); // 发布 88 平方米房子的价格
salesOffices.trigger( 'squareMeter110', 3000000 ); // 发布 110 平方米房子的价格
发布-订阅模式的通用实现
现在我们已经看到了如何让售楼处拥有接受订阅和发布事件的功能。假设现在小明又去另一个售楼处买房子,那么这段代码是否必须在另一个售楼处对象上重写一次呢,有没有办法可以让
所有对象都拥有发布—订阅功能呢?
所以我们把发布—订阅的功能提取出来,放在一个单独的对象内:
var event = {
clientList: [],
listen: function( key, fn ){
if ( !this.clientList[ key ] ){
this.clientList[ key ] = [];
}
this.clientList[ key ].push( fn ); // 订阅的消息添加进缓存列表
},
trigger: function(){
var key = Array.prototype.shift.call( arguments ), // (1);
fns = this.clientList[ key ];
if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息
return false;
}
for( var i = 0, fn; fn = fns[ i ]; ){
fn.apply( this, arguments ); // (2) // arguments 是 trigger 时带上的参数
}
}
};
再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布—订阅功能:
var installEvent = function( obj ){
for ( var i in event ){
obj[ i ] = event[ i ];
}
};
//再来测试一番,我们给售楼处对象 salesOffices 动态增加发布—订阅功能:
var salesOffices = {};
installEvent( salesOffices );
salesOffices.listen( 'squareMeter88', function( price ){ // 小明订阅消息
console.log( '价格= ' price );
});
salesOffices.listen( 'squareMeter100', function( price ){ // 小红订阅消息
console.log( '价格= ' price );
});
salesOffices.trigger( 'squareMeter88', 2000000 ); // 输出:2000000
salesOffices.trigger( 'squareMeter100', 3000000 ); // 输出:3000000
取消订阅的事件
event.remove = function( key, fn ){
var fns = this.clientList[ key ];
if ( !fns ){ // 如果 key 对应的消息没有被人订阅,则直接返回
return false;
}
if ( !fn ){ // 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
fns && ( fns.length = 0 );
}else{
for ( var l = fns.length - 1; l >=0; l-- ){ // 反向遍历订阅的回调函数列表
var _fn = fns[ l ];
if ( _fn === fn ){
fns.splice( l, 1 ); // 删除订阅者的回调函数
}
}
}
};
var salesOffices = {};
var installEvent = function( obj ){
for ( var i in event ){
obj[ i ] = event[ i ];
}
}
installEvent( salesOffices );
salesOffices.listen( 'squareMeter88', fn1 = function( price ){ // 小明订阅消息
console.log( '价格= ' price );
});
salesOffices.listen( 'squareMeter88', fn2 = function( price ){ // 小红订阅消息
console.log( '价格= ' price );
});
salesOffices.remove( 'squareMeter88', fn1 ); // 删除小明的订阅
salesOffices.trigger( 'squareMeter88', 2000000 ); // 输出:2000000
真实的例子——网站登录
假如我们正在开发一个商城网站,网站里有 header 头部、nav 导航、消息列表、购物车等模块。
如果它们和用户信息模块产生了强耦合,比如下面这样
的形式:
login.succ(function(data){
header.setAvatar( data.avatar); // 设置 header 模块的头像
nav.setAvatar( data.avatar ); // 设置导航模块的头像
message.refresh(); // 刷新消息列表
cart.refresh(); // 刷新购物车列表
});
等到有一天,项目中又新增了一个收货地址管理的模块,这个模块本来是另一个同事所写的,在最后部分加上这行代码:
login.succ(function( data ){
header.setAvatar( data.avatar);
nav.setAvatar( data.avatar );
message.refresh();
cart.refresh();
address.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: function (data) {
console.log('设置 header 模块的头像');
}
}
})();
var nav = (function () { // nav 模块
login.listen('loginSucc', function (data) {
nav.setAvatar(data.avatar);
});
return {
setAvatar: function (avatar) {
console.log('设置 nav 模块的头像');
}
}
})();
如上所述,我们随时可以把 setAvatar 的方法名改成 setTouxiang。如果有一天在登录完成之
后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可,
而这可以让开发该模块的同事自己完成,你作为登录模块的开发者,永远不用再关心这些行为了。
代码如下:
var address = (function () { // nav 模块
login.listen('loginSucc', function (obj) {
address.refresh(obj);
});
return {
refresh: function (avatar) {
console.log('刷新收货地址列表');
}
}
})();
全局的发布-订阅对象
其实在现实中,买房子未必要亲自去售楼处,我们只要把订阅的请求交给中介公司,而各大
房产公司也只需要通过中介公司来发布房子信息。
同样在程序中,发布—订阅模式可以用一个全局的 Event 对象来实现,订阅者不需要了解消
息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event 作为一个类似“中介者”
的角色,把订阅者和发布者联系起来。见如下代码:
var Event = (function () {
var clientList = {},
listen,
trigger,
remove;
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
trigger = function () {
var key = Array.prototype.shift.call(arguments),
fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn; fn = fns[i ];) {
fn.apply(this, arguments);
}
};
remove = function (key, fn) {
var fns = clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
} else {
for (var l = fns.length - 1; l >= 0; l--) {
var _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1);
}
}
}
};
return {
listen: listen,
trigger: trigger,
remove: remove
}
})();
Event.listen('squareMeter88', function (price) { // 小红订阅消息
console.log('价格= ' price); // 输出:'价格=2000000'
});
Event.trigger('squareMeter88', 2000000); // 售楼处发布消息
全局事件的命名冲突
- 模块间通信
- 必须先订阅再发布吗
在提供最终的代码之前,我们来感受一下怎么使用这两个新增的功能。
/************** 先发布后订阅 ********************/
Event.trigger( 'click', 1 );
Event.listen( 'click', function( a ){
console.log( a ); // 输出:1
});
/************** 使用命名空间 ********************/
Event.create( 'namespace1' ).listen( 'click', function( a ){
console.log( a ); // 输出:1
});
Event.create( 'namespace1' ).trigger( 'click', 1 );
Event.create( 'namespace2' ).listen( 'click', function( a ){
console.log( a ); // 输出:2
});
Event.create( 'namespace2' ).trigger( 'click', 2 );
具体实现代码如下:
var Event = (function () {
var global = this,
Event,
_default = 'default';
Event = function () {
var _listen,
_trigger,
_remove,
_slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {},
_create,
find,
each = function (ary, fn) {
var ret;
for (var i = 0, l = ary.length; i < l; i ) {
var n = ary[i];
ret = fn.call(n, i, n);
}
return ret;
};
_listen = function (key, fn, cache) {
if (!cache[key]) {
cache[key] = [];
}
cache[key].push(fn);
};
_remove = function (key, cache, fn) {
if (cache[key]) {
if (fn) {
for (var i = cache[key].length; i >= 0; i--) {
if (cache[key][i] === fn) {
cache[key].splice(i, 1);
}
}
} else {
cache[key] = [];
}
}
};
_trigger = function () {
var cache = _shift.call(arguments),
key = _shift.call(arguments),
args = arguments,
_self = this,
ret,
stack = cache[key];
if (!stack || !stack.length) {
return;
}
return each(stack, function () {
return this.apply(_self, args);
});
};
_create = function (namespace) {
var namespace = namespace || _default;
var cache = {},
offlineStack = [], // 离线事件
ret = {
listen: function (key, fn, last) {
_listen(key, fn, cache);
if (offlineStack === null) {
return;
}
if (last === 'last') {
offlineStack.length && offlineStack.pop()();
} else {
each(offlineStack, function () {
this();
});
}
offlineStack = null;
},
one: function (key, fn, last) {
_remove(key, cache);
this.listen(key, fn, last);
},
remove: function (key, fn) {
_remove(key, cache, fn);
},
trigger: function () {
var fn,
args,
_self = this;
_unshift.call(arguments, cache);
args = arguments;
fn = function () {
return _trigger.apply(_self, args);
};
if (offlineStack) {
return offlineStack.push(fn);
}
return fn();
}
};
return namespace ?
(namespaceCache[namespace] ? namespaceCache[namespace] :
namespaceCache[namespace] = ret)
: ret;
};
return {
create: _create,
one: function (key, fn, last) {
var event = this.create();
event.one(key, fn, last);
},
remove: function (key, fn) {
var event = this.create();
event.remove(key, fn);
},
listen: function (key, fn, last) {
var event = this.create();
event.listen(key, fn, last);
},
trigger: function () {
var event = this.create();
event.trigger.apply(this, arguments);
}
};
}();
return Event;
})();
小结
发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常
广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。
第九章 命令模式
有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请
求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接
收者能够消除彼此之间的耦合关系。
在大型项目开发中,这是很正常的分工。对于绘制按钮的程序员来说,他完全不知道某个按
钮未来将用来做什么,可能用来刷新菜单界面,也可能用来增加一些子菜单,他只知道点击这个
按钮会发生某些事情。那么当完成这个按钮的绘制之后,应该如何给它绑定 onclick 事件呢?
<body>
<button id="button1">点击按钮 1</button>
<button id="button2">点击按钮 2</button>
<button id="button3">点击按钮 3</button>
</body>
<script>
var button1 = document.getElementById( 'button1' ),
var button2 = document.getElementById( 'button2' ),
var button3 = document.getElementById( 'button3' );
</script>
var setCommand = function( button, command ){
button.onclick = function(){
command.execute();
}
};
var MenuBar = {
refresh: function(){
console.log( '刷新菜单目录' );
}
};
var SubMenu = {
add: function(){
console.log( '增加子菜单' );
},
del: function(){
console.log( '删除子菜单' );
}
};
//在让 button 变得有用起来之前,我们要先把这些行为都封装在命令类中:
var RefreshMenuBarCommand = function( receiver ){
this.receiver = receiver;
};
RefreshMenuBarCommand.prototype.execute = function(){
this.receiver.refresh();
};
var AddSubMenuCommand = function( receiver ){
this.receiver = receiver;
};
AddSubMenuCommand.prototype.execute = function(){
this.receiver.add();
};
var DelSubMenuCommand = function( receiver ){
this.receiver = receiver;
};
DelSubMenuCommand.prototype.execute = function(){
console.log( '删除子菜单' );
};
//最后就是把命令接收者传入到 command 对象中,并且把 command 对象安装到 button 上面:
var refreshMenuBarCommand = new RefreshMenuBarCommand( MenuBar );
var addSubMenuCommand = new AddSubMenuCommand( SubMenu );
var delSubMenuCommand = new DelSubMenuCommand( SubMenu );
setCommand( button1, refreshMenuBarCommand );
setCommand( button2, addSubMenuCommand );
setCommand( button3, delSubMenuCommand );
以上只是一个很简单的命令模式示例,但从中可以看到我们是如何把请求发送者和请求接收
者解耦开的。
宏命令
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。想象一下,家
里有一个万能遥控器,每天回家的时候,只要按一个特别的按钮,它就会帮我们关上房间门,顺
便打开电脑并登录 QQ。
下面我们看看如何逐步创建一个宏命令。首先,我们依然要创建好各种 Command:
var closeDoorCommand = {
execute: function(){
console.log( '关门' );
}
};
var openPcCommand = {
execute: function(){
console.log( '开电脑' );
}
};
var openQQCommand = {
execute: function(){
console.log( '登录 QQ' );
}
};
var MacroCommand = function(){
return {
commandsList: [],
add: function( command ){
this.commandsList.push( command );
},
execute: function(){
for ( var i = 0, command; command = this.commandsList[ i ]; ){
command.execute();
}
}
}
};
var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute();
当然我们还可以为宏命令添加撤销功能,跟 macroCommand.execute
类似,当调用macroCommand.undo
方法时,宏命令里包含的所有子命令对象要依次执行各自的 undo 操作。
第十章 组合模式
更强大的宏命
目前的万能遥控器,包含了关门、开电脑、登录 QQ 这 3 个命令。现在我们需要一个“超级
万能遥控器”,可以控制家里所有的电器,这个遥控器拥有以下功能:
- 打开空调
- 打开电视和音响
- 关门、开电脑、登录 QQ
var MacroCommand = function(){
return {
commandsList: [],
add: function( command ){
this.commandsList.push( command );
},
execute: function(){
for ( var i = 0, command; command = this.commandsList[ i ]; ){
command.execute();
}
}
}
};
var openAcCommand = {
execute: function(){
console.log( '打开空调' );
}
};
/**********家里的电视和音响是连接在一起的,所以可以用一个宏命令来组合打开电视和打开音响的命令
*********/
var openTvCommand = {
execute: function(){
console.log( '打开电视' );
}
};
var openSoundCommand = {
execute: function(){
console.log( '打开音响' );
}
};
var macroCommand1 = MacroCommand();
macroCommand1.add( openTvCommand );
macroCommand1.add( openSoundCommand );
/*********关门、打开电脑和打登录 QQ 的命令****************/
var closeDoorCommand = {
execute: function(){
console.log( '关门' );
}
};
var openPcCommand = {
execute: function(){
console.log( '开电脑' );
}
};
var openQQCommand = {
execute: function(){
console.log( '登录 QQ' );
}
};
var macroCommand2 = MacroCommand();
macroCommand2.add( closeDoorCommand );
macroCommand2.add( openPcCommand );
macroCommand2.add( openQQCommand );
/*********现在把所有的命令组合成一个“超级命令”**********/
var macroCommand = MacroCommand();
macroCommand.add( openAcCommand );
macroCommand.add( macroCommand1 );
macroCommand.add( macroCommand2 );
/*********最后给遥控器绑定“超级命令”**********/
var setCommand = (function( command ){
document.getElementById( 'button' ).onclick = function(){
command.execute();
}
})( macroCommand );
当按下遥控器的按钮时,所有命令都将被依次执行.
每当对最上层的对象
进行一次请求时,实际上是在对整个树进行深度优先的搜索,而创建组合对象的程序员并不关心
这些内在的细节,往这棵树里面添加一些新的节点对象是非常容易的事情。
第十章 模板方法模式
模板方法模式是一种只需使用继承就可以实现的非常简单的模式。
在 Web 开发中也能找到很多模板方法模式的适用场景,比如我们在构建一系列的 UI 组件,
这些组件的构建过程一般如下所示:
- (1) 初始化一个 div 容器;
- (2) 通过 ajax 请求拉取相应的数据;
- (3) 把数据渲染到 div 容器里面,完成组件的构造;
- (4) 通知用户组件渲染完毕。
我们看到,任何组件的构建都遵循上面的 4 步,其中第(1)步和第(4)步是相同的。第(2)步不
同的地方只是请求 ajax 的远程地址,第(3)步不同的地方是渲染数据的方式。
于是我们可以把这 4 个步骤都抽象到父类的模板方法里面,父类中还可以顺便提供第(1)步和
第(4)步的具体实现。当子类继承这个父类之后,会重写模板方法里面的第(2)步和第(3)步。
第十二章 享元模式
享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量
级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
第十三章 职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间
的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
实际开发中的职责链模式
假设我们负责一个售卖手机的电商网站,经过分别交纳 500 元定金和 200 元定金的两轮预定
后(订单已在此时生成),现在已经到了正式购买的阶段。
公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用
户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金
的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。
下面我们把这个流程写成代码:
var order = function( orderType, pay, stock ){
if ( orderType === 1 ){ // 500 元定金购买模式
if ( pay === true ){ // 已支付定金
console.log( '500 元定金预购, 得到 100 优惠券' );
}else{ // 未支付定金,降级到普通购买模式
if ( stock > 0 ){ // 用于普通购买的手机还有库存
console.log( '普通购买, 无优惠券' );}else{
console.log( '手机库存不足' );
}
}
}
else if ( orderType === 2 ){ // 200 元定金购买模式
if ( pay === true ){
console.log( '200 元定金预购, 得到 50 优惠券' );
}else{
if ( stock > 0 ){
console.log( '普通购买, 无优惠券' );
}else{
console.log( '手机库存不足' );
}
}
} else if ( orderType === 3 ){
if ( stock > 0 ){
console.log( '普通购买, 无优惠券' );
}else{
console.log( '手机库存不足' );
}
}
};
order( 1 , true, 500); // 输出: 500 元定金预购, 得到 100 优惠券
虽然我们得到了意料中的运行结果,但这远远算不上一段值得夸奖的代码。order 函数不仅
巨大到难以阅读,而且需要经常进行修改。虽然目前项目能正常运行,但接下来的维护工作无疑
是个梦魇。恐怕只有最“新手”的程序员才会写出这样的代码。
用职责链模式重构代码
现在我们采用职责链模式重构这段代码,先把 500 元订单、200 元订单以及普通购买分成 3
个函数。
接下来把 orderType、pay、stock 这 3 个字段当作参数传递给 500 元订单函数,如果该函数不
符合处理条件,则把这个请求传递给后面的 200 元订单函数,如果 200 元订单函数依然不能处理
该请求,则继续传递请求给普通购买函数,代码如下:
// 500 元订单
var order500 = function( orderType, pay, stock ){
if ( orderType === 1 && pay === true ){
console.log( '500 元定金预购, 得到 100 优惠券' );
}else{
order200( orderType, pay, stock ); // 将请求传递给 200 元订单
} };
// 200 元订单
var order200 = function( orderType, pay, stock ){
if ( orderType === 2 && pay === true ){
console.log( '200 元定金预购, 得到 50 优惠券' );
}else{
orderNormal( orderType, pay, stock ); // 将请求传递给普通订单
}
};
// 普通购买订单
var orderNormal = function( orderType, pay, stock ){
if ( stock > 0 ){
console.log( '普通购买, 无优惠券' );
}else{
console.log( '手机库存不足' );
}
}; // 测试结果:
order500( 1 , true, 500); // 输出:500 元定金预购, 得到 100 优惠券
order500( 1, false, 500 ); // 输出:普通购买, 无优惠券
order500( 2, true, 500 ); // 输出:200 元定金预购, 得到 500 优惠券
order500( 3, false, 500 ); // 输出:普通购买, 无优惠券
order500( 3, false, 0 ); // 输出:手机库存不足
这依然是违反开放封闭原则的,如果有天我们要增加 300 元预订或者去掉 200 元预订,意
味着就必须改动这些业务函数内部。就像一根环环相扣打了死结的链条,如果要增加、拆除或者
移动一个节点,就必须得先砸烂这根链条。
灵活可拆分的职责链节点
var order500 = function( orderType, pay, stock ){
if ( orderType === 1 && pay === true ){
console.log( '500 元定金预购,得到 100 优惠券' );
}else{
return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
}
};
var order200 = function( orderType, pay, stock ){
if ( orderType === 2 && pay === true ){
console.log( '200 元定金预购,得到 50 优惠券' );
}else{
return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
}
};
var orderNormal = function( orderType, pay, stock ){
if ( stock > 0 ){
console.log( '普通购买,无优惠券' );
}else{
console.log( '手机库存不足' );
}
}; // Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点
var Chain = function( fn ){
this.fn = fn;
this.successor = null;
};
Chain.prototype.setNextSuccessor = function( successor ){
return this.successor = successor;
};
Chain.prototype.passRequest = function(){
var ret = this.fn.apply( this, arguments );
if ( ret === 'nextSuccessor' ){
return this.successor && this.successor.passRequest.apply( this.successor, arguments );
}
return ret;
};
//现在我们把 3 个订单函数分别包装成职责链的节点:
var chainOrder500 = new Chain( order500 );
var chainOrder200 = new Chain( order200 );
var chainOrderNormal = new Chain( orderNormal );
//然后指定节点在职责链中的顺序:
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
//最后把请求传递给第一个节点:
chainOrder500.passRequest( 1, true, 500 ); // 输出:500 元定金预购,得到 100 优惠券
chainOrder500.passRequest( 2, true, 500 ); // 输出:200 元定金预购,得到 50 优惠券
chainOrder500.passRequest( 3, true, 500 ); // 输出:普通购买,无优惠券
chainOrder500.passRequest( 1, false, 0 ); // 输出:手机库存不足
var order300 = function(){
// 具体实现略
};
chainOrder300= new Chain( order300 );
chainOrder500.setNextSuccessor( chainOrder300);
chainOrder300.setNextSuccessor( chainOrder200);
对于程序员来说,我们总是喜欢去改动那些相对容易改动的地方,就像改动框架的配置文件
远比改动框架的源代码简单得多。在这里完全不用理会原来的订单函数代码,我们要做的只是增
加一个节点,然后重新设置链中相关节点的顺序。```
职责链模式的优缺点
使用了职责链模式之后,链中的节点对象可以灵活地拆分重组。增加或者删除一个节
点,或者改变节点在链中的位置都是轻而易举的事情。这一点我们也已经看到,在上面的例子中,
增加一种订单完全不需要改动其他订单函数中的代码。
职责链模式使得程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分
节点并没有起到实质性的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,我们要避
免过长的职责链带来的性能损耗。
第十四章 中介者模式
中介者模式的例子——购买商品
假设我们正在编写一个手机购买的页面,在购买流程中,可以选择手机的颜色以及输入购买
数量,同时页面中有两个展示区域,分别向用户展示刚刚选择好的颜色和数量。还有一个按钮动
态显示下一步的操作,我们需要查询该颜色手机对应的库存,如果库存数量少于这次的购买数量,
按钮将被禁用并且显示库存不足,反之按钮可以点击并且显示放入购物车。
这个需求是非常容易实现的,假设我们已经提前从后台获取到了所有颜色手机的库存量:
var goods = { // 手机库存
"red": 3,
"blue": 6
};
那么页面有可能显示为如下几种场景:
选择红色手机,购买 4 个,库存不足。如图 14-8 所示。
图 14-8
选择蓝色手机,购买 5 个,库存充足,可以加入购物车。如图 14-9 所示。
或者是没有输入购买数量的时候,按钮将被禁用并显示相应提示。如图 14-10 所示。
开始编写代码
<body>
选择颜色: <select id="colorSelect">
<option value="">请选择</option>
<option value="red">红色</option>
<option value="blue">蓝色</option>
</select>
选择内存: <select id="memorySelect">
<option value="">请选择</option>
<option value="32G">32G</option>
<option value="16G">16G</option>
</select>
输入购买数量: <input type="text" id="numberInput"/><br/>
您选择了颜色: <div id="colorInfo"></div><br/>
您选择了内存: <div id="memoryInfo"></div><br/>
您输入了数量: <div id="numberInfo"></div><br/>
<button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button>
</body>
var colorSelect = document.getElementById( 'colorSelect' ),
numberInput = document.getElementById( 'numberInput' ),
memorySelect = document.getElementById( 'memorySelect' ),
colorInfo = document.getElementById( 'colorInfo' ),
numberInfo = document.getElementById( 'numberInfo' ),
memoryInfo = document.getElementById( 'memoryInfo' ),
nextBtn = document.getElementById( 'nextBtn' );
var goods = { // 手机库存
"red|32G": 3, // 红色 32G,库存数量为 3
"red|16G": 0,
"blue|32G": 1,
"blue|16G": 6
};
colorSelect.onchange = function(){
var color = this.value,
memory = memorySelect.value,
stock = goods[ color '|' memory ];
number = numberInput.value, // 数量
colorInfo.innerHTML = color;
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;
}
if ( number > stock ){ // 当前选择数量没有超过库存量
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
return ;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
};
memorySelect.onchange = function(){
var color = colorSelect.value, // 颜色
number = numberInput.value, // 数量
memory = this.value,
stock = goods[ color '|' memory ]; // 该颜色手机对应的当前库存
memoryInfo.innerHTML = memory;
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;
}
if ( number > stock ){ // 当前选择数量没有超过库存量
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
return ;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
};
很遗憾,我们仅仅是增加一个内存的选择条件,就要改变如此多的代码,这是因为在目前的
实现中,每个节点对象都是耦合在一起的,改变或者增加任何一个节点对象,都要通知到与其相
关的对象。
引入中介者
var goods = { // 手机库存
"red|32G": 3,
"red|16G": 0,
"blue|32G": 1,
"blue|16G": 6
};
var mediator = (function(){
var colorSelect = document.getElementById( 'colorSelect' ),
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.innerHTML = 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.innerHTML = '放入购物车';
}
}
})();
// 事件函数:
colorSelect.onchange = function(){
mediator.changed( this );
};
memorySelect.onchange = function(){
mediator.changed( this );
};
numberInput.oninput = function(){
mediator.changed( this );
};
可以想象,某天我们又要新增一些跟需求相关的节点,比如 CPU 型号,那我们只需要稍稍
改动 mediator 对象即可:
var goods = { // 手机库存
"red|32G|800": 3, // 颜色 red,内存 32G,cpu800,对应库存数量为 3
"red|16G|801": 0,
"blue|32G|800": 1,
"blue|16G|801": 6
};
var mediator = (function(){
// 略
var cpuSelect = document.getElementById( 'cpuSelect' );
return {
change: function(obj){
// 略
var cpu = cpuSelect.value,
stock = goods[ color '|' memory '|' cpu ];
if ( obj === cpuSelect ){
cpuInfo.innerHTML = cpu;
}
// 略
}
}
})();
小结
中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应
该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象
发生改变之后,难免会影响到其他的对象,跟“城门失火,殃及池鱼”的道理是一样的。而在中
介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。
第十五章 装饰者模式
在程序开发中,许多时候都并不希望某个类天
生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式。装饰者模式可以动态地
给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
数据统计上报
比如页面中有一个登录 button
,点击这个 button
会弹出登录浮层,与此同时要进行数据上报,
来统计有多少用户点击了这个登录button
:
<html>
<button tag="login" id="button">点击打开登录浮层</button>
<script>
var showLogin = function(){
console.log( '打开登录浮层' );
log( this.getAttribute( 'tag' ) );
}
var log = function( tag ){
console.log( '上报标签为: ' tag );
// (new Image).src = 'http:// xxx.com/report?tag=' tag; // 真正的上报代码略
}
document.getElementById( 'button' ).onclick = showLogin;
</script>
</html>
我们看到在 showLogin
函数里,既要负责打开登录浮层,又要负责数据上报,这是两个层面
的功能,在此处却被耦合在一个函数里。使用 AOP 分离之后,代码如下:
<html>
<button tag="login" id="button">点击打开登录浮层</button>
<script>
Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};
var showLogin = function(){
console.log( '打开登录浮层' );
}
var log = function(){
console.log( '上报标签为: ' this.getAttribute( 'tag' ) );
}
showLogin = showLogin.after( log ); // 打开登录浮层之后上报数据
document.getElementById( 'button' ).onclick = showLogin;
</script>
</html>
解决 CSRF
攻击最简单的一个办法就是在 HTTP 请求中带上一个Token
参数。
假设我们已经有一个用于生成 Token
的函数:
var getToken = function(){
return 'Token';
}
//现在的任务是给每个 ajax 请求都加上 Token 参数:
var ajax = function( type, url, param ){
param = param || {};
Param.Token = getToken(); // 发送 ajax 请求的代码略...
};
虽然已经解决了问题,但我们的 ajax
函数相对变得僵硬了,每个从ajax
函数里发出的请求
都自动带上了Token
参数,虽然在现在的项目中没有什么问题,但如果将来把这个函数移植到其
他项目上,或者把它放到一个开源库中供其他人使用,Token
参数都将是多余的。
也许另一个项目不需要验证 Token
,或者是 Token
的生成方式不同,无论是哪种情况,都必
须重新修改 ajax
函数。
为了解决这个问题,先把 ajax
函数还原成一个干净的函数:
var ajax= function( type, url, param ){
console.log(param); // 发送 ajax 请求的代码略
};
//然后把 Token 参数通过 Function.prototyte.before 装饰到 ajax 函数的参数 param 对象中:
var getToken = function(){
return 'Token';
}
ajax = ajax.before(function( type, url, param ){
param.Token = getToken();
});
ajax( 'get', 'http:// xxx.com/userinfo', { name: 'sven' } );
//从 ajax 函数打印的 log 可以看到,Token 参数已经被附加到了 ajax 请求的参数中:
{name: "sven", Token: "Token"}
明显可以看到,用 AOP 的方式给ajax
函数动态装饰上 Token 参数,保证了 ajax 函数是一
个相对纯净的函数,提高了 ajax 函数的可复用性,它在被迁往其他项目的时候,不需要做任何
修改。
插件式的表单验证
我们很多人都写过许多表单验证的代码,在一个 Web
项目中,可能存在非常多的表单,如
注册、登录、修改用户信息等。在表单数据提交给后台之前,常常要做一些校验,比如登录的时
候需要验证用户名和密码是否为空,代码如下:
<html>
<body>
用户名:<input id="username" type="text"/>
密码: <input id="password" type="password"/>
<input id="submitBtn" type="button" value="提交">
</body>
<script>
var username = document.getElementById( 'username' ),
password = document.getElementById( 'password' ),
submitBtn = document.getElementById( 'submitBtn' );
var formSubmit = function(){
if ( username.value === '' ){
return alert ( '用户名不能为空' );
}
if ( password.value === '' ){
return alert ( '密码不能为空' );
}
var param = {
username: username.value,
password: password.value
}
ajax( 'http:// xxx.com/login', param ); // ajax 具体实现略
}
submitBtn.onclick = function(){
formSubmit();
}
</script>
</html>
formSubmit
函数在此处承担了两个职责,除了提交 ajax
请求之外,还要验证用户输入的合法
性。这种代码一来会造成函数臃肿,职责混乱,二来谈不上任何可复用性。
var validata = function(){
if ( username.value === '' ){
alert ( '用户名不能为空' );
return false;
}
if ( password.value === '' ){
alert ( '密码不能为空' );
return false;
}
}
var formSubmit = function(){
if ( validata() === false ){ // 校验未通过
return;
}
var param = {
username: username.value,
password: password.value
}
ajax( 'http:// xxx.com/login', param );
}
submitBtn.onclick = function(){
formSubmit();
}
现在的代码已经有了一些改进,我们把校验的逻辑都放到了validata
函数中,但 formSubmit
函数的内部还要计算validata
函数的返回值,因为返回值的结果表明了是否通过校验。
接下来进一步优化这段代码,使 validata
和 formSubmit
完全分离开来。首先要改写 Function.
prototype.before
,如果beforefn
的执行结果返回 false
,表示不再执行后面的原函数,代码如下:
Function.prototype.before = function( beforefn ){
var __self = this;
return function(){
if ( beforefn.apply( this, arguments ) === false ){
// beforefn 返回 false 的情况直接 return,不再执行后面的原函数
return;
}
return __self.apply( this, arguments );
}
}
var validata = function(){
if ( username.value === '' ){
alert ( '用户名不能为空' );
return false;
}
if ( password.value === '' ){
alert ( '密码不能为空' );
return false;
}
}
var formSubmit = function(){
var param = {
username: username.value,
password: password.value
}
ajax( 'http:// xxx.com/login', param );
}
formSubmit = formSubmit.before( validata );
submitBtn.onclick = function(){
formSubmit();
}
在这段代码中,校验输入和提交表单的代码完全分离开来,它们不再有任何耦合关系,formSubmit = formSubmit.before( validata )
这句代码,如同把校验规则动态接在 formSubmit
函数
之前,validata
成为一个即插即用的函数,它甚至可以被写成配置文件的形式,这有利于我们分
开维护这两个函数。再利用策略模式稍加改造,我们就可以把这些校验规则都写成插件的形式,
用在不同的项目当中。
值得注意的是,因为函数通过 Function.prototype.before
或者 Function.prototype.after
被装
饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失。
代码如下:
var func = function(){
alert( 1 );
}
func.a = 'a';
func = func.after( function(){
alert( 2 );
});
alert ( func.a ); // 输出:undefined
另外,这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些
影响。
第十六章 状态模式
状态模式是一种非同寻常的优秀模式,它也许是解决某些需求场景的最好方法。虽然状态模
式并不是一种简单到一目了然的模式(它往往还会带来代码量的增加),但你一旦明白了状态模
式的精髓,以后一定会感谢它带给你的无与伦比的好处。
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
第一个例子:电灯程序
有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时
按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同
的状态下,表现出来的行为是不一样的。
首先给出不用状态模式的电灯程序实现:
var Light = function(){
this.state = 'off'; // 给电灯设置初始状态 off
this.button = null; // 电灯开关按钮
};
Light.prototype.init = function(){
var button = document.createElement( 'button' ),
self = this;
button.innerHTML = '开关';
this.button = document.body.appendChild( button );
this.button.onclick = function(){
self.buttonWasPressed();
}
};
Light.prototype.buttonWasPressed = function(){
if ( this.state === 'off' ){
console.log( '开灯' );
this.state = 'on';
}else if ( this.state === 'on' ){
console.log( '关灯' );
this.state = 'off';
}
};
var light = new Light();
light.init();
OK,现在可以看到,我们已经编写了一个强壮的状态机.
令人遗憾的是,这个世界上的电灯并非只有一种。许多酒店里有另外一种电灯,这种电灯也
只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电
灯。现在必须改造上面的代码来完成这种新型电灯的制造:
Light.prototype.buttonWasPressed = function(){
if ( this.state === 'off' ){
console.log( '弱光' );
this.state = 'weakLight';
}else if ( this.state === 'weakLight' ){
console.log( '强光' );
this.state = 'strongLight';
}else if ( this.state === 'strongLight' ){
console.log( '关灯' );
this.state = 'off';
}
};
很明显buttonWasPressed
方法是违反开放封闭原则的,每次新增或者修改light
的状态,
都需要改动 buttonWasPressed
方法中的代码,这使得 buttonWasPressed
成为了一个非常不
稳定的方法。
下面进入状态模式的代码编写阶段,首先将定义 3 个状态类,分别是 offLightState
、WeakLightState
、strongLightState
。这 3 个类都有一个原型方法 buttonWasPressed
,代表在各自状
态下,按钮被按下时将发生的行为,代码如下:
// OffLightState:
var OffLightState = function( light ){
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function(){
console.log( '弱光' ); // offLightState 对应的行为
this.light.setState( this.light.weakLightState ); // 切换状态到 weakLightState
};
// WeakLightState:
var WeakLightState = function( light ){
this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function(){
console.log( '强光' ); // weakLightState 对应的行为
this.light.setState( this.light.strongLightState ); // 切换状态到 strongLightState
};
// StrongLightState:
var StrongLightState = function( light ){
this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function(){
console.log( '关灯' ); // strongLightState 对应的行为
this.light.setState( this.light.offLightState ); // 切换状态到 offLightState
};
接下来改写 Light
类,现在不再使用一个字符串来记录当前的状态,而是使用更加立体化的
状态对象。我们在Light
类的构造函数里为每个状态类都创建一个状态对象,这样一来我们可以
很明显地看到电灯一共有多少种状态,代码如下:
var Light = function(){
this.offLightState = new OffLightState( this );
this.weakLightState = new WeakLightState( this );
this.strongLightState = new StrongLightState( this );
this.button = null;
};
在 button
按钮被按下的事件里,Context
也不再直接进行任何实质性的操作,而是通过self.currState.buttonWasPressed()
将请求委托给当前持有的状态对象去执行,代码如下:
Light.prototype.init = function(){
var button = document.createElement( 'button' ),
self = this;
this.button = document.body.appendChild( button );
this.button.innerHTML = '开关';
this.currState = this.offLightState; // 设置当前状态
this.button.onclick = function(){
self.currState.buttonWasPressed();
}
};
最后还要提供一个 Light.prototype.setState
方法,状态对象可以通过这个方法来切换 light
对象的状态。前面已经说过,状态的切换规律事先被完好定义在各个状态类中。在 Context
中再
也找不到任何一个跟状态切换相关的条件分支语句:
Light.prototype.setState = function( newState ){
this.currState = newState;
};
//现在可以进行一些测试:
var light = new Light();
light.init();
当我们需要为light
对象增加一种新的状态时,只需要增加一个新的状态类,再稍稍改变一
些现有的代码即可。假设现在 light 对象多了一种超强光的状态,那就先增加 SuperStrongLightState
类:
var SuperStrongLightState = function( light ){
this.light = light;
};
SuperStrongLightState.prototype.buttonWasPressed = function(){
console.log( '关灯' );
this.light.setState( this.light.offLightState );
};
//然后在 Light 构造函数里新增一个 superStrongLightState 对象:
var Light = function(){
this.offLightState = new OffLightState( this );
this.weakLightState = new WeakLightState( this );
this.strongLightState = new StrongLightState( this );
this.superStrongLightState = new SuperStrongLightState( this ); // 新增 superStrongLightState 对象
this.button = null;
};
//最后改变状态类之间的切换规则,从 StrongLightState---->OffLightState 变为 StrongLightState---->SuperStrongLightState---->OffLightState:
StrongLightState.prototype.buttonWasPressed = function(){
console.log( '超强光' ); // strongLightState 对应的行为
this.light.setState( this.light.superStrongLightState ); // 切换状态到 offLightState
};
第十七章 适配器模式
当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。
这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿
到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种办法是创建
一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。
现实中的适配器
- 港式插头转换器
- 电源适配器
- USB 转接口
适配器模式的应用
当我们向 googleMap
和 baiduMap
都发出“显示”请求时,googleMap
和 baiduMap
分别以各自的方式在页面中展现了地图:
var googleMap = {
show: function(){
console.log( '开始渲染谷歌地图' );
}
};
var baiduMap = {
show: function(){
console.log( '开始渲染百度地图' );
}
};
var renderMap = function( map ){
if ( map.show instanceof Function ){
map.show();
}
};
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMap ); // 输出:开始渲染百度地图
这段程序得以顺利运行的关键是 googleMap
和 baiduMap
提供了一致的show
方法,但第三方的
接口方法并不在我们自己的控制范围之内,假如 baiduMap
提供的显示地图的方法不叫 show
而叫display
呢?
baiduMap
这个对象来源于第三方,正常情况下我们都不应该去改动它。此时我们可以通过增
加 baiduMapAdapter
来解决问题:
var googleMap = {
show: function(){
console.log( '开始渲染谷歌地图' );
}
};
var baiduMap = {
display: function(){
console.log( '开始渲染百度地图' );
}
};
var baiduMapAdapter = {
show: function(){
return baiduMap.display();
}
};
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMapAdapter ); // 输出:开始渲染百度地图
再来看看另外一个例子。假设我们正在编写一个渲染广东省地图的页面。目前从第三方资源
里获得了广东省的所有城市以及它们所对应的 ID
,并且成功地渲染到页面中:
var getGuangdongCity = function(){
var guangdongCity = [
{
name: 'shenzhen',
id: 11,
}, {
name: 'guangzhou',
id: 12,
}
];
return guangdongCity;
};
var render = function( fn ){
console.log( '开始渲染广东省地图' );
document.write( JSON.stringify( fn() ) );
};
render( getGuangdongCity );
利用这些数据,我们编写完成了整个页面,并且在线上稳定地运行了一段时间。但后来发现
这些数据不太可靠,里面还缺少很多城市。于是我们又在网上找到了另外一些数据资源,这次的
数据更加全面,但遗憾的是,数据结构和正运行在项目中的并不一致。新的数据结构如下:
var guangdongCity = {
shenzhen: 11,
guangzhou: 12,
zhuhai: 13
};
除了大动干戈地改写渲染页面的前端代码之外,另外一种更轻便的解决方式就是新增一个数
据格式转换的适配器:
var getGuangdongCity = function(){
var guangdongCity = [
{
name: 'shenzhen',
id: 11,
}, {
name: 'guangzhou',
id: 12,
}
];
return guangdongCity;
};
var render = function( fn ){
console.log( '开始渲染广东省地图' );
document.write( JSON.stringify( fn() ) );
};
var addressAdapter = function( oldAddressfn ){
var address = {},
oldAddress = oldAddressfn();
for ( var i = 0, c; c = oldAddress[ i ]; ){
address[ c.name ] = c.id;
}
return function(){
return address;
}
};
render( addressAdapter( getGuangdongCity ) );
那么接下来需要做的,就是把代码中调用getGuangdongCity
的地方,用经过 addressAdapter
适配器转换之后的新函数来代替。
第十八章 单一职责原则
单一职责原则(SRP) 原则体现为:一个对象(方法)只做一件事情。
如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如在 ajax
请求的时候,创建 xhr 对象和发送 xhr 请求几乎总是在一起的,那么创建 xhr 对象的职责和发送
xhr 请求的职责就没有必要分开。
第十九章 最少知识原则
最少知识原则(LKP)说的是一个软件实体应当尽可能少地与其他实体发生相互作用。这
里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。
最少知识原则在设计模式中体现得最多的地方是中介者模式和外观模式,下面我们分别进行
介绍。
- 中介者模式
在世界杯期间购买足球彩票,如果没有博彩公司作为中介,上千万的人一起计算赔率和输赢
绝对是不可能的事情。博彩公司作为中介,每个人都只和博彩公司发生关联,博彩公司会根据所
有人的投注情况计算好赔率,彩民们赢了钱就从博彩公司拿,输了钱就赔给博彩公司。
中介者模式很好地体现了最少知识原则。通过增加一个中介者对象,让所有的相关对象都通
过中介者对象来通信,而不是互相引用。所以,当一个对象发生改变时,只需要通知中介者对象
即可。
假设我们要编写一个具有缓存效果的计算乘积的函数 function mult (){},我们需要一个对
象 var cache = {}来保存已经计算过的结果。cache 对象显然只对 mult 有用,把 cache 对象放在
mult 形成的闭包中,显然比把它放在全局作用域更加合适,代码如下:
var mult = (function(){
var cache = {};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if ( cache[ args ] ){
return cache[ args ];
}
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return cache[ args ] = a;
}
})();
mult( 1, 2, 3 ); // 输出: 6
第二十章 开放-封闭原则
在面向对象的程序设计中,开放封闭原则(OCP)是最重要的一条原则。很多时候,一个
程序具有良好的设计,往往说明它是符合开放封闭原则的。
- 扩展 window.onload 函数
假设我们是一个大型 Web
项目的维护人员,在接手这个项目时,发现它已经拥有 10 万行以
上的 JavaScript
代码和数百个JS
文件。
不久后接到了一个新的需求,即在window.onload
函数中打印出页面中的所有节点数量。这
当然难不倒我们了。于是我们打开文本编辑器,搜索出 window.onload
函数在文件中的位置,在
函数内部添加以下代码:
window.onload = function(){
// 原有代码略
console.log( document.getElementsByTagName( '*' ).length );
};
而我们的需求又不仅仅是打印一个 log 这么简单。那么“改好一个 bug
,引发
其他bug
”这样的事情就很可能会发生。我们永远不知道刚刚的改动会有什么副作用,很可能会
引发一系列的连锁反应。
有没有办法在不修改代码的情况下,就能满足新需求呢?
Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};
window.onload = ( window.onload || function(){} ).after(function(){
console.log( document.getElementsByTagName( '*' ).length );
});
通过动态装饰函数的方式,我们完全不用理会从前window.onload
函数的内部实现,无论它
的实现优雅或是丑陋。
- 用对象的多态性消除条件分支
过多的条件分支语句是造成程序违反开放封闭原则的一个常见原因。每当需要增加一个新
的 if
语句时,都要被迫改动原函数。把if
换成switch-case
是没有用的,这是一种换汤不换药
的做法。实际上,每当我们看到一大片的if
或者 swtich-case
语句时,第一时间就应该考虑,能
否利用对象的多态性来重构它们。
利用对象的多态性来让程序遵守开放封闭原则,是一个常用的技巧。我们依然选用 1.2 节中
让动物发出叫声的例子。下面先提供一段不符合开放封闭原则的代码。每当我们增加一种新的
动物时,都需要改动 makeSound
函数的内部实现:
var makeSound = function( animal ){
if ( animal instanceof Duck ){
console.log( '嘎嘎嘎' );
}else if ( animal instanceof Chicken ){
console.log( '咯咯咯' );
}
};
var Duck = function(){};
var Chicken = function(){};
makeSound( new Duck() ); // 输出:嘎嘎嘎
makeSound( new Chicken() ); // 输出:咯咯咯
//动物世界里增加一只狗之后,makeSound 函数必须改成:
var makeSound = function( animal ){
if ( animal instanceof Duck ){
console.log( '嘎嘎嘎' );
}else if ( animal instanceof Chicken ){
console.log( '咯咯咯' );
}else if ( animal instanceof Dog ){ // 增加跟狗叫声相关的代码
console.log('汪汪汪' );
}
};
var Dog = function(){};
makeSound( new Dog() ); // 增加一只狗
利用多态的思想,我们把程序中不变的部分隔离出来(动物都会叫),然后把可变的部分封
装起来(不同类型的动物发出不同的叫声),这样一来程序就具有了可扩展性。当我们想让一只
狗发出叫声时,只需增加一段代码即可,而不用去改动原有的makeSound
函数:
var makeSound = function( animal ){
animal.sound();
};
var Duck = function(){};
Duck.prototype.sound = function(){
console.log( '嘎嘎嘎' );
};
var Chicken = function(){};
Chicken.prototype.sound = function(){
console.log( '咯咯咯' );
};
makeSound( new Duck() ); // 嘎嘎嘎
makeSound( new Chicken() ); // 咯咯咯
/********* 增加动物狗,不用改动原有的 makeSound 函数 ****************/
var Dog = function(){};
Dog.prototype.sound = function(){
console.log( '汪汪汪' );
};
makeSound( new Dog() ); // 汪汪汪
第二十一章 接口和面向接口编程
过鸭子类型的概念:
“如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。”
书上说的很多,其实就是说js不像java那样的强语言,没有类型检查,我们需要增加一些防御代码,或者使用Typescript
编写代码。
在Object.prototype.toString.call( [] ) === '[object Array]'
被发现之前,我们经常用鸭子
类型的思想来判断一个对象是否是一个数组,代码如下:
var isArray = function( obj ){
return obj &&
typeof obj === 'object' &&
typeof obj.length === 'number' &&
typeof obj.splice === 'function'
};
第二十二章 代码重构
提炼函数
- 避免出现超大函数。
- 独立出来的函数有助于代码复用。
- 独立出来的函数更容易被覆写。
- 独立出来的函数如果拥有一个良好的命名,它本身就起到了注释的作用。
合并重复的条件片段
把条件分支语句提炼成函数
合理使用循环
在函数体内,如果有些代码实际上负责的是一些重复性的工作,那么合理利用循环不仅可以
完成同样的功能,还可以使代码量更少。下面有一段创建 XHR 对象的代码,为了简化示例,我们
只考虑版本 9 以下的 IE 浏览器,代码如下:
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;
};
嵌套的条件分支语句绝对是代码维护者的噩梦,对于阅读代码的人来说,嵌套的 if、else
语句相比平铺的 if、else,在阅读和理解上更加困难,有时候一个外层 if 分支的左括号和右括
号之间相隔 500 米之远。
重构后的 del 函数如下:
var del = function( obj ){
if ( obj.isReadOnly ){ // 反转 if 表达式
return;
}
if ( obj.isFolder ){
return deleteFolder( obj );
}
if ( obj.isFile ){
return deleteFile( obj );
}
};
传递对象参数代替过长的参数列表
有时候一个函数有可能接收多个参数,而参数的数量越多,函数就越难理解和使用。
这时我们可以把参数都放入一个对象内,
函数需要的数据可以自行从该对象里获取。现在不用再关心参数的数量和顺序,只要保证参数对
应的 key 值不变就可以了
尽量减少参数数量
少用三目运算符
即使我们假设三目运算符的效率真的比 if、else 高,这点差距也是完全可以忽略不计的。
在实际的开发中,即使把一段代码循环一百万次,使用三目运算符和使用 if、else 的时间开销
处在同一个级别里。
同样,相比损失的代码可读性和可维护性,三目运算符节省的代码量也可以忽略不计。让 JS
文件加载更快的办法有很多种,如压缩、缓存、使用 CDN 和分域名等。把注意力只放在使用三
目运算符节省的字符数量上,无异于一个 300 斤重的人把超重的原因归罪于头皮屑。
如果条件分支逻辑简单且清晰,这无碍我们使用三目运算符:var global = typeof window !== "undefined" ? window : this;
但如果条件分支逻辑非常复杂,如下段代码所示,那我们最好的选择还是按部就班地编写if、else。if、else
语句的好处很多,一是阅读相对容易,二是修改的时候比修改三目运算符周
围的代码更加方便:
if ( !aup || !bup ) {
return a === doc ? -1 :
b === doc ? 1 :
aup ? -1 :
bup ? 1 :
sortInput ?
( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
0;
}
合理使用链式调用
User.setId( 1314 ).setName( 'sven' )
如果该链条的结构相对稳定,后期不易发生修改,那么使用链式调用无可厚非。但如果该链
条很容易发生变化,导致调试和维护困难,那么还是建议使用普通调用的形式:
var user = new User();
user.setId( 1314 );
user.setName( 'sven' );
分解大型类
用 return 退出多重循环
假设在函数体内有一个两重循环语句,我们需要在内层循环中判断,当达到某个临界条件时
退出外层的循环。我们大多数时候会引入一个控制标记变量:
var func = function(){
var flag = false;
for ( var i = 0; i < 10; i++ ){
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
flag = true;
break;
}
}
if ( flag === true ){
break;
}
}
};
第二种做法是设置循环标记:
var func = function(){
outerloop:
for ( var i = 0; i < 10; i++ ){
innerloop:
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
break outerloop;
}
}
}
};
这两种做法无疑都让人头晕目眩,更简单的做法是在需要中止循环的时候直接退出整个方法:
var func = function(){
for ( var i = 0; i < 10; i++ ){
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
return;
}
}
}
};
当然用 return 直接退出方法会带来一个问题,如果在循环之后还有一些将被执行的代码呢?
如果我们提前退出了整个方法,这些代码就得不到被执行的机会:
var func = function(){
for ( var i = 0; i < 10; i++ ){
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
return;
}
}
}
console.log( i ); // 这句代码没有机会被执行
};
为了解决这个问题,我们可以把循环后面的代码放到 return 后面,如果代码比较多,就应
该把它们提炼成一个单独的函数:
var print = function( i ){
console.log( i );
};
var func = function(){
for ( var i = 0; i < 10; i++ ){
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
return print( i );
}
}
}
};
func();
作者:刘伟波
链接:http://www.liuweibo.cn/p/208
来源:刘伟波博客
本文原创版权属于刘伟波 ,转载请注明出处,谢谢合作
发表评论: