JavaScript中的多态是什么?

111

我已经阅读了一些关于多态性的可能文章。但我认为我还没有很好地理解它的含义和重要性。大部分文章都没有说为什么它很重要以及如何在面向对象编程中实现多态行为(当然是在JavaScript中)。

我无法提供任何代码示例,因为我还不知道如何实现它,所以我的问题如下:

  1. 它是什么?
  2. 为什么我们需要它?
  3. 它是如何工作的?
  4. 我该如何在JavaScript中实现这种多态行为?

我有一个例子,但是很容易理解这段代码将产生什么结果,它并没有清楚地说明多态性本身。

function Person(age, weight) {
    this.age = age;
    this.weight = weight;
    this.getInfo = function() {
        return "I am " + this.age + " years old " +
        "and weighs " + this.weight +" kilo.";
    }
}
function Employee(age, weight, salary) {
    this.salary = salary;
    this.age = age;
    this.weight = weight;
    this.getInfo = function() {
        return "I am " + this.age + " years old " +
        "and weighs " + this.weight +" kilo " +
        "and earns " + this.salary + " dollar.";
    }
}

Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
  // The argument, 'obj', can be of any kind
  // which method, getInfo(), to be executed depend on the object
  // that 'obj' refer to.

function showInfo(obj) {
    document.write(obj.getInfo() + "<br>");
}

var person = new Person(50,90);
var employee = new Employee(43,80,50000);
showInfo(person);
showInfo(employee);

这个问题可能太宽泛了,不适合在StackOverflow上得到很好的解答。我们最好能做的就是为您提供一些其他多态性解释的链接。StackOverflow最擅长回答关于特定问题或澄清特定问题的问题,例如“源代码中说多态性是XYZ,但Y是什么意思?” - Vitruvie
2
你根本不需要它。在JS中甚至不需要类,实际上,有许多其他的范例可以用于应用程序构建,其中一些可能更好。apply/call/bind消除了同质性的需求,并且使用软对象,您可以修改任何内容以满足您的需求,而无需预先装饰或继承特殊情况。 - dandavis
1
多态性不仅与面向对象有关,它还有许多含义。您可能想阅读此问题下的其他答案 Is Polymorphism Possible without Inheritance - Edwin Dalorzo
在JavaScript中,继承通常被错误地实现。创建一个Parent的实例来用作Child原型的方式表明了对构造函数和原型在定义和创建对象方面的作用缺乏理解。有关更多信息,请参见此答案:https://dev59.com/J2Qo5IYBdhLWcg3wbe5K#16063711 - HMR
7个回答

117

多态是面向对象编程中的十项原则之一。它是一种设计对象共享行为并能够覆盖这些共享行为的特定实现的做法。为了实现多态,它利用了继承的优势。

在面向对象编程中,一切都被认为是对象。这种抽象可以一直延伸到汽车零部件,或者像年份、制造商和型号这样简单的车型。

要创建一个多态的汽车场景,首先需要有基础汽车类型,然后还需要子类,这些子类继承自汽车并提供自己的行为,以覆盖汽车本身已经具备的基本行为。例如,子类可以是拖车,它仍然具有年份、制造商和型号,但还可能具有一些额外的行为和属性,这些属性可以作为一个是否正在牵引的标志,也可以是有关牵引设备的详细信息。

回到人和员工的例子上来,所有的员工都是人,但不是所有的人都是员工,也就是说人将是超类,而员工将是子类。人可能有年龄和体重,但没有薪水。员工是人,所以他们天生就具有年龄和体重,但也由于他们是员工,他们会有一份薪水。

因此,为了实现这一点,我们首先需要编写超类(Person)。

function Person(age,weight){
 this.age = age;
 this.weight = weight;
}

我们将赋予用户共享他们信息的能力。

Person.prototype.getInfo = function(){
 return "I am " + this.age + " years old " +
    "and weighs " + this.weight +" kilo.";
};

