JavaScript设计模式:深入理解多态
本文深入解析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。实际开发中可能不尽如此,此时可借助适配器模式解决。
以上就是本文的全部内容,通过生动举例全面讲解多态,希望能帮助大家真正有所收获。
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01
为什么JS中typeof []返回object:数组类型检测误区解析
2026-11-04 19:50:47
