JavaScript设计模式:深入理解多态

作者:木棉

时间:2026-10-29

浏览:637

本文深入解析JavaScript中的多态概念,通过动物叫声与地图API两个实例,阐述如何将“做什么”与“谁去做”分离。文章对比了静态语言与动态语言在多态实现上的差异,重点讲解利用对象多态性消除条件分支语句的方法,帮助开发者写出更灵活、易扩展的代码。

文章目录

多态的基本概念

多态的核心定义在于:针对同一操作作用于不同对象时,能够产生不同的解释与执行结果。简言之,当向不同对象发送相同消息时,各对象会依据自身特性给出相应的反馈。

多态的字面含义较为抽象,接下来将通过具体案例进行解析。

假设主人饲养了一只鸭子和一只鸡,当发出“叫”的指令时,鸭子会发出“嘎嘎嘎”声,而鸡则发出“咯咯咯”声。尽管两者同为动物且具备发声能力,但面对相同指令,它们执行了不同的行为。

这一场景即蕴含了多态的思想,下面将通过代码进一步阐释。

JavaScript中的多态实现

1. 一段“多态”的JavaScript代码

将上述故事转化为JavaScript实现如下:


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函数以支持新类型。频繁修改代码会增加出错风险,且随着动物种类增多,该函数将变得庞大且难以维护。

多态的本质是将“做什么”与“谁去做以及怎样去做”分离,即区分“不变的事物”与“可能改变的事物”。故事中动物发声是不变的,但具体发声方式可变。通过隔离不变部分并封装可变部分,程序得以扩展,符合开放-封闭原则。相比修改代码,仅增加代码即可实现新功能,既优雅又安全。

对象的多态性与扩展性

2. 对象的多态性

改写后的代码首先隔离不变部分,即所有动物都能发声:


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() ); //汪汪汪

类型检查与静态语言的多态

3. 类型检查和多态

类型检查是实现对象多态性前的重要话题。JavaScript作为动态类型语言无需类型检查,为深入理解多态目的,需从静态类型语言入手。

静态类型语言在编译时进行类型匹配检查。以Java为例,编译时需严格检查类型,变量不能赋予不同类型的值,这种检查有时导致代码僵化,代码如下:


String str;

str = abc; //没有问题 
str = 2; //报错

尝试将鸭子与鸡发声的例子转换为Java代码:


public class Duck { //鸭子类
 public void makeSound(){
 System.out.println( 嘎嘎嘎 );
 }
}

public class Chicken { //鸡类
 public void makeSound(){
 System.out.println( 咯咯咯 );
 }
}


public class AnimalSound {
 public void makeSound( Duck duck ){ //(1)
 duck.makeSound();
 }

}

public class Test {
 public static void main( String args[] ){
 AnimalSound animalSound = new AnimalSound();
 Duck duck = new Duck();
 animalSound.makeSound( duck ); //输出:嘎嘎嘎
 }
}

虽然成功让鸭子发声,但若想让鸡也发声则无法实现。因为(1)处AnimalSound类的makeSound方法被限定为仅接受Duck类型参数:


public class Test {
 public static void main( String args[] ){
 AnimalSound animalSound = new AnimalSound();
 Chicken chicken = new Chicken();
 animalSound.makeSound( chicken ); //报错,只能接受Duck类型的参数
 }
} 

在享受静态语言类型检查带来的安全性时,我们往往也感到被束缚。

为解决此问题,静态类型面向对象语言通常支持向上转型:给类变量赋值时,既可使用该类本身,也可使用其超类。例如描述麻雀或喜鹊时,可说“一只麻雀在飞”或“一只喜鹊在飞”,若忽略具体类型,则可说“一只鸟在飞”。

同理,当Duck和Chicken对象被隐藏于超类型Animal之后,二者可互换使用,这是对象表现多态性的必经之路,而多态性正是实现众多设计模式的目标。

利用继承实现多态

4. 使用继承得到多态效果

利用继承实现多态是最常用手段,包括实现继承和接口继承。本节讨论实现继承,接口继承示例见第21章。

首先创建Animal抽象类,让Duck和Chicken继承自该抽象类。下述代码中(1)处和(2)处的赋值语句成立,因为鸭子和鸡也是动物:


public abstract class Animal {
 abstract void makeSound(); //抽象方法
} 

public class Chicken extends Animal{
 public void makeSound(){
 System.out.println( 咯咯咯 );
 }
}

public class Duck extends Animal{
 public void makeSound(){
 System.out.println( 嘎嘎嘎 );
 }
}

Animal duck = new Duck(); //(1)
Animal chicken = new Chicken(); //(2)

接下来使AnimalSound类的makeSound方法接受Animal类型参数,而非具体的Duck或Chicken类型:


