(一)补充重要知识点:
(一)this、call和apply
1、this的指向
(1)作为对象的方法调用--------指向该对象
(2)作为普通函数调用 ------指向全局对象,如果不想指向全局对象,那么需要在函数内部定义保存引用的变量。
(3)构造器调用-------通常指向返回的对象,但是如果构造器显示地返回了一个object类型的对象,那么此次运算结果最终会返回这个对象。
(4)Function.protype.call或者Function.protype.apply调用-----动态改变传入函数的this。
2、call和apply
(1)两个作用一模一样,区别在于传入参数不同
(2)apply接收两个参数,第一个参数制定了函数体内的this的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数。
(3)call传入的采纳数不固定,第一个参数也是函数体内的this指向,从第二个参数开始往后,每个参数依次传入函数。call是包装在apply上的一颗语法糖。
(4)当使用call或者apply时,如果第一个参数为null,则函数体内的this会指向默认的宿主对象,在浏览器中是window,但是在严格模式下,函数体内的this还是null。
(5)call和apply的用途:
a、改变this指向
b、Function.protype.bind用来指定函数内部的this指向
c、借用其他对象的方法
(二)闭包和高阶函数
1、闭包
(1)封装变量为私有变量
(2)延续局部变量的生命周期
(3)实现命令模式
(4)解决循环引用带来的内存泄漏问题,需要将循环引用中的变量设置为null,意味着切断变量与它之前引用的值之间的连接,当垃圾收集器下次运行时,将会删除这些并回收他们占用的内存。
2、高阶函数
(1)函数作为参数传递
a、回调函数---可以将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 );
});
b、Array.protype.sort-----接收一个函数作为参数,这个函数封装了数组元素的排序规则。从大到小和从小到大排列
[ 1, 4, 3 ].sort( function( a, b ){
return a - b;
});
[ 1, 4, 3 ].sort( function( a, b ){
return b - a;
});
(2)函数作为返回值传递
a、判断数据的类型
//所以我们可以编写一系列的isType 函数。代码如下:
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]';
};
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
var getSingle = function ( fn ) {
var ret;
return function () {
return ret || ( ret = fn.apply( this, arguments ) );
};
};
b、getSingle
略
(3)高阶函数实现AOP
AOP:把一些跟核心业务逻辑块无关的功能抽离出来,包括日志统计、安全控制和异常处理等,优点是保持业务逻辑模块的纯净和高内聚性,其次是可以方便地复用日志统计等功能模块。
在javascript中实现AOP,都是指把一个函数“动态织入”到另外一个函数中,夏利通过扩展Function.protype实现
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();
(4)高阶函数的其他应用
a、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
b、uncurrying
Function.prototype.uncurrying = function () {
var self = this;
return function() {
var obj = Array.prototype.shift.call( arguments );
return self.apply( obj, arguments );
};
};
另一种实现
Function.prototype.uncurrying = function(){
var self = this;
return function(){
return Function.prototype.call.apply( self, arguments );
}
};
c、函数节流
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 );
d、分时函数
创建webqq的列表,短时间内插入大量的dom节点
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 ); // 分批执行的时间间隔,也可以用参数的形式传入
};
};
e、惰性加载
嗅探浏览器
第一种普通方法
var addEvent = function( elem, type, handler ){
if ( window.addEventListener ){
return elem.addEventListener( type, handler, false );
}
if ( window.attachEvent ){
return elem.attachEvent( 'on' + type, handler );
}
};
第二种把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候进行一次判断,以便让addEvent返回一个包裹了正确逻辑的函数。
var addEvent = (function(){
if ( window.addEventListener ){
return function( elem, type, handler ){
elem.addEventListener( type, handler, false );
}
}
if ( window.attachEvent ){
return function( elem, type, handler ){
elem.attachEvent( 'on' + type, handler );
}
}
})();
第三种惰性载入方案,第一次进入时有分支函数,下一次进入不存在分支函数
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);
});
(二)设计模式
一、原型模式
通过克隆创建对象。
二、单例模式
保证一个类仅有一个实例,并提供一个访问他的全局访问点。
var Singleton = function(name) {
this.name = name;
this.instance = null;
};
Singleton.prototype.getName = function() {
alert(this.name);
};
Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
};
var a = Singleton.getInstance('seven1');
var b = Singleton.getInstance('seven2');
三、策略模式
定义一系列的算法,把他们一个个封装起来,并且是他们可以相互替换。
四、代理模式
为一个对象提供一个代用品或者占位符,以便控制对它的访问。
1、实现图片预加载
var myImage = (function(){
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return {
setSrc :function(src){
imgNode.src = src;
}
}
})();
myImage.setSrc = (url);
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage.setSrc(this.src);
}
return{
setSrc: function(src){
myImage.setSrc('file://c:/Users/...');//此处填占位图片
img.src = src;
}
}
})();
proxyImage.setSrc('file://c:/Users/...');//此处填真实图片
2、缓存代理
var multi = function(){
var a = 1;
for(var i = 0; l = arguments.length; i < l; i++){
a *= arguments[i];
}
return a;
}
var proxyMult = (function(){
var cache = {};
return function(){
var args = Array.prototype.join.call(argument, ',');
if(args in cache){
return cache[args];
}
return cache[args] = mult.apply(this, arguments);
}
})();
porxyMult(1,2,3,4);
porxyMult(1,2,3,4);//第二次没有计算,直接返回结果
五、迭代器模式
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。$.each()
1、实现一个each函数
var each = function(ary, callback){
for(var i = 0, l = ary.length; i < l; ++i){
callback.call(ary[i], i , ary[i]);
}
};
each([1,2,3], function(i, n){
alert([i, n]);
});
2、内外部迭代器
each函数属于内部迭代器
需求如下:判断两个数组里的元素是否完全相等。
var compare = function(ary1, ary2){
if(ary1.length !== ary2.length){
throw new Error('ary1和ary2不相等');
}
each(ary1, function(i, n){
if( n !== ary2[i]){
throw new Error('ary1和ary2不相等');
}
});
alert('ary1和ary2相等');
}
compare([1,2,3], [1,2,4]);
外部迭代器必须显示地请求迭代下一个元素,《松本行弘的程序世界》第四章用Raby实现外部迭代器,改为javascript实现。
var Iterator = function( obj ){
var current = 0;
var next = function(){
current += 1;
};
var isDone = function(){
return current >= obj.length;
};
var getCurrItem = function(){
return obj[ current ];
};
return {
next : next,
isDone : isDone,
getCurrItem : getCurrItem
}
}
改写compare函数:
var compare = function(iterator1, iterator2){
while( !iterator.isDone() && !iterator.isDone()){
if(!iterator1.getCurrItem() !== iterator2.getCurrItem()){
throw new Error('iterator1和iterator2不相等');
}
iterator1.next();
iterator2.next();
}
alert('iterator1和iterator2相等');
}
var iterator1 = Iterator([1,2,3]);
var iterator2 = Iterator([1,2,3]);
compare(iterator1, iterator2);
3、倒序访问迭代器
var reverseEach = function(ary, callback){
for( var l = ary.length - 1; l >= 0l l--){
callback(l, ary[l]);
}
};
revaerseEach([0,1,2], function(i, n){
console.log(n);
});
4、中止迭代器
提前结束循环
5、迭代器应用举例:根据不同的浏览器获取上传组件
六、发布-订阅模式
又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知。在js开发中,我们一般用事件模型来替代传统的发布-订阅模式。
实现方法:
(1)指定发布者(售楼处)
(2)给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册)
(3)最后发布消息的时候,发布者会遍历这个缓存列表,一次出发里面存放的订阅者回调函数(遍历花名册,挨个发短信)
1、简单的发布-订阅模式
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function( fn ){ // 增加订阅者
this.clientList.push( fn ); // 订阅的消息添加进缓存列表
};
salesOffices.trigger = function(){ // 发布消息
for( var i = 0, fn; fn = this.clientList[ i++ ]; ){
fn.apply( this, arguments ); // (2) // arguments 是发布消息时带上的参数
}
};
//下面我们来进行一些简单的测试:
salesOffices.listen( function( price, squareMeter ){ // 小明订阅消息
console.log( '价格= ' + price );
console.log( 'squareMeter= ' + squareMeter );
});
salesOffices.listen( function( price, squareMeter ){ // 小红订阅消息
console.log( '价格= ' + price );
console.log( 'squareMeter= ' + squareMeter );
});
salesOffices.trigger( 2000000, 88 ); // 输出:200 万,88 平方米
salesOffices.trigger( 3000000, 110 ); // 输出:300 万,110 平方米
缺点是小明只想买88平的房子,但是发布者把110平的信息也推送给了小明,对小明造成干扰,增加key,让订阅者只订阅自己感兴趣的消息。
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 时带上的参数
}
}
};
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
发布订阅的好处在于一旦发布某种事件,则业务方只需要坚听这种事件方法,一旦事件发生,业务方就可执行对应的代码,典型事件有dom绑定事件和ajax。同时可以使用全局的Event对象做终结者把订阅者和发布者联系起来。
优点:实现空间和时间上的解耦。但不可过度使用,防止对象间必要关系隐藏过深。
七、命令模式
最简单、最优雅的模式之一。命令模式中的命令指的是一个执行某些特定事情的指令。
使用场景:有时候需要向某些对象发送请求,但是不知道请求的接收者是谁,也不知道被请求的操作者是什么。此时希望用一种松耦合的方式来设计软件,是的请求发送者和请求接收者能够消除彼此之间的耦合关系。
命令模式其实是回调函数的一个面向对象的替代品。js的很多高阶函数可以实现命令模式,命令模式在js语言中是一种隐形的模式。
宏命令:一次执行一批命令。
八、组合模式
组合模式就是用小的子对象来构建更大的对象,而这些子对象也许是由更小的孙对象构成的。
1、组合模式用途:
(1)将对象组合成树形结构,以表示部分整体的层次结构。
(2)通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
2、请求在树中传递的过程
请求从上到下沿着数进行传递,直到树的尽头。作为客户,只关心树最顶层的组合对象,客户只要请求这个组合对象,请求便会沿着树往下传递,依次到达所有的叶对象。
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 );
说明:
(1)当按下按钮,所有的命令依次被执行。
(2)客户不需要知道当前处理的命令是宏命令还是普通命令,只要他是一个命令,并且有execute方法,这个命令就可以被添加到树中。
(3)命令可以嵌套命令,即命令既可以是组合对象,也可以是叶对象。此举会带来隐患,比如子对象下面没有叶节点,如果试图给叶节点下面添加子节点。解决方法是给子对象也添加add方法,并且在调用这个方法时,抛出一个异常。
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 openTvCommand = {
execute: function(){
console.log( '打开电视' );
},
add: function(){
throw new Error( '叶对象不能添加子节点' );
}
};
var macroCommand = MacroCommand();
macroCommand.add( openTvCommand );
openTvCommand.add( macroCommand ) // Uncaught Error: 叶对象不能添加子节点(4)
组合模式的例子----扫描文件夹
注意:
(1)组合模式不是父子关系
(2)对叶操作的一致性,对叶对象一致操作。
(3)双向映射关系如果叶对象属于不同的父对象,那么不适合用组合对象,可以采用中介者模式来管理这些对象。
(4)用职责链模式提高组合模式性能
何时使用组合模式
(1)表示对象的部分整体层次结构
(2)客户希望同意对待树中的所有对象
八、模板方法模式
模板方法模式是一种只需使用继承就可以实现的非常简单的模式。
1、构成:
(1)第一部分是抽象父类
(2)第二部分是具体的实现子类
通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且也可以选择重写弗雷德所有方法。
2、举例:咖啡和茶
共同点:
(1)把水煮沸
(2)用沸水冲泡咖啡/茶
(3)把咖啡/茶倒进杯子
(4)加糖/柠檬
不同点:
(1)原料
(2)泡的方式不同
(3)加入的调料不同
抽象:
(1)把水煮沸
(2)把沸水倒进杯子
(3)把饮料倒进杯子
(4)加调料
var Coffee = function(){};
Coffee.prototype.boilWater = function(){
console.log( '把水煮沸' );
};
Coffee.prototype.brewCoffeeGriends = function(){
console.log( '用沸水冲泡咖啡' );
};
Coffee.prototype.pourInCup = function(){
console.log( '把咖啡倒进杯子' );
};
Coffee.prototype.addSugarAndMilk = function(){
console.log( '加糖和牛奶' );
};
Coffee.prototype.init = function(){
this.boilWater();
this.brewCoffeeGriends();
this.pourInCup();
this.addSugarAndMilk();
};
var coffee = new Coffee();
coffee.init();
var Tea = function(){};
Tea.prototype.boilWater = function(){
console.log( '把水煮沸' );
};
Tea.prototype.steepTeaBag = function(){
console.log( '用沸水浸泡茶叶' );
};
Tea.prototype.pourInCup = function(){
console.log( '把茶水倒进杯子' );
};
Tea.prototype.addLemon = function(){
console.log( '加柠檬' );
};
Tea.prototype.init = function(){
this.boilWater();
this.steepTeaBag();
this.pourInCup();
this.addLemon();
};
var tea = new Tea();
tea.init();
var Beverage = function(){};
Beverage.prototype.boilWater = function(){
console.log( '把水煮沸' );
};
Beverage.prototype.brew = function(){}; // 空方法,应该由子类重写
Beverage.prototype.pourInCup = function(){}; // 空方法,应该由子类重写
Beverage.prototype.addCondiments = function(){}; // 空方法,应该由子类重写
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
真正的模板方法是Beverage.protype.init
模板方法模式是严重依赖抽象类的设计模式。
java中抽象类不能实例化,一定要由某个具体的自累继承,js中通过protype继承父类。
3、js中没有抽象类的缺点和解决办法
(1)鸭子类型模拟接口检查,以确保子类中确实重写了父类的方法
(2)调用父类方法时抛出一个异常
4、使用场景
(1)架构师搭建项目的框架
(2)web开发中构建一系列得UI组件
初始化一个div容器,通过ajax请求拉取相应的数据,把数据渲染到div容器中,完成组建的构造,通知用户组件渲染完毕
5、钩子方法
在父类中容易变化的地方防止钩子,钩子可以有一个默认实现,究竟要不要挂钩子,由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向。这样程序就有了变化的可能。
在实际中,js可以通过高阶函数来代替模板模式。
九、享元模式
享元模式是一种用于性能优化的模式,核心是运用共享技术来有效支持大量细粒度的对象。
1、内部状态和外部状态
享元模式要求将对象的属性划分为内部状态和外部状态(状态在这里指属性)。享元模式的目标是尽量减少共享对象的数量,以下是划分内部状态和外部状态的经验
(1)内部状态存储于内部对象
(2)内部状态可以被一些对象共享
(3)内部状态独立于具体的场景,通常不会改变
(4)外部状态取决于具体的场景,并根据场景而变化,并根据场景而变化,外部状态不能被共享
使用享元模式的关键在于如何区别内部状态和外部状态
2、享元模式的通用结构
(1)一开始并不需要所有的共享对象
(2)给model对象手动设置了underwear外部状态,在更复杂的系统中,这不是一个最好的方法,因为外部状态可能会相当复杂,他们与共享对象的联系会变得困难
3、文件上传的例子
4、享元模式的适用性
(1)一个程序使用了大量的相似对象
(2)由于使用了大量对象,造成了很大的内存开销
(3)对象的大多数状态都可以变为外部状态
(4)剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象
十、职责模式
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,知道有一个对象处理它为止。
称请求链中的对象为链中的节点。
1、职责链模式的优点:
(1)解耦了请求发送者和N个接收者之间的复杂关系,由于不知道链中的哪个节点可以处理你发出的请求,所以你只需把请求传递给第一个节点即可。
(2)增加或者减少一个节点不需要改动其他的代码,并且更改节点顺序都没有影响
(3)可以手动指定其实节点,请求并不是非得从链中的第一个节点开始传递
2、缺点:
(1)不能保证某个请求一定会被链中的节点处理(可以在链尾加一个保底的接收者节点来处理这种即将离开链尾的请求)
(2)使程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分节点没有起到实际作用,它的作用仅仅使让请求传递下去,从性能方面考虑,要避免过长的职责链带来的性能损失。
3、用AOP实现职责链
Function.prototype.after = function( fn ){
var self = this;
return function(){
var ret = self.apply( this, arguments );
if ( ret === 'nextSuccessor' ){
return fn.apply( this, arguments );
}
return ret;
}
};
var order = order500yuan.after( order200yuan ).after( orderNormal );
order( 1, true, 500 ); // 输出: 500 元定金预购,得到 100 优惠券
order( 2, true, 500 ); // 输出: 200 元定金预购,得到 50 优惠券
order( 1, false, 500 ); // 输出:普通购买,无优惠券
4、用职责链模式获取文件上传对象
var getActiveUploadObj = function(){
try{
return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
}catch(e){
return 'nextSuccessor' ;
}
};
var getFlashUploadObj = function(){
if ( supportFlash() ){
var str = '<object type="application/x-shockwave-flash"></object>';
return $( str ).appendTo( $('body') );
}
return 'nextSuccessor' ;
};
var getFormUpladObj = function(){
return $( '<form><input name="file" type="file"/></form>' ).appendTo( $('body') );
};
var getUploadObj = getActiveUploadObj.after( getFlashUploadObj ).after( getFormUpladObj );
console.log( getUploadObj() );
十一、中介者模式
作用就是接触对象与对象之间的耦合关系。增加一个中介者后,所有的相关对象都通过中介者对象来通信,而不是相互引用,当一个对象发生变化时,只需要通知中介者对象即可。中介者模式使网状的多对多关系变成了相对简单的一对多关系。
1、现实中的中介者
机场指挥塔、博彩公司
2、例子:泡泡堂游戏
3、例子:购买商品
4、优点
(1)使各个对象解耦,以中介者和对象之间的一对多取代了对象之间的网状多对多关系
(2)各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象实现和维护
5、缺点:
(1)最大的缺点是系统中新增了一个中介者对象,因为对象之间交互的复杂性,转移成了终结者对象的复杂性,使得中介者对象自身往往就是一个难以维护的对象
(2)对象之间并非一定需要解耦。
十二、装饰者模式
动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。跟继承相比,装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
装饰者模式将一个对象嵌入到另一个对象中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递到所有对象,每个对象都有处理这条请求的机会。
var plane = {
fire: function(){
console.log( '发射普通子弹' );
}
}
var missileDecorator = function(){
console.log( '发射导弹' );
}
var atomDecorator = function(){
console.log( '发射原子弹' );
}
var fire1 = plane.fire;
plane.fire = function(){
fire1();
missileDecorator();
}
var fire2 = plane.fire;
plane.fire = function(){
fire2();
atomDecorator();
}
plane.fire();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
在不改变函数源代码的情况下,能给函数增加功能 :通过保存原引用的方式就可以改写某个函数,符合开放/封闭原则 。但是这种方式存在以下两个问题。
(1) 必须维护_onload这个中间变量,虽然看起来并不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多。
(2)其实还遇到了 this被劫持的问题,在window.onload的例子中没有这个烦恼,是因为调用普通函数_onload时,this也指向window,跟调用window.onload时一样(函数作为对象的方法被调用时,this指向该对象,所以此处this也只指向window)
改进:使用AOP装饰函数
首先给出 Function.prototype.before方法和Function.prototype.after方法
Function.prototype.before = function( beforefn ){
var __self = this; // 保存原函数的引用
return function(){ // 返回包含了原函数和新函数的"代理"函数
beforefn.apply( this, arguments ); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply( this, arguments ); // 执行原函数并返回原函数的执行结果,
// 并且保证 this 不被劫持
}
}
Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};
通过 Function.prototype.apply来动态传入正确的this,保证了函数在被装饰之后,this不会被劫持
var before = function( fn, beforefn ){
return function(){
beforefn.apply( this, arguments );
return fn.apply( this, arguments );
}
}
var a = before(
function(){alert (3)},
function(){alert (2)}
);
a = before( a, function(){alert (1);} );
a();
不改变ajax,添加token
var getToken = function(){
return 'Token';
}
ajax = ajax.before(function( type, url, param ){
param.Token = getToken();
});
ajax( 'get', 'http:// xxx.com/userinfo', { name: 'sven' } );
代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链。
十三、状态模式
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
1、电灯的例子
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();
增加几种状态
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';
}
};
改进为状态模式:
状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部
// 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
};
var Light = function(){
this.offLightState = new OffLightState( this );
this.weakLightState = new WeakLightState( this );
this.strongLightState = new StrongLightState( this );
this.button = null;
};
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 = function( newState ){
this.currState = newState;
};
现在可以进行一些测试:
var light = new Light();
light.init();
定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。
第二部分是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果。
2、状态模式的通用结构
首先定义了 Light 类,Light类在这里也被称为上下文(Context)。随后在Light 的构造函数中,我们要创建每一个状态类的实例对象,Context 将持有这些状态对象的引用,以便把请求委托给状态对象。用户的请求,即点击button 的动作也是实现在 Context 中的。
如果我们编写一个状态子类时,忘记了给这个状态子类实现 buttonWasPressed方法,则会在状态切换的时候抛出异常。因为 Context总是把请求委托给状态对象的 buttonWasPressed方法。 建议的解决方案跟《模板方法模式》中一致,让抽象父类的抽象方法直接抛出一个异常,这个异常至少会在程序运行期间就被发现:
var State = function(){};
State.prototype.buttonWasPressed = function(){
throw new Error( '父类的 buttonWasPressed 方法必须被重写' );
};
var SuperStrongLightState = function( light ){this.light = light;
};
SuperStrongLightState.prototype = new State(); // 继承抽象父类
SuperStrongLightState.prototype.buttonWasPressed = function(){ // 重写 buttonWasPressed 方法
console.log( '关灯' );
this.light.setState( this.light.offLightState );
};
3、另一个例子-文件上传
4、状态模式的优点如下。
(1)状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
(2)避免 Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context中原本过多的条件分支。
(3)用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
(4)Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
5、状态模式的缺点
在系统中定义许多状态类 ,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。
6、性能优化点
(1)有两种选择来管理 state 对象的创建和销毁。第一种是仅当 state 对象被需要时才创建并随后销毁,另一种是一开始就创建好所有的状态对象,并且始终不销毁它们。
(2)我们为每个 Context 对象都创建了一组 state 对象,实际上这些state对象之间是可以共享的,各Context 对象可以共享一个 state 对象,这也是享元模式的应用场景之一。
7、状态模式和策略模式的关系
相同点:策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行 。
不同点:策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。
GitHub 上有一个对应的库实现,通过这个库,可以很方便地创建出FSM
var fsm = StateMachine.create({
initial: 'off',
events: [
{ name: 'buttonWasPressed', from: 'off', to: 'on' },
{ name: 'buttonWasPressed', from: 'on', to: 'off' }
],
callbacks: {
onbuttonWasPressed: function( event, from, to ){
console.log( arguments );
}
},
error: function( eventName, from, to, args, errorCode, errorMessage ) {
console.log( arguments ); // 从一种状态试图切换到一种不可能到达的状态的时候
}
});
button.onclick = function(){
fsm.buttonWasPressed();
}
十七、适配器模式
适配器模式是解决两个软件实体间的接口不兼容的问题。适配器的别名是包装器。
1、现实中的适配器
2、适配器的应用
适配器是一种亡羊补牢模式。
(三)设计原则和编程技巧
一、单一职责原则
二、最少知识
三、开放封闭
四、接口和面向接口编程
五、代码重构