接下来,我们希望有一个Person的子类Employee

function Employee(age,weight,salary){
 this.age = age;
 this.weight = weight;
 this.salary = salary;
}
Employee.prototype = new Person();

我们将通过定义更适合员工的一个函数,来覆盖 getInfo 的行为。

Employee.prototype.getInfo = function(){
 return "I am " + this.age + " years old " +
    "and weighs " + this.weight +" kilo " +
    "and earns " + this.salary + " dollar.";  
};

这些可以像您原来的代码使用一样使用

var person = new Person(50,90);
var employee = new Employee(43,80,50000);

console.log(person.getInfo());
console.log(employee.getInfo());

然而,这里使用继承并没有太多的收益,因为Employee的构造函数与Person的构造函数非常相似,并且原型中唯一的函数被覆盖了。多态设计的强大之处在于共享行为。


2
@user3138436 - 没错。原型链将被检查,以找到第一次出现的 getInfo,因为它在链中的位置比 Person 的更高,所以这个方法是 Employee 的。这就是我所说的“覆盖”的意思。 - Travis J
23
你没有重新使用构造函数(Person.call(this,arg)),并将Employee原型设置为Person的实例。原型是共享成员的地方,而构造函数是创建特定实例成员的地方。你的示例代码复制粘贴了构造函数的代码,并错误地继承了原型部分(Person有特定于实例的成员,这些成员不应该在Employee.prototype上,尤其是如果你有可变成员)。有关使用构造函数和原型进行继承的更多信息,请参见此处:https://dev59.com/J2Qo5IYBdhLWcg3wbe5K#16063711 - HMR
3
员工可以通过以下方式重用和扩展个人信息的getinfo方法: return Person.prototype.getInfo.call(this) + "并赚取" + this.salary + "美元。"; 这样做不仅可以避免复制黏贴代码,还可以实现代码的重用。 - HMR
7
多态应用在哪里? - albert Jegani
3
多态的重点在于 OP 的示例可以更好地展现出来。函数 showInfo() 接受一个普通对象,现在多态是根据对象类型而有所不同的反应能力。我认为这个答案没有表达得够清楚,因为它对每个特定的对象都调用了 getInfo()。 - Stefan
显示剩余13条评论

31

正如这个答案所解释的那样,多态有不同的解释。

我读过的关于这个主题最好的解释是由著名类型理论家Luca Cardelli写的一篇文章。这篇文章的名称是《理解类型、数据抽象和多态性》(On Understanding Types, Data Abstraction, and Polymorphism)

它是什么?

Cardelli在这篇文章中定义了几种类型的多态:

  • 通用的(universal)
  • 参数化(parametric)
  • 包容(inclusion)
  • 特定场景的(ad-hoc)
  • 重载(overloading)
  • 强制转换(coercion)

或许在JavaScript中,很难看到多态的效果,因为更经典的多态类型在静态类型系统中更加明显,而JavaScript具有动态类型系统。

例如,在JavaScript中,没有方法或函数的重载或自动类型转换。在动态语言中,我们通常认为这些事情是理所当然的。由于语言的动态性质,我们也不需要像参数化多态那样的东西。

然而,JavaScript具有一种类型继承形式,模拟了与Java或C#等其他面向对象编程语言中的子类型多态相同的思想(如我在上面分享的另一个答案所解释的那样)。

动态语言中另一种非常典型的多态形式被称为鸭子类型

认为多态只与面向对象编程有关是错误的。其他编程模型(如函数式、过程式、逻辑等)在其类型系统中提供了不同形式的多态,在仅熟悉面向对象编程的人看来可能有点陌生。

为什么我们需要它?

多态性在软件中促进了许多良好的属性,其中包括促进模块化和可重用性,并使类型系统更加灵活和可塑。如果没有它,很难推理出类型。多态性确保一个类型可以被其他兼容的类型替换,只要它们满足公共接口,因此这也促进了信息隐藏和模块化。

