65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript 中的原型继承

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.39/5 (13投票s)

2015年3月18日

CPOL

6分钟阅读

viewsIcon

27004

本文解释了 JavaScript 中如何使用原型来实现继承链,以及如何实现类似于经典编程模型的自定义继承。

引言

JavaScript 是使用最广泛的编程语言,然而大量的程序员在使用 JavaScript 时并不知道其细微之处。JavaScript 中的继承支持是另一个常常让那些将 JavaScript 作为第二编程语言学习并来自 C# 和 Java 等面向对象背景的程序员感到困惑的特性。

这篇文章是我为阐明 JavaScript 中的继承,它是如何以及为何有用的,以及使其工作的各个组成部分,最后是如何在 JavaScript 中模仿经典继承模型所做的努力。

原型继承

函数是 JavaScript 中的一等公民,这是什么意思?这意味着函数在语言的各个方面都受到特殊对待。让我们看一个如下所示的非常简单的函数:

function Animal() {

}
var animal = new Animal();

上面的代码写得虽然简单,但在底层却做了相当复杂的事情。“Function”是一个全局函数,JavaScript 中的每个函数都是“Function”函数的对象,是的,这不是笔误。

在上面代码片段被调用后发生的事情可以用下图可视化:

在继续阅读时,请牢记:
a) 所有 JavaScript 函数都有“prototype”属性。
b) 所有 JavaScript 对象都有一个内部链接“__proto__”,它指向该对象构造函数的原型。(继续阅读就会明白)。

让我们更详细地回顾一下上面的图示。

  1. 创建了“Animal”函数对象。
  2. Animal 被赋予了一个名为“prototype”的属性,并将一个新的对象赋值为 Animal 对象“prototype”属性的值,我将这个对象称为“Animal.prototype”。
  3. Animal.prototype 对象获得了一个名为“constructor”的属性,它指向“Animal”函数对象本身。
  4. Animal.prototype 对象有一个指向另一个对象的内部链接,可以通过其“__proto__”属性检索到。这个内部链接实现了 JavaScript 中继承的所有魔法。“__proto__”的 Animal.prototype 指向另一个对象,这个对象就是“Object.prototype”。
  5. 因为“Object”和“Animal”都是 JavaScript 函数,并且也是全局函数“Function”的对象,
    两者都有指向“Function.prototype”的内部链接“__proto__”。
  6. Object.prototype”的“__proto__”属性指向 null。
  7. null”对象是继承链的终点。
  8. 对于每个函数调用,如“new Animal()”,都会创建一个新对象,所有这些对象都将具有指向 Animal.prototype 的内部链接 __proto__
  9. 最后,“Function.__proto__”的链接指向哪里?嗯,它指向“Function.prototype”指向的同一个对象。


注意:__proto__ 的使用是有争议的,因为它最初并未包含在 ECMASCRIPT 规范中,但在 ECMASCRIPT 6 中,它已被包含在规范中并已标准化。还有其他方法可以访问链接对象,我将在本文稍后提到这些方法。

当我们使用“new Animal()”创建一个函数“Animal”的新对象时,会创建一个新的空对象,并将“Animal.prototype”设置为其内部链接“__proto__”。

内部链接“__proto__”的用途是什么?

上面看到的内部链接“__proto__”实现了 JavaScript 中所谓的原型继承的所有魔法。当我们尝试访问 JavaScript 中对象的任何属性(或方法)时,JavaScript 会通过内部链接“__proto__”遍历整个对象链来查找该属性,直到在任何对象中找到该属性或到达“null”对象。

在原型继承中,对象动态地从对象继承属性,这与类(对象的蓝图)从其他类继承的经典继承不同。

这是 JavaScript 的一个非常强大的功能,也是它如此酷的原因。

让我们再举一个例子:

var Person = function (name) {
    this.name = name;
}

Person.prototype.age = 24;
var person1 = new Person("Mike");

在上面的代码中,我们创建了一个简单的构造函数“Person”。理论上,JavaScript 中的任何函数都可以充当构造函数,并且按照惯例,我们用大写字母命名构造函数。但是,只有以特定方式编写的构造函数才有用。我将在本文后面更详细地讨论它们。

