【学习笔记javascript设计模式与开发实践(状态模式)----16】

第16章状态模式

状态模式是一种非同寻常的优秀模式,它也许是解决某些需求场景的最好方法。虽然状态模式不是一种简单到一目了然的模式,但你一旦明白了状态模式的精髓,以后一定会感谢它带给你的无与伦比的好处。

16.1 初识状态模式

我们来想象这样一个场景:有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。

16.1.1 第一个例子:电灯程序

首先给出不用状态模式的电灯程序:

var Light= function(){
  this.state = ‘off’; //初始状态
  this.button = null;//电灯开关按钮
}

接下来定义Light.prototype.init方法,该方法负责在页面中创建一个真实的button节点,假设这个button就是电灯的开关按钮,当button的onclick事件被触发时,就是电灯开关被按下的时候,代码如下:

Light.prototype.init=function(){
  var button=document.createElement('button');
  var self=this;
  button.innerHTML='开关';
  this.button=document.body.appendChild(button);
  this.button.onclick=function(){
     self.buttonWasPressed();
   }
}

当开关被按下时,程序会调用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=newLight();
light.init();

现在可以看到,我们已经编写了一个强壮的状态机,这个状态机的逻辑简单又缜密,看起来这段代码设计得无懈可击,这个程序没有任何bug,实际上这种代码我们已经编写过无数遍,比如要交替切换一个button的class,跟此例一样,往往先用一个变量state来记录按钮的当前状态,在事件发生时,再根据这个状态来决定下一步的行为。
另人一遗憾的是,这个世界上的电灯并非只有一种。许多酒店里有另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。现在必须改造上面的代码来完成这种新型电灯的制造。

Light.prototype.buttonWasPressed=function(){
   if(this.state==='off'){
       console.log('弱光');
       this.state='weakLight';
   }else if(this.state==='weakLight'){
       console.log('强光');
       this.state='strongLigth';
   }else if(this.state==='strongLight'){
       console.log('关灯');
       this.state='off';
    }
}

下面我们来考虑以下上述代码的缺点:

  • 很明显buttonWasPressed方法违反开放—封闭原则的,每次新增或者修改light的状态,都需要改动buttonWasPressed方法中的代码,这使得buttonWasPressed成为一个非常不稳定的方法
  • 所有跟状态有关的行为,都被封装在buttonWasPressed方法里,如果以后这个电灯又增加了强强光、超强光和终极强光,我们那将无法预计这个方法将膨胀到什么地步。当然为了简化示例,此处在状态发生改变的时候,只是简单地打印一条log和改变button的innerHTML。在实际开发中,要处理的事情可能比这多得多……
  • 状态的切换非常不明显,仅仅表现为对state变量赋值,比如this.state = ‘weakLight’。在实际开发中很容易不小心漏掉。我们也没有办法一目了然地明白电灯一共有多少种状态,除非耐心读完buttonWasPressed方法的代码
  • 状态之间的切换关系,不过是往buttonWasPressed方法里面堆砌if/else语句,增加或者修改一个状态可能需要改变若干个操作,这使buttonWasPressed方法更加难以阅读和维护

16.1.2 状态模式改进电灯程序

现在我们学习使用状态模式改进电灯程序。有意思的是,通常我们谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以button被按下的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染自身的行为。

同时我们还可以把状态的切换规则事先分布在状态类中,这样就有效地消除了原本存在大量条件分支语句。

下面进入状态模式的代码编写阶段,首先将定义3个状态类,分别是offLightState、WeakLightState、strongLightState。这3个类都有一个原型方法buttonWasPressed,代表各自状态下,按钮被按下时将发生的行为:

var OffLightState=function(light){
  this.light= light;
}
OffLightState.prototype.buttonWasPressed=function(){
  console.log('弱光');
  this.light.setState(this.light.weakLightState);
}

//weakLightState
var WeakLightState=function(light){
  this.light= light;
}

WeakLightState.prototype.buttonWasPressed=function(){
   console.log('强光');
   this.light.setState(this.light.strongLightState);
}

//strongLightState
var StrongLightState=function(light){
  this.light= light;
}

