面向对象的JavaScript(一):类型语言与多态

205 阅读5分钟

JavaScript没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象与对象之间的继承。在JavaScript中,也没有对抽象类和接口的支持。因为JavaScript本身是一门类型模糊语言。这或许是一种失色,但也可以说是一种洒脱。

在我们开始学习设计模式之前,我们有必要了解一些JavaScript在面向对象方面的知识。

1. 动态类型语言和鸭子类型

编程语言按照数据类型大题可以分成两类:

  • 静态语言类型: 静态类型语言在编译时便已确定变量的类型
    • 优点: 在编译时就能发现类型不匹配的错误,可以避免在程序在运行时可能出现的一些错误,在一定程度上也可以提高程序的执行速度。
    • 缺点: 在编写代码的时候类型的声明会增加代码量,在这个过程中或多或少会分散我们在业务逻辑上的精力。毕竟我们大多数人编写程序的目的是为了交付。
  • 动态类型语言: 变量的类型在程序运行时才确定,待变量被赋值后,才会具有类型。(毫无疑问,JavaScript是动态语言类型
    • 优点: 代码数量更少,看起来也简洁。我们可以把更多的经历放在业务逻辑上。
    • 缺点: 无法保证变量的类型。

动态类型语言对变量的宽容实际上给编码带来了很大的灵活性。这一切都建立在 鸭子类型(duck typing) 的概念上。

鸭子类型:如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。

我们可以通过一个故事来了解鸭子类型:

国王觉得鸭子的叫声是一种绝妙的声音,他要组建一个需要1000个鸭子组成的合唱团。但是全世界只有999只鸭子,不过却有一只非常特别的鸡。这只鸡的叫声跟鸭子一摸一样,于是这只鸡就成为了合唱团的一员。

这个故事告诉我们,鸭子类型只关注对象的行为,而不关注对象的本本身

let duck = {
  duckSinging: () => {
    console.log("嘎嘎嘎");
  },
};

let chicken = {
  duckSinging: () => {
    console.log("嘎嘎嘎");
  },
};

let choir = [];

let joinChoir = (animal) => {
  if (animal && typeof animal.duckSinging === "function") {
    choir.push(animal);
    animal.duckSinging();
    console.log("恭喜加入合唱团");
    console.log(`合唱团现有成员:${choir.length}`);
  }
};

joinChoir(duck);
joinChoir(chicken);

//嘎嘎嘎
//恭喜加入合唱团
//合唱团现有成员:1
//嘎嘎嘎
//恭喜加入合唱团
//合唱团现有成员:2

小结: 在动态类型语言的面向对象设计中,鸭子类型的概念至关重要,我们相当于是“面向接口编程”。例如:一个对象若有push和pop方法,并且这些方法提供了正确实现,它就可以被当作栈来使用。如果一个数组有length属性,也可以依照下标来存取属性(最好还拥有slice和splice等方法),这个对象就可以被当作数组来使用。

2.多态

多态的实际含义是: 同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同对象发送同一个消息时,这些对象会根据消息分别给出不同的反馈。

下面我们来通过一个小故事来理解:

主人家里养了一只鸭和一只鸡,当主人向它们发出命令“叫”的时候,鸭子会“嘎嘎嘎”的叫,而鸡会“咯咯咯”的叫。 “他们同样是动物,并且都会发出叫声”,但是根据主人的指令,它们会各自发出不同的叫声

这其中就蕴含了多态的思想。

下面我们来看一段“多态”的代码。

let makeSound = (animal) => {
  if (animal instanceof Duck) {
    console.log("嘎嘎嘎");
  } else if (animal instanceof Chicken) {
    console.log("咯咯咯");
  }
};

let Duck = function () {};
let Chicken = function () {};

makeSound(new Duck());
makeSound(new Chicken());

//嘎嘎嘎
//咯咯咯

这段代码确实体现了”多态性“,但是这样的代码显然是令人不够满意的。为什么?试想如果后来又增加了一个动物,比如狗,猫之类的。显然就不是“咯咯咯”或者“嘎嘎嘎”的叫了。这个时候我们就需要改动makeSound函数里面的内容,用if else语句继续往里堆叠。修改代码总是危险的,修改的地方越多,程序出错的可能性就越大。当动物越来越多的时候,makeSound函数有可能会变得巨大。

比较满意的代码应该是这样的。

let makeSound = (animal) => {
  if (animal.sound && typeof animal.sound === "function") {
    animal.sound();
  }
};

let Duck = function () {};
Duck.prototype.sound = () => {
  console.log("嘎嘎嘎");
}

let Chicken = function () {};
Chicken.prototype.sound = () => {
  console.log("咯咯咯");
}

let Dog = function () {};
Dog.prototype.sound = () => {
  console.log("汪汪汪");
}

makeSound(new Duck());
makeSound(new Chicken());
makeSound(new Dog());

//嘎嘎嘎
//咯咯咯
//汪汪汪

如果代码这样写,我们就很直观的可以知道,当动物增加的时候,我们不必去动以前的代码,也就是makeSound函数里面的东西,这个时候只需要追加一些代码就可以了。

多态的核心思想是: 将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”和“可变的事物”分离开来。 这给予了我们程序的扩展能力,程序看起来是可生长的,也是符合开放-封闭原则。相对于修改以前的代码来说,追加新代码就能完成相同的功能,这显然优雅和安全得多。

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

多态最根本的作用就是通过 把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。

下面我们来通过一个小小故事来加深理解多态吧。

一个剧组拍戏的时候,灯光师,演员,摄像师都有不同的职责。在导演喊出“action”的时候每个人都要做自己的事情。如果没有多态思想的话,相当于在导演喊出“action“时每个人都要来导演面前”领任务“。而有了多态思想之后,这些人需要做的事情,已经被安排到他们每个对象身上。只需要等待导演的“action”,就马上做自己的事情了。

消除if else的代码参照上面的代码。