它是如何工作的?

这不是简单的问题,不同的语言有不同的实现方法。在JavaScript的情况下,如上所述,您将看到它以类型层次结构的形式实现,使用原型继承,您还可以使用鸭子类型来利用它。

这个主题比较广泛,您在一个帖子中提出了太多问题。也许最好的方法是先阅读Cardelli的论文,然后尝试理解多态性,而不考虑任何语言或编程范式,然后您将开始将理论概念与像JavaScript这样的任何特定语言提供的实现联系起来。


19

多态的目的是什么?

多态使得静态类型系统在不失去(显著)静态类型安全的情况下更加灵活,通过放宽类型等价条件来实现。证明仍然是程序只有不包含任何类型错误时才能运行。

多态函数或数据类型比单态函数或数据类型更通用,因为它可以在更广泛的场景中使用。从这个意义上讲,多态代表了严格类型语言中泛化的思想。

这如何适用于Javascript?

Javascript具有弱动态类型系统。这样的类型系统相当于只包含一个类型的严格类型系统。我们可以将这样的类型视为巨大的联合类型(伪代码语法):

type T =
 | Undefined
 | Null
 | Number
 | String
 | Boolean
 | Symbol
 | Object
 | Array
 | Map
 | ...

每个值都将在运行时与这些类型的其中一个关联。由于Javascript是弱类型语言,每个值可以任意更改其类型。

如果我们从类型理论的角度出发,认为只有一种类型,那么我们可以确定Javascript的类型系统没有多态的概念。相反,我们有鸭子类型和隐式类型转换。

但这不应阻止我们在程序中考虑类型。由于Javascript缺乏类型,我们需要在编码过程中推断它们。我们的思维必须替代缺失的编译器,即当我们看到一个程序时,我们必须识别出不仅是算法,还有底层(可能是多态的)类型。这些类型将帮助我们构建更可靠、更健壮的程序。

为了正确地做到这一点,我将为您概述多态最常见的表现形式。

参数多态(又称泛型)

参数多态表示不同的类型是可互换的,因为类型根本不重要。定义一个或多个参数为参数多态类型的函数不必知道相应的参数是什么,而是将它们全部视为相同的,因为它们可以适应任何类型。这是相当限制的,因为这样的函数只能处理其参数中不属于数据部分的属性:

// parametric polymorphic functions

const id = x => x;

id(1); // 1
id("foo"); // "foo"

const k = x => y => x;
const k_ = x => y => y;

k(1) ("foo"); // 1
k_(1) ("foo"); // "foo"

const append = x => xs => xs.concat([x]);

append(3) ([1, 2]); // [1, 2, 3]
append("c") (["a", "b"]); // ["a", "b", "c"]

自适应多态(又称重载)

自适应多态指的是不同类型仅在特定目的下等效。为了在这个意义上等效,一个类型必须实现一组特定于该目的的函数。定义一个或多个参数为自适应多态类型的函数需要知道哪些函数集与其各个参数相关联。

自适应多态使函数与更大范围的类型兼容。下面的例子说明了“映射”目的以及类型如何实现此约束。与一组函数不同,“可映射”约束仅包括单个map函数:

// Option type
class Option {
  cata(pattern, option) {
    return pattern[option.constructor.name](option.x);
  }
  
  map(f, opt) {
    return this.cata({Some: x => new Some(f(x)), None: () => this}, opt);
  }
};

class Some extends Option {
  constructor(x) {
    super(x);
    this.x = x;
  }
};

class None extends Option {
  constructor() {
    super();
  }
};


// ad-hoc polymorphic function
const map = f => t => t.map(f, t);

// helper/data

const sqr = x => x * x;

const xs = [1, 2, 3];
const x = new Some(5);
const y = new None();

// application

console.log(
  map(sqr) (xs) // [1, 4, 9]
);

console.log(
  map(sqr) (x) // Some {x: 25}
);

console.log(
  map(sqr) (y) // None {}
);

