JavaScript设计模式之状态模式

article/2025/10/25 16:06:57

什么是状态模式?

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

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。

初识状态模式

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


现在用代码来描述这个场景,首先定义一个Light类,可以预见,电灯对象light将从Light类创建而出, light对象将拥有两个属性,我们用state来记录电灯当前的状态,用button表示具体的开关按钮。下面
来编写这个电灯程序的例子。

第一个例子:电灯程序

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

var Light = function(){this.state = 'off'; // 给电灯设置初始状态offthis.button = null; // 电灯开关按钮
};

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

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();}
};

当开关被按下时,程序会调用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,现在可以看到,我们已经编写了一个强壮的状态机,这个状态机的逻辑既简单又缜密,看起来这段代码设计得无懈可击,这个程序没有任何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 = 'strongLight';}else if ( this.state === 'strongLight' ){console.log( '关灯' );this.state = 'off';}
};

现在这个反例先告一段落,我们来考虑一下上述程序的缺点。

  • 很明显buttonWasPressed方法是违反开放-封闭原则的,每次新增或者修改light的状态,都需要改动buttonWasPressed方法中的
    代码,这使得buttonWasPressed成为了一个非常不稳定的方法。

  • 所有跟状态有关的行为,都被封装在buttonWasPressed方法里,如果以后这个电灯又增加了强强光、超强光和终极强光,那我们将
    无法预计这个方法将膨胀到什么地步。当然为了简化示例,此处在状态发生改变的时候,只是简单地打印一条log和改变button的
    innerHTML。在实际开发中,要处理的事情可能比这多得多,也就是说,buttonWasPressed方法要比现在庞大得多。

  • 状态的切换非常不明显,仅仅表现为对state变量赋值,比如this.state=‘weakLight’。在实际开发中,这样的操作很容易被程序员不小心漏掉。我们也没有办法一目了然地明白电灯一共有多少种状态,除非耐心地读完buttonWasPressed方法里的所有代
    码。当状态的种类多起来的时候,某一次切换的过程就好像被埋藏在一个巨大方法的某个阴暗角落里。

  • 状态之间的切换关系,不过是往buttonWasPressed方法里堆砌if、else语句,增加或者修改一个状态可能需要改变若干个操作,这使buttonWasPressed更加难以阅读和维护。

状态模式改进电灯程序

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

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

下面进入状态模式的代码编写阶段,首先将定义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();

不出意外的话,执行结果跟之前的代码一致,但是使用状态模式的好处很明显,它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码。

另外,状态之间的切换都被分布在状态类内部,这使得我们无需编写过多的if、else条件分支语言来控制状态之间的转换。

当我们需要为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 ); // 切换状态到 superStrongLightState
};

状态模式的定义

通过电灯的例子,相信我们对于状态模式已经有了一定程度的了解。在回头来看GoF中对状态模式的定义:

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

我们以逗号分割,把这句话分为两部分来看。第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。电灯的例子足以说明这一点,在off和on这两种不同的状态下,我们点击同一个按钮,得到的行为反馈是截然不同的。

第二部分是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果。

状态模式的通用结构

在前面的电灯例子中,我们完成了一个状态模式程序的编写。首先定义了Light类,Light类在这里也被称为上下文(Context)。随后在Light的构造函数中,我们要创建每一个状态类的实例对象,Context将持有这些状态对象的引用,以便把请求委托给状态对象。用户的请求,即点击button的动作也是实现在Context中的,代码如下:

