JavaScript 中的原型继承






4.89/5 (21投票s)
JavaScript 中的原型继承
引言
我用 JavaScript 已经有一段时间了,大多数情况下都觉得它很直接。最近,我一直在使用它以及一些框架,包括 React.js 和 Node.js。作为一名 Web 开发者,JavaScript 是你工具箱的一部分,并且熟练使用它绝对是必需的。
我的背景是 C++ 和 C# 等经典的面向对象语言,我使用 JavaScript 进行 Web 开发。因此,JavaScript 中有几个地方让我感到困惑,因为它与大多数其他语言的运作方式不同。我希望能在未来的文章中涵盖那些其他方面。
本文的目的是涵盖 JavaScript 对面向对象的实现,特别是它是如何实现继承的。
背景
本文假定读者熟悉 JavaScript。虽然这里描述的概念同样适用于所有原型语言,但为了本文的目的,我将专门使用 JavaScript 来进行所有解释和示例。
大多数读者会熟悉 C++、Java、C# 等语言的经典继承。这可能会在理解 JavaScript 中的继承时造成一定程度的困惑,因为 JavaScript 使用的是所谓的原型继承。JavaScript 并非唯一采用这种技术实现继承的语言。其他语言包括 Self、Lua 和 NewtonScript 等。
在经典的面向对象语言中,你基于抽象创建类(类型)。然后,我们将属性和行为分配给这些类。这些类在我们的代码中被实例化以执行各种操作和任务。例如,你可能定义一个 Order 类,并为其添加返回订单总金额的行为。在你的应用程序中,你会在需要时创建这些 Order 类的实例,并调用它们的行为,例如调用特定订单返回订单总额的行为。
JavaScript 中的对象
对象是 JavaScript 中的一个重要原始类型。该语言就是建立在对象之上的。而在建立在类之上的经典面向对象语言中,JavaScript 却是建立在对象之上的。因此,在 JavaScript 中,你可以如下创建一个新的对象实例。
myObject = new Object();
你也可以使用此语法来创建相同的对象。
var myObject = {};
此语法使用了对象字面量语法。在 JavaScript 中,对象本质上是键值对的集合,如下例所示。
myObject = {"Firstname": "Fred", "LastName": "Smith" };
从这个对象字面量表示法可以看出,JSON 格式的来源就在于此。JSON 本质上是 JavaScript 对象字面量,不包含任何行为(方法)。
由于 JavaScript 是一种函数式语言(函数是第一公民),因此你也可以使用函数来创建对象,如下例所示。
function myObject(){}
var myObject = new myObject();
这就是在 JavaScript 中创建对象的方法。现在让我们继续讨论继承。
JavaScript 中的继承
到目前为止一切都相当直接。然而,JavaScript 的工作方式并非如此。它是一种无类、面向对象的语言。在 JavaScript(或任何其他原型语言)中没有类的概念。尽管某些文本可能表明 JavaScript 能够实现经典继承,但事实并非如此。这只是语法糖,给经典语言的开发者一种错觉,让他们认为可以在 JavaScript 中实现经典继承。归根结底,在 JavaScript 中,所有继承最终都是通过原型继承实现的。这是因为。
- JavaScript 是无类的(所有经典的面向对象语言都依赖于类的基本概念)
- 所有继承最终都是通过原型链实现的(无论是直接还是间接使用语法糖)
经典继承在 JavaScript 中是模拟的。
什么是原型继承?
JavaScript 中的对象包含一个名为 prototype
的内部对象属性。
Object.prototype
prototype
对象是当前对象所基于的对象,即它是当前对象的原型。用经典面向对象的语言来说,prototype
就是当前对象所创建的基类或祖先。当前对象上找到的所有方法和属性也可以在其 prototype
上找到。这个 prototype
对象反过来也包含一个 prototype
对象,其中包含它所创建的对象。这条链(称为原型链)一直向上延伸,直到我们最终达到根 Object
。此时我们就不能再往上走了,因为我们已经到达了原型链的顶部。
让我们从一个名为 o 的对象开始。
var o;
我们的对象 o 包含一个名为 prototype
的属性。这个对象就是 o 所创建的对象(或者用经典术语来说,是 o 所派生的对象)。
var o = new Object()
console.log("typeof Object: " + typeof o); //outputs object
var p = o.prototype;
console.log("typeof o.prototype: " + typeof p); //outputs undefined
我们可以一直这样做,直到最终达到原型链的顶部。
JavaScript 中的继承是通过两个关键概念实现的。
- 遍历原型链 - 如果 JavaScript 找不到指定的属性/方法,它会查找对象的
prototype
。如果在那里找不到,它会查找该对象的prototype
。它会不断查找每个对象的prototype
以查找指定的属性/方法,直到找到它或到达原型链的顶部。 prototype
对象可以被不同的对象共享。不同的对象可以共享同一个prototype
对象,因此可以共享相同的方法和属性。
如何实现原型继承?
现在我们已经理解了 JavaScript 中实现继承的底层概念,让我们来看一个简单的例子。在这里,我们定义了一个名为 Vehicle 的对象。然后,我们向 Vehicle 对象添加一个名为 make 的属性,并添加一个返回该属性的函数。
//Defining and instantiating an object called Vehicle using a function
function Vehicle() {}
var vehicle = new Vehicle();
//Adding properties to the Vehicle object. We can either add these as
//instance methods / properties, or to the Vehicle prototype so all
//Vehicles can inherit them.
//adding an instance property
function Vehicle(make) {
// Instance properties can be set on each instance of the class
this.make = make;
}
//Adding the property to the prototype ensures that all instances
//share the property
Vehicle.prototype.Make = function() {
console.log("My make is " + this.make);
};
var vehicle = new Vehicle('Vehicle');
vehicle.Make(); // My make is Vehicle
到目前为止一切都很好。让我们通过创建一个 Car 类来扩展 Vehicle 对象。
function Car(make){
Vehicle.call(this, make);
}
Car.prototype = new Vehicle();
var car = new Car("Car");
car.Make();
我们正在将 Vehicle 对象赋值给 Car 的 prototype
。这使得 Car 对象可以继承 Vehicle 的所有方法和属性。我们使用 JavaScript 的 call()
方法来实现这一点。它的作用是允许我们调用一个函数并指定其执行的上下文 - 因此我们传入了 this
对象。我们知道 Car 已经继承了 Vehicle 的属性,因为我们调用了仅在我们的 Vehicle 对象上定义的 Make() 方法。
摘要
对于来自经典面向对象背景的开发者来说,这种继承方法可能更难理解。我知道我花了一段时间才完全掌握其中的运作原理。如果你使用 JavaScript,那么理解 JavaScript 如何实现继承非常重要,希望本文能提供一个很好的概述。如果你希望我进一步阐述本文中的任何内容,请随时留下评论。