子类型多态性

由于其他答案已经涵盖了子类型多态性,我将跳过它。

结构多态性(也称为结构子类型)

结构多态性表示,如果不同的类型具有相同的结构,那么它们是等效的。其中一个类型具有另一个类型的所有属性,但可能包含额外的属性。话虽如此,结构多态性在编译时实现了鸭子类型,并提供了一些额外的类型安全性。但是,通过声称两个值属于同一类型,仅因为它们共享某些属性,它完全忽略了值的语义级别:

const weight = {value: 90, foo: true};
const speed =  {value: 90, foo: false, bar: [1, 2, 3]};

不幸的是,速度被认为是重量的子类型,当我们比较value属性时,实际上我们正在比较苹果和橙子。


1
虽然从许多方面来看,这个回答更加“准确”(当然也更加全面)比起被接受的答案,但是它并不具有相同的易懂性:这个回答假设提问者已经太聪明了,不需要费心思考这个问题 :) - Jared Smith
@JaredSmith 我试图将这个主题简化为一些易于理解的段落。但是我越深入挖掘,它就变得越复杂。我从来没有找到一个好的关于无类型语言中多态性的来源,因此我认为这个答案仍然有价值。 - user6445533
编辑大大提高了可访问性。至于动态语言中多态的实用性,有很多,但我很难想到JS中的一个好例子。更好的例子是Python的魔术方法,它允许用户定义的类型与多态函数(如len)一起工作。或者来自Clojure的conj - Jared Smith

9

什么是多态?

Poly= 多,morphism=形式或行为切换。

为什么需要多态?

在编程中,当我们希望一个函数(比如说函数X)的接口足够灵活,可以接受不同类型或数量的参数时,就会使用多态。此外,根据参数类型或数量的变化,我们可能希望函数X表现出不同的形态(morphism)。

它是如何工作的?

我们编写函数X的多个实现,每个实现都接受不同类型或数量的参数。根据参数的类型或数量,编译器(在运行时)在某段代码调用X时决定执行哪个X实现。

我该如何在javascript中实现这种多态行为?

JS不是一种类型化的语言,因此并不适合使用面向对象编程(OOP)的概念,如多态。然而,新版本的JS现在包括类,并且有可能多态在JS中开始有意义。其他答案提供了一些有趣的解决方法。


7

多态性是指能够在不同的对象上调用相同的方法,并且每个对象都会以不同的方式响应,这就是所谓的多态性

    function Animal(sound){
    this.sound=sound;
    this.speak=function(){
       return this.sound;
     }
    }
//one method 
    function showInfo(obj){
      console.log(obj.speak());
    }
//different objects
    var dog = new Animal("woof");
    var cat = new Animal("meow");
    var cow = new Animal("humbow");
//responds different ways
    showInfo(dog);
    showInfo(cat);
    showInfo(cow);


3

JavaScript是一种解释型语言,而不是编译型语言。

编译时多态性(或静态多态性)编译时多态性就是Java、C++中的方法重载。

所以在JavaScript中不可能进行方法重载。

但动态(运行时)多态性是存在于运行时的多态性,因此在JavaScript中可以进行方法重写。

另一个例子是PHP。


2
多态是指定义一种通用的行为类型,这种类型在应用于不同类型时会有不同的表现。
举个例子,假设我们有一个动物类实现了talk方法。如果狗和猫继承自动物类中的talk(),那么对象dog和cat都会说话,但形式不同。
当我们迭代一个集合时,该集合必须支持可迭代协议。集合对象的形式无关紧要,无论它是数组还是字典。
+、-、*、/ 运算符是多态的。因为它们可以与不同的类型一起使用:复数、小数、整数。
多态的主要好处是可以重用代码。如果您有重复的代码,您需要额外的内存空间来保存,这会降低性能,并且还需要人或其他东西来维护该重复的代码。
由于您正在重用单个代码,因此有助于轻松调试您的代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接