JavaScript 中的原型继承






3.39/5 (13投票s)
本文解释了 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__”,它指向该对象构造函数的原型。(继续阅读就会明白)。
让我们更详细地回顾一下上面的图示。
- 创建了“
Animal
”函数对象。 Animal
被赋予了一个名为“prototype
”的属性,并将一个新的对象赋值为Animal
对象“prototype
”属性的值,我将这个对象称为“Animal.prototype
”。Animal.prototype
对象获得了一个名为“constructor
”的属性,它指向“Animal
”函数对象本身。Animal.prototype
对象有一个指向另一个对象的内部链接,可以通过其“__proto__
”属性检索到。这个内部链接实现了 JavaScript 中继承的所有魔法。“__proto__
”的Animal.prototype
指向另一个对象,这个对象就是“Object.prototype
”。- 因为“Object”和“Animal”都是 JavaScript 函数,并且也是全局函数“Function”的对象,
两者都有指向“Function.prototype”的内部链接“__proto__”。 - “
Object.prototype
”的“__proto__
”属性指向 null。 - “
null
”对象是继承链的终点。 - 对于每个函数调用,如“
new Animal()
”,都会创建一个新对象,所有这些对象都将具有指向Animal.prototype
的内部链接__proto__
。 - 最后,“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
”属性,我们仍然可以访问它的值,因为它的原型具有该属性。
访问任何对象的原型
- 如前所述,您可以通过“
__proto__
”属性来访问它。 - 您也可以使用
Object.getPrototypeOf
方法访问任何对象的原型,如下所示:var person2 = new Person("Hussey"); var personProto = Object.getPrototypeOf(person1);
- 根据之前的讨论,请记住,每个函数的原型都有“
constructor
”属性,它指向函数本身,而且我们可以通过对象直接访问任何对象原型的属性,利用这两个事实,我们可以访问对象的原型,如下所示:person2.constructor.prototype
创建自定义继承链
使用对象
这是创建继承的一种非常直接的方式。让我们使用对象字面量创建两个对象 x
和 y
,并使用 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 很有帮助。