我们将“age”属性添加到函数的原型中。接下来,让我们使用 new 关键字“person1”创建这个函数的一个对象,并使用以下代码打印 person1 的所有属性:

for (var p in person1) {
    if (person1.hasOwnProperty(p)) {
        console.log(p);
    }
}

上面代码的输出是“name”,不出所料。
所以我们可以看到 person1 对象只有“name”属性。让我们运行以下代码:

console.log(person1.age);?

我们可以看到它确实打印出了 24,所以即使 person1 对象没有“age”属性,我们仍然可以访问它的值,因为它的原型具有该属性。

访问任何对象的原型

  1. 如前所述,您可以通过“__proto__”属性来访问它。
  2. 您也可以使用 Object.getPrototypeOf 方法访问任何对象的原型,如下所示:
    var person2 = new Person("Hussey");
    var personProto = Object.getPrototypeOf(person1);
  3. 根据之前的讨论,请记住,每个函数的原型都有“constructor”属性,它指向函数本身,而且我们可以通过对象直接访问任何对象原型的属性,利用这两个事实,我们可以访问对象的原型,如下所示:
    person2.constructor.prototype

创建自定义继承链

使用对象

这是创建继承的一种非常直接的方式。让我们使用对象字面量创建两个对象 xy,并使用 Object.setPortotypeOf 方法使 x 成为 y 的父对象,如下面的代码所示:

var x = { name: "Mike" };
var y = { age: 27 };
Object.setPrototypeOf(y, x);

这就是从一个对象到另一个对象创建工作继承所需的一切。在这里,对象 y 是从对象 x 派生的。

使用构造函数

构造函数是按照特定方式编写的普通 JavaScript 函数。构造函数使用 new 关键字调用。当我们使用 new 调用构造函数时,会创建一个 new 对象,并将该对象作为其上下文(this)传递给函数。我们可以在函数体内部使用 this 关键字访问任何函数的上下文。

在 C# 等面向对象语言中,要创建继承,一个类(对象的蓝图)是从另一个类派生的,这与我们在上一节中看到的从一个对象派生另一个对象的类比。我们可以使用构造函数在 JavaScript 中模仿类似的经典继承。让我们看看如何实现。

让我们以经典的 Employee Manager 继承为例。这里 Employee 是父类,Manager 是子类。我们将编写一个非常简单的构造函数来表示 Employee 对象的蓝图,如下所示:

var Employee = function (organization) {
    this.organization = organization;
}

Employee.prototype.getOrganization = function () {
    console.log(this.organization);
}

接下来,让我们创建 Manager 构造函数,记住它将从 Employee 派生,我们将 Employee 构造函数及其参数作为 Manager 函数的参数传递,如下所示:

var Manager = function (department, parentArg1, parentFn) {
    this.department = department;
    parentFn.call(this, parentArg1);
}

Manager.prototype.getDepartment = function () {
    console.log(this.department);
}

在下一步中,我们需要建立 Employee Manager 函数之间的原型关系。根据我们先前的讨论,我们知道需要从子函数的原型到父函数原型的内部链接(__proto__),让我们写一个辅助方法来实现这一点,如下所示:

function CreateInheritance(child, parent) {
    for (var p in parent) {
        if (parent.hasOwnProperty(p)) {
            child[p] = parent[p];
        }
    }

    var tempProto = function () { this.constructor = child; };
    tempProto.prototype = parent.prototype;
    child.prototype = new tempProto();
}

在下一步中,让我们使用这个实用函数来实际创建关系并创建一个 Manager 实例:

CreateInheritance(Manager, Employee);

var mgr = new Manager("HR", "A WonderFull Company", Employee);

现在 mgr 对象继承了 Employee Manager 两者的属性和行为,您可以像它们属于对象本身一样访问它们。

结论

正如您所见,它并不十分直观,并且实现 JavaScript 中的经典继承似乎要做很多工作,但它当然是可能的。另外,ECMASCRIPT6 规范确实引入了 JavaScript 中的概念,但它仅仅是语法糖,内部工作原理仍然相同。JavaScript 对原型继承的支持是其最伟大的功能之一,了解它的确切工作原理 certainly 很有帮助。

© . All rights reserved.