var Light = function(){this.offLightState = new OffLightState( this ); // 持有状态对象的引用this.weakLightState = new WeakLightState( this );this.strongLightState = new StrongLightState( this );this.superStrongLightState = new SuperStrongLightState( 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对象被传入状态类的构造函数,状态对象也需要持有light对象的引用,以便调用light中的方法或者直接操作light对象:

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

状态模式的优缺点

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

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

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

状态模式中的性能优化点

在上面这个例子中,我们并没有太多地从性能方面考虑问题,实际上,这里有一些比较大的优化点。

  • 有两种选择来管理state对象的创建和销毁。第一种是仅当state对象被需要时才创建并随后销毁,另一种是一开始就创建好所有的
    状态对象,并且始终不销毁它们。如果state对象比较庞大,可以用第一种方式来节省内存,这样可以避免创建一些不会用到的对象
    并及时地回收它们。但如果状态的改变很频繁,最好一开始就把这些state对象都创建出来,也没有必要销毁它们,因为可能很快将
    再次用到它们。

  • 在本章的例子中,我们为每个Context对象都创建了一组state对象,实际上这些state对象之间是可以共享的,各Context对象可
    以共享一个state对象,这也是享元模式的应用场景之一

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

状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。

策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。

它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。

JavaScript版本的状态机

上面例子是模拟传统面向对象语言的状态模式实现,我们为每种状态都定义一个状态子类,然后在Context中持有这些状态对象的引用,以便把currState设置为当前的状态对象。

状态模式是状态机的实现之一,但在JavaScript这种“无类”语言中,没有规定让状态对象一定要从类中创建而来。另外一点,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(){ // 将客户的操作委托给delegation对象return delegation.buttonWasPressed.apply( client, arguments );}}
};
var FSM = {off: {buttonWasPressed: function(){console.log( '关灯' );this.button.innerHTML = '下一次按我是开灯';this.currState = this.onState;}},on: {buttonWasPressed: function(){console.log( '开灯' );this.button.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 = new Light();
light.init();

结语

以上例子讲解了状态模式在实际开发中的应用。状态模式也许是被大家低估的模式之一。实际上,通过状态模式重构代码之后,很多杂乱无章的代码会变得清晰。虽然状态模式一开始并不是非常容易理解,但我们有必须去好好掌握这种设计模式。

状态模式是一种特别重要的设计模式,我们一定要好好体会


http://chatgpt.dhexx.cn/article/A1iC1tbf.shtml

相关文章

【JavaScript设计模式】观察者模式

观察者模式 文章目录 观察者模式🌰:理解观察者模式手写观察者模式必须先订阅再发布吗Vue数据双向绑定(响应式系统)的实现原理实现一个Event Bus/ Event Emitter在Vue中使用Event Bus来实现组件间的通讯 观察者模式与发布-订阅模式…

js设计模式

js设计模式 1.构造器函数 function Ema(name, age) {this.name nameconsole.log(this);//谁调用了this就指向谁this.sayfunction(){console.log(1)}}let ema1new Ema(zhangsna1)let ema2new Ema(李四1)console.log(ema1);//Ema obj {name:zhangsan1, Fun say}console.log(ema…

八大最常用的JavaScript设计模式

八大最常用的JavaScript设计模式 设计模式(Design pattern) 是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。我们使用设计模式最终的目…

JavaScript 设计模式之组合模式

引 我们知道地球和一些其他行星围绕着太阳旋转,也知道在一个原子中,有许多电子围绕着原子核旋转。我曾经想象,我们的太阳系也许是一个更大世界里的一个原子,地球只是围绕着太阳原子的一个电子。而我身上的每个原子又是一个星系&a…

JavaScript设计模式

JavaScript设计模式 设计模式:代码经验的总结,是可重用的用于解决软件设计中一般问题的方案。 设计模式都是面向对象的。 学习设计模式,有助于写出可复用和可维护性高的程序。 常用的12种设计模式: 工厂模式 单例模式 原型模…

JavaScript中常见的十五种设计模式

一、单例模式 二、策略模式 三、代理模式 四、迭代器模式 五、发布—订阅模式 六、命令模式 七、组合模式 八、模板方法模式 九、享元模式 十、职责链模式 十一、中介者模式 十二、装饰者模式 十三、状态模式 十四、适配器模式 十五、外观模式 一、单例模式 1. …

JS 常用的六种设计模式介绍

常用设计模式 前言 我们经常听到一句话,“写代码要有良好的封装,要高内聚,低耦合”。究竟怎样的代码才算得上是良好的代码。 什么是高内聚,低耦合? 即五大基本原则(SOLID)的简写 高层模块不…

git工具统计项目的代码行数

1、git 查看代码的项目总行数 (1)打开Git终端,进入项目的根目录 git log --prettytformat: --numstat | awk { add $1; subs $2; loc $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add,…

小技巧之统计代码行数

欢迎关注我的微信公众号“人小路远”哦,在这里我将会记录自己日常学习的点滴收获与大家分享,以后也可能会定期记录一下自己在外读博的所见所闻,希望大家喜欢,感谢支持! 搞了两个月,连搬带抄写出来的代码&a…

MAC代码下统计代码行数工具

作为一名程序员在很多的时候需要统计代码行数: 支持windows系统的代码行数统计方法以及软件很多,但是MAC系统的统计代码行数的真的不太多。 大家都知道用 wc -l 命令进行代码行数统计,但是它会将代码中的注释、空行所占用的文本行都统计在内…

cloc工具 命令行 统计代码行数

基本用法 :cloc后面跟目录名,文件名,或压缩文件名 例如: cloc ./application 1.安装(参考官网http://cloc.sourceforge.net/#apt-get) 根据操作系统不同,选择以下任意安装方法 sudo npm install -g cloc …

程序代码行数统计

程序写完了,提交著作权的时候不知道代码行数是多少怎么办? 介绍 软件名称兼容系统下载地址代码统计工具Windows软件下载 下面我们开始教程 打开主应用程序点击加号添加程序项目所在的目录点击按钮选择需要统计文件的文件后缀,看个人需求如…

计代码行数cloc,一个代码统计行数很好用的工具

分为window、mac系统区分,基本是一样的,一个代码统计行数很好用的工具。 CLOC简介 Cloc是一款使用Perl语言开发的开源代码统计工具,支持多平台使用、多语言识别,能够计算指定目标文件或文件夹中的文件数(files&#x…

统计代码量-代码统计工具 CLOC | gitlab统计代码量

文章目录 一、代码统计工具 CLOC什么是CLOC?下载安装clocs使用 二、gitlab统计代码量命令行统计图形化统计IDE Statistic统计代码插件 一、代码统计工具 CLOC 什么是CLOC? github: https://github.com/AlDanial/cloc CLOC是Count Lines of Code的意思,可以计算…

Python实现一个代码行数统计工具(以C/C++为例)

前几天在网上看到一个有意思的题,题目是设计一个代码行数统计工具。这类工具我经常会用到,但是具体是如何实现的呢?这个问题我还从未思考过,于是便试着做出这种工具。 题目描述是这样的: 题目要求:   请…

统计项目代码行数工具cloc

Ubuntu用户 使用cloc在ubuntu内统计代码行数 安装cloc工具 sudo apt-get install cloc进入需要统计的目录内,然后执行 cloc .然后就会显示文件目录中的文件数(files)、空白行数(blank)、注释行数(comment)和代码行数(code)。 Windows 用户 也是使用cloc工具 …

Win10 代码行数统计工具CLOC的安装和使用

简介 CLOC(Count Lines of Code),是一个可以统计多种编程语言中空行、评论行和物理行的工具。这个工具还是蛮实用的,可以帮我们快速了解一个项目中代码的信息。 注:底下这个命令可以实现统计代码行数的功能,只是不排除空行和注释…

代码行数统计小工具

一、先下载好SourceCounter小工具。解压,然后直接打开文件夹中的SourceCounter.exe。如果没有找到此工具的下载链接,点这里下载 二、选择代码类型,勾选上所有类型 三、双击点开后,选择文件夹,就可以直接统计出字…

在项目开发中统计代码行数的6种方式

文章目录 一、使用find和wc命令统计代码行数进行参数的过滤筛选命令参数简要说明 二、PowerShell工具统计代码行数条件过滤输出所有文件的行数PowerShell相关命令的简要说明 三、git命令git ls-filesgit log 四、代码编辑器插件五、jscpd六、自己实现一个注释和空行忽略目录和文…

chatgpt赋能python:Python代码行数统计-统计Python代码行数的常用工具与使用方法

Python代码行数统计 - 统计Python代码行数的常用工具与使用方法 Python编程语言是当今最流行的编程语言之一,在数据科学、人工智能、Web应用程序等许多领域都得到了广泛应用。当我们开发Python项目时,我们经常需要统计代码行数以管理代码库并监视进度。…