StrongLightState.prototype.buttonWasPressed=function(){
   console.log('关灯');
   this.light.setState(this.light.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');
  var 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对象增加一种新的状态时,只需要增加一个新的状态类,再稍稍改变一些现有的代码即可。假设现在ligth对象多了一种超级强光的状态,那么我先增加SuperStrongLightState类:

SuperStrongLightState.prototype.buttonWasPressed=function(){
   console.log('关灯');
   this.light.setState(this.light.offLightState);
}

然后在Light构造函数里新增一个superStrongLightState对象:

this.superStrongLightState =new SuperStrongLightState();

16.2 定义

通过上例,相信我们对状态模式已经有了一定程序的了解。现在回头来看GoF中对状态模式的定义:
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
上句以逗号分隔,第一部分的意思是将状态封闭成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。
第二部分是从客户的角度来看,我们使用对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的。实际上这是使用了委托的效果。

16.3 状态模式的通用结构

在前面的电灯例子中,我们完成了一个状态模式程序的编写。首先定义了Light类,Light类在这里也被称为上下文。随后在Light的构造函数中,我们要创建每一个状态类的实例对象,Context持有这些状态对象的引用,以便把请求委托给状态对象。用户的请求,即点击button的动作也是实现在Context中的,接下来可能是个苦力活,我们要编写各种状态类,light对象被传入状态类的构造函数,状态对象也需要持有light对象的引用,以便调用light中的方法或者直接操作light对象

16.4 缺少抽象类的变通方式

在java中,所有的状态类必须继承自一个State抽象父类,当然如果没共同的功能值得放入抽象父类中,也可以选择实现State接口。这样做的原因一方面是我们曾多次提过的向上转型,另一方面是保证所有的状态子类都实现了buttonWasPressed方法。遗憾的是,javascript既不支持抽象类,也没有接口的概念。所以在使用状态模式的时候要格外小心,如果我们编写一个状态子类时,忘记了给这个状态子类实现buttonWasPressed方法,则会在状态切换的时候抛出异常。因为Context总是把请求委托给状态对象的buttonWasPressed方法。
不论怎样严格要求程序员,也许都避免不了犯错的那一天,毕竟如果没有编译器的帮助,只依靠程序员的自觉以及一点好运气,是不靠谱的。这里建议的解决方案跟《模板方法模式》中一致,让抽象父类的抽象方法直接抛出一个异常,这个异常至少会在程序运行期间被发现:

var State = function(){};
State.prototype.buttonWasPressed =function(){
   throw new Error(‘父类的buttonWasPressed方法必须被重写’);
}
var SuperStrongLightState =function(light){
   this.light = light;
}
SuperStrongLightState.prototype = newState(); //继承抽象父类
SuperStrongLightState.prototype.buttonWasPressed = function(){
  console.log(‘关灯’);
 this.light.setState(this.light.offLightState);
}

16.5 另一个状态模式示例—-文件上传

实际上,不论是文件上传,还是音乐、视频播放器,都可以找到一些明显的状态区分。比如文件上传程序中有扫描、正在上传、暂停、上传成功、上传失败这几种状态。音乐播放器可以分为加载中、正在播放、暂停、播放放完毕这几种状态。点击同一个按钮,在上传中和暂停状态下的行为表现是不一样的,同时它们的样式class也不同。下面我们以上传为例进行说明。

16.5.1 更复杂的切换条件

相对于电灯的例子,文件上传不同的地方在于,现在我们将面临更加复杂的条件切换关系。在电灯的例子中,电灯的状态总是从关到开再到关,或者从关到弱光、弱光到强光、强光再到关。看起来总是循规蹈矩的AàBàCàA,所以即使不使用状态模式来编写电灯程序,而是使用原始的if、else来切换,我们也不至于在逻辑编写中迷失自己,因为状态的切换总是遵循一些简单的规律如下:

if(this.state==='off'){
   console.log('开弱光');
   this.button.innerHTML='下一次按我是强光';
}else if(this.state==='weakLight'){
   console.log('开强光');
   this.button.innerHTML='下一次按我是关灯';

}else if(this.state==='strongLight'){
   console.log('关灯');
   this.button.innerHTML='下一次按我是弱光';
}

而文件上传的状态切换相比要复杂得多,控制文件上传的流程需要两个节点按钮,第一个用于暂停和继续上传,第二个用于删除文件。
现在看看文件在不同的状态下,点击两个按钮将分别发生什么行为:
o 文件在扫描状态中,是不能进行任何操作的,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成之后,根据文件的md5值判断,若确认该文件已经存在于服务器,则直接跳到上传完成状态。如果该文件的大小超出上传的最大值,或者该文件已经损坏,则跳往上传失败状态。剩下的情况下才进入上传中状态。
o 上传过程中可以点击暂停上传,暂停后点击同一个按钮会继续上传
o 扫描和上传文件过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件。

16.5.2 一些准备工作

作者的项目中提供了一些浏览器插件来帮助完成文件上传,插件类型根据浏览器的不同,有可能是ActiveObject,也有可能是WebkitPlugin。
上传是一个异步过程,所以控件会不停地调用javascript提供的一个全局函数window.external.upload,来通知javascript目前的上传进度,控件会把当前的文件状态作为参数state塞进window.external.upload。在这里无法提供一个完整的上传插件,我们将简单地用setTimeout来模拟文件的上传进度,window.external.upload函数在此例中也只负责打印一些log:

window.external.upload = function(state){
 console.log(state); //
}

//另外我们需要在页面上放置一个用于上传的插件对象:

var plugin= (function(){
   var plugin=document.createElement('embed');
   plugin.style.display='none';
   plugin.type='application/txftn-webkit';
   plugin.sign = function(){
     console.log('开始文件扫描')
    }

   plugin.pause = function(){
     console.log('暂停文件上传')
    }
   plugin.uploading=function(){
     console.log('开始文件上传')
    }
   plugin.del = function(){
     console.log('删除文件上传')
    }
   plugin.done = function(){
     console.log('文件上传完成')
    }
   document.body.appendChild(plugin);
   return plugin;
})();

16.5.3 开始编写代码

接下来开始完成其他代码的编写,先定义Upload类,控制上传过程的对象将从Upload类中创建出来:

var Upload = function(fileName){
 this.plugin = plugin;
 this.fileName = fileName;
 this.button1 = null;
 this.button2=null;
 this.state = ‘sign’ //设置初始状态为waiting
}

Upload.prototype.init 方法会进行一些初始化工作,包括创建页面中的一些节点,起主要作用的是两个用于控制上传流程的按钮,第一个按钮用于暂停和继续上传,第二个用于删除文件:

var Upload = function(){}
Upload.prototype.init=function(){
  var that= this;
  this.dom=document.prototype.createElement('div');
  this.dom.innerHTML='<span>文件名称:'+this.fileName+'</span>'+
      '<buttondata-action="button1">扫描中</button>'+
      '<buttondata-action="button2">删除</button>';
      document.body.appendChild(this.dom);
      this.button1=this.dom.querySelector('[data-action="button1"]');
      this.button2=this.dom.querySelector('[data-action="button2"]');
      this.bindEvent();
}

Upload.prototype.bindAsEventListener=function(){
  var self=this;
  this.button1.onclick =function(){
     if(self.state==='sign'){
        console.log('扫描中,点击无效....');
     }else if(self.state==='uploading') {//上传中,点击切换到暂停
        self.changeState('pause');
     }else if(self.state==='pause'){//暂停中,点击切换到上传中
        self.chageState('uploading');
     }else if(self.state==='done'){
         console.log('文件已完成上传,点击无效');
     }else if(self.state==='error'){
         console.log('文件上传失败,点击无效');
     }
   }
  this.button2.onclick =function(){
     if(self.state==='done'||self.state==='error'||self.state==='pause'){
       //上传完成、上传失败和暂停状态下可以删除
       self.changeState('del');
     }else if(self.state==='del'){
         console.log('文件正在扫描中,不能删除');
     }else if(self.state==='uploading'){
         console.log('文件正在上传中,不能删除');
     }
   }
}

再接下来是Upload.prototype.changeState方法,它负责切换状态之后具体行为,包括改变按钮的innerHTML,以及调用插件开始一些“真正”的操作。

Upload.prototype.changeState=function(state){
   switch(state) {
       case 'sign':
           this.plugin.sign();
           this.button1.innerHTML='扫描中,任何操作无效'
           break;
       case 'uploading':
           this.plugin.uploading();
           this.button1.innerHTML='正在上传,点击暂停'
           break;
       case 'pause':
           this.plugin.pause();
           this.button1.innerHTML='已暂停,点击继续上传'
           break;
       case 'done':
           this.plugin.done();
           this.button1.innerHTML='上传完成'
           break;
       case 'error':
           this.button1.innerHTML='上传失败';
            break;
       case 'del':
           this.plugin.del();
           this.dom.parentNode.removeChild(this.dom);
           console.log('删除完成');
           break;
    }
   this.changeState= state;
}

最后我们来进行一些测试工作:

var uploadObj = new Upload(‘javascript设计模式与开发实践’);
uploadObj.init();
window.external.upload = function(state){
 uploadObj.changeState(state);
}

window.external.upload(‘sign’);
setTimeout(function(){
  window.external.upload(‘uploading’);
},1000); //1秒后开始上传

setTimeout(function(){
  window.external.upload(‘done’);
},5000); //5秒后开始上传完成

至此就完成了一个简单的文件上传程序的编写。当然这仍然是一个反例,这里的缺点跟电灯例子中的第一段代码一样,程序中充斥着if、else条件分支,状态和行为都被耦合在一个巨大的方法里,我们很难修改和扩展这个状态机。文件状态之间的联系如此复杂,这个问题显得更加严重了。

16.5.4 状态模式重构文件上传程序

状态模式在文件上传的程序中,是最优雅的解决办法之一。通过电灯的例子,我们已经熟知状态模式的结构了,下面就开始一步步地重构它。
第一步仍然是提供window.external.upload函数,在页面中模拟创建上传插件,这部分没有变。

window.external.upload = function(state){
 console.log(state); //
}
var plugin= (function(){
   var plugin=document.createElement('embed');
   plugin.style.display='none';
   plugin.type='application/txftn-webkit';
   plugin.sign = function(){
     console.log('开始文件扫描')
    }

   plugin.pause = function(){
     console.log('暂停文件上传')
    }
   plugin.uploading=function(){
     console.log('开始文件上传')
    }
   plugin.del = function(){
     console.log('删除文件上传')
    }
   plugin.done = function(){
     console.log('文件上传完成')
    }
   document.body.appendChild(plugin);
   return plugin;
})();

第二步,改造Upload构造函数,在构造函数中为每种状态子类都创建一个实例对象:

var Upload = function(fileName){
 this.plugin=postponeNativePluginInstallation;
 this.fileName= fileName;
 this.button1=null;
 this.button2=null;
 this.signState=new SignState(this);
 this.uploadingState=new UploadingState(this);
 this.pauseState=new PasuseState(this);
 this.doneState=new DoneState(this);
 this.errorState=new ErrorState(this);
 this.currState=this.signState;//设置当前状态
}

第三步,Upload.prototype.init方法无需改变,仍然负责往页面中创建跟上传流程有关的DOM节点,并开始绑定按钮事件。

Upload.prototype.init=function(){
  var that= this;
  this.dom=document.prototype.createElement('div');
  this.dom.innerHTML='<span>文件名称:'+this.fileName+'</span>'+
      '<buttondata-action="button1">扫描中</button>'+
      '<buttondata-action="button2">删除</button>';
      document.body.appendChild(this.dom);
      this.button1=this.dom.querySelector('[data-action="button1"]');
      this.button2=this.dom.querySelector('[data-action="button2"]');
      this.bindEvent();
}

第四步,负责具体的按钮事件实现,在点击了按钮之后,Context并不做任何的操作,而是把请求委托给当前的状态类来执行:

Upload.prototype.bindEvent = function(){
   var self = this;
   this.button1.onclick =function(){
       self.currState.clickHandler1();
    }
   this.button2.onclick =function(){
       self.currState.clickHandler2();
    }
}

第四步中的代码有一些变化,我们把状态对应的逻辑行为放在Upload类中:

Upload.prototype.sign = function(){
 this.plugin.sign();
 this.currSate = this.signState;
};
Upload.prototype.uploading = function(){
 this.button1.innerHTML = ‘正在上传,点击暂停’;
 this.plugin.uploading();
 this.currState = this.uploadingState;
}
Upload.prototype.pause = function(){
 this.button1.innerHTML = ‘已暂停,点击继续上传’;
 this.plugin.pause();
 this.currState = this.pauseState;
}
Upload.prototype.pause = function(){
 this.button1.innerHTML = ‘上传完成’;
 this.plugin.done();
 this.currState = this.doneState;
}

Upload.prototype.pause = function(){
 this.button1.innerHTML = ‘上传失败’;
 this.plugin.pause();
 this.currState = this.errorState;
}

Upload.prototype.del = function(){
 this.plugin.del();
 this.dom.parentNode.removeChild(this.dom);
}

第五步,工作略显乏味,我们要编写各个状态类的实现,值得注意的是我们使用了StateFactory,从而避免因为javascript中没有抽象类所带来的问题

var StateFactory= (function(){
  var State = function(){};
  State.prototype.clickHandler1=function(){
     throw new Error('子类必须重写父类的clickHandler1方法');
   }
  State.prototype.clickHandler2=function(){
     throw new Error('子类必须重写父类的clickHandler1方法');
   }

  return function(param){
       var F = function(uploadObj){
          this.uploadObj= uploadObj;
       }
      F.prototype =newState();
      for(variinparam){
          F.prototype[i] = param[i];
      }
      return F;
   }

})();

var SingState=StateFactory({
   clickHandler1:function(){
       console.log('扫描中,点击无效....');
   },
   clickHandler2:function(){
       console.log('文件正在上传中,不能删除');
    }
});

var UploadingState=StateFactory({
   clickHandler1:function(){
       this.uploadObj.pause();
   },
   clickHandler2:function(){
       console.log('文件正在上传中,不能删除');
    }
});

var PauseState=StateFactory({
   clickHandler1:function(){
       this.uploadObj.uploading();
   },
   clickHandler2:function(){
       this.uploadObj.del();
    }
});

var DoneState=StateFactory({
   clickHandler1:function(){
       console.log('文件完成上传,点击无效');
   },
   clickHandler2:function(){
       this.uploadObj.del();
    }
});

var ErrorState=StateFactory({
   clickHandler1:function(){
       console.log('文件上传失败,点击无效');
   },
   clickHandler2:function(){
       this.uploadObj.del();
    }
});

最后是测试时间:

var uploadObj = new Upload(‘javascript设计模式与开发实践’);
uploadObj.init();
window.external.upload = function(state){
 uploadObj[state]();
}

window.external.upload(‘sign’);
setTimeout(function(){
  window.external.upload(‘uploading’);
},1000); //1秒后开始上传

setTimeout(function(){
  window.external.upload(‘done’);
},5000); //5秒后开始上传完成

16.6 状态模式的优缺点

到这里我们已经学习了两个状态模式的例子,现在是时候来总结状态模式的优缺点了。状态的优点如下:

  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换
  • 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
  • Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响

状态模式的缺点是会在系统中定义许多状态类,编写20个状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。

16.7 状态模式中的性能优化

在这两个例子中,我们并没有太多性能方面考虑问题,实际上,这里有一些比较大的优化点:
有两种选择来管理state对象的创建和销毁。第一种是仅当state对象被需要时才创建并随后销毁,另一种是一开始就创建好所有的状态对象,并且始终不销毁它们。
在本章的例子中,我们为每个Context对象都创建了一组state对象,实际上这些state对象之间是可以共享的,各Context对象可以共享一个state对象,这也是享元模式的应用场景之一。

16.8 状态模式和策略模式的关系

状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为他们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。
策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行
它们的区别是策略模式中的各个策略类之间是平等又平行,它们之间没有任何联系,所以客户必须熟知这些策略的作用,以便客户可以随时主动切换算法;而状态模式中,状态和状态的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。

16.9 javascript版本的状态机

前面两个示例都是模拟传统面向对象语言的状态模式实现,我们为每种状态都定义一个状态子类,然后在Context中持有这些状态对象的引用,以便把currState设置为当前的状态对象。
状态模式是状态机的实现之一,但在javascript这种无类语言中,没有规定状态对象一定要从类中创建而来。下面的状态机选择了通过Function.prototype.call方法直接把请求委托给某个字面量对象来执行。
下面改写电灯的例子,来展示这种更加轻巧的做法:

var Light = function(){
 this.currState =FSM.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.currState.buttonWasPressed.call(self);//把请求委托给FSM状态机
      }
}

var FSM = {
 off:{
       buttonWasPressed:function(){
         console.log(‘关灯’);
         this.button.innerHTML = ‘下一次按是开灯’
         this.currState = FSM.on;       
      }
   },
 on:{
      buttonWasPressed:function(){
         console.log(‘开灯’);
         this.button.innerHTML = ‘下一次按是关灯’
         this.currState = FSM.off;       
      }
   }

}

var light = new Light();
light.init();

接下来尝试另外一种方法,即利用下面的delegate函数来完成这个状态机编写。这是面向对象设计和闭包互换的一个例子,前者把变量保存为对象属性,而后者把封闭在闭包形成环境中:

var delegate=function(client,delegation){
  return {
     buttonWasPressed:function(){
        return delegation.buttonWasPressed.apply(client,arguments);
     }
   }
}

var FSM = {
   off:{
      buttonWasPressed:function(){
          console.log('关灯');
          this.buttonWasPressed.innerHTML='下一次按我是开灯';
          this.currState=this.onState;
      }
   },
   on:{
       buttonWasPressed:function(){
           console.log('开灯');
           this.buttonWasPressed.innerHTML='下一次按我是关灯';
           this.currState=this.offState;
       }
    }
}

var Light = function(){
  this.offState=delegate(this,FSM.off);
  this.onState=delegate(this,FSM.on);
  this.currState=this.offState;
  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.currState.buttonWasPressed();
   }
}

var light=newLight();
light.init();
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值