public class AnimalSound{
 public void makeSound( Animal animal ){ //接受Animal类型的参数
 animal.makeSound();
 }
}

public class Test {
 public static void main( String args[] ){
 AnimalSound animalSound= new AnimalSound ();
 Animal duck = new Duck();
 Animal chicken = new Chicken();
 animalSound.makeSound( duck ); //输出嘎嘎嘎
 animalSound.makeSound( chicken ); //输出咯咯咯
 }
}

JavaScript的动态多态特性

5. JavaScript的多态

前文指出,多态思想在于分离“做什么”与“谁去做”,关键在于消除类型间的耦合。若未消除耦合,makeSound方法中指定的对象类型便无法替换。在Java中,可通过向上转型实现多态。

JavaScript变量类型在运行期可变。一个JavaScript对象既可表示Duck类型,也可表示Chicken类型,这意味着JavaScript对象的多态性与生俱来。

这种多态性易于解释。作为动态类型语言,JavaScript编译时无类型检查,既不检查对象创建类型,也不检查传递参数类型。在2节的代码示例中,makeSound函数既可接收duck对象,也可接收chicken对象。

由此可见,动物能否发声仅取决于其是否拥有makeSound方法,而非其具体类型,不存在“类型耦合”。这正是鸭子类型所揭示的道理。在JavaScript中,无需向上转型等技术即可实现多态效果。

多态在程序设计中的作用

6. 多态在面向对象程序设计中的作用

许多人认为多态是面向对象编程语言中最重要的技术,但这一点尚不明显。毕竟人们不关心鸡鸭如何叫,这与程序员有何关联?

Martin Fowler在《重构:改善既有代码的设计》中写道:

多态的最根本好处在于,无需向对象询问“你是什么类型”再根据答案调用行为——只需调用行为即可,多态机制会自动安排妥当。

换言之,多态的根本作用是将过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支。

Martin Fowler的话可通过以下例子诠释:

电影拍摄现场,导演喊“action”时,主角背台词,照明师打光,群众演员倒地,道具师撒雪。收到同一消息时,每个对象都知道自己该做什么。若不利用多态而用面向过程方式编写,相当于导演每次走到每人面前确认职业(类型)后告知工作内容。映射到程序中,将充斥条件分支语句。

利用对象多态性,导演发布消息时无需考虑各对象接消息后该做什么。对象该做什么并非临时决定,而是事先约定排练完毕。每个对象的行为已成为其内部方法,各司其职。因此,各对象可根据同一消息有条不紊地分别工作。

将行为分布在各对象中并让其负责自身行为,这正是面向对象设计的优点。

多态在地图API中的应用案例

再看一个现实开发案例,其思想与动物叫声故事相似。

假设编写地图应用,可选两家地图API提供商。目前选择谷歌地图,其API提供show方法展示地图。示例代码如下:


var googleMap = {
 show: function(){
 console.log( '开始渲染google地图' );
 }
};

var renderMap = function(){
 googleMap.show(); 
};

renderMap(); // 输出: 开始渲染google地图 

后因故需将谷歌地图替换为百度地图。为使renderMap函数保持弹性,使用条件分支使其同时支持谷歌和百度地图:


var googleMap = {
 show: function(){
 console.log( '开始渲染google地图' );
 }
};

var baiduMap = {
 show: function(){
 console.log( '开始渲染baidu地图' );
 }
};

var renderMap = function( type ){
 if ( type === 'google' ){
 googleMap.show(); 
 }else if ( type === 'baidu' ){
 baiduMap.show();
 }
};

renderMap( 'google' ); // 输出: 开始渲染google地图 
renderMap( 'baidu' ); // 输出: 开始渲染baidu地图 

虽然renderMap函数目前具备一定弹性,但这种弹性脆弱。若需替换为搜搜地图,必须修改renderMap函数并继续堆砌条件分支语句。

我们先将程序中相同部分抽象出来,即显示某个地图:


var renderMap = function( map ){
 if ( map.show instanceof Function ){
 map.show();
 }
};

renderMap( googleMap ); // 输出: 开始渲染google地图 
renderMap( baiduMap ); // 输出: 开始渲染baidu地图 

现在寻找这段代码中的多态性。当向谷歌和百度地图对象分别发送“展示地图”消息时,调用各自的show方法,产生不同执行结果。对象的多态性提示我们,“做什么”与“怎么去做”可分离。即使新增搜搜地图,renderMap函数也无需改变,如下所示:


var sosoMap = {
 show: function(){
 console.log( '开始渲染soso地图' );
 }
};

renderMap( sosoMap ); // 输出: 开始渲染soso地图 

此例假设各地图API展示地图的方法名均为show。实际开发中可能不尽如此,此时可借助适配器模式解决。

以上就是本文的全部内容,通过生动举例全面讲解多态,希望能帮助大家真正有所收获。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章