Learn web development

JavaScript 中的继承

了解了 OOJS 的大多数细节之后,本文将介绍如何创建“子”对象类别(构造器)并从“父”类别中继承功能。此外,我们还会针对何时何处使用 OOJS 给出建议。

预备知识: 基本的计算机素养,对 HTML 和 CSS 有基本的理解,熟悉 JavaScript 基础(参见 First stepsBuilding blocks)以及面向对象的JavaScript (OOJS) 基础(参见 Introduction to objects)。
目标: 理解在 JavaScript 中如何实现继承。

原型式的继承

 

到目前为止我们已经了解了一些关于原型链的实现方式以及成员变量是如何通过它来实现继承,但是之前涉及到的大部分都是浏览器内置函数(比如 StringDateNumber 和 Array),那么我们如何创建一个继承自另一对象的JavaScript对象呢?

正如前面课程所提到的,有些人认为JavaScript并不是真正的面向对象语言,在经典的面向对象语言中,你可能倾向于定义类对象,然后你可以简单地定义哪些类继承哪些类(参考C++ inheritance里的一些简单的例子),JavaScript使用了另一套实现方式,继承的对象函数并不是通过复制而来,而是通过原型链继承(通常被称为 原型式继承 —— prototypal inheritance

让我们通过具体的例子来解释上述概念

开始

首先,将oojs-class-inheritance-start.html文件复制到你本地(也可以点击 running live 在线查看 ),其中你能看到一个只定义了一些属性的Person构造器,与之前通过模块来实现所有功能的Person的构造器类似。


function Person(first, last, age, gender, interests) {
  this.name = {
    first,
    last
  };
  this.age = age;
  this.gender = gender;
  this.interests = interests;
};

所有的方法都定义在构造器的prototype上,比如:


Person.prototype.greeting = function() {
  alert('Hi! I\'m ' + this.name.first + '.');
};

比如我们想要创建一个Teacher类,就像我们前面在面向对象概念解释时用的那个一样。这个类会继承Person的所有成员,同时也包括:

  1. 一个新的属性,subject——这个属性包含了教师教授的学科。
  2. 一个被更新的greeting()方法,这个方法打招呼听起来比一般的greeting()方法更正式一点——对于一个教授一些学生的老师来说。

定义 Teacher() 构造函数

我们要做的第一件事是创建一个Teacher()构造器——将下面的代码加入到现有代码之下:


function Teacher(first, last, age, gender, interests, subject) {
  Person.call(this, first, last, age, gender, interests);
  this.subject = subject;
}

这在很多方面看起来都和Person的构造器很像,但是这里有一些我们从没见过的奇怪玩意——call()函数。基本上,这个函数允许你调用一个在这个文件里别处定义的函数。第一个参数指明了在你运行这个函数时想对“this”指定的值,也就是说,你可以重新指定你调用的函数里所有“this”指向的对象。其他的变量指明了所有目标函数运行时接受的参数。

 

Note: In this case we specify the inherited properties when we create a new object instance, but note that you'll need to specify them as parameters in the constructor even if the instance doesn't require them to be specified as parameters (Maybe you've got a property that's set to a random value when the object is created, for example.)

注意:在这个例子里我们在创建一个新的对象实例时同时指派了继承的所有属性,但是注意你需要在构造器里将它们作为参数来指派,即使实例不要求它们被作为参数指派(比如也许你在创建对象的时候已经得到了一个设置为任意值的属性)

所以在这个例子里,我们很有效的在Teacher()构造函数里运行了Person()构造函数(见上文),得到了和在Teacher()里定义的一样的属性,但是用的是传送给Teacher(),而不是Person()的值(我们简单使用这里的this作为传给call()this,意味着this指向Teacher()函数)。

在构造器里的最后一行代码简单地定义了一个新的subject属性,这将是教师会有的,而一般人没有的属性。

顺便提一下,我们本也可以这么做:


function Teacher(first, last, age, gender, interests, subject) {
  this.name = {
    first,
    last
  };
  this.age = age;
  this.gender = gender;
  this.interests = interests;
  this.subject = subject;
}

但是这只是重新定义了一遍属性,并不是将他们从Person()中继承过来的,所以这违背了我们的初衷。这样写也会需要更长的代码。

设置 Teacher() 的 prototype 和 constructor引用

到目前为止一切看起来都还行,但是我们遇到问题了。我们已经定义了一个新的构造器,这个构造器默认有一个空的原型属性。我们需要让Teacher()Person()的原型对象里继承方法。我们要怎么做呢?

  1. 在你上的添加内容的下面加上以下这一行:
    
    Teacher.prototype = Object.create(Person.prototype);
    这里我们的老朋友 create() 又来帮忙了——在这个例子里我们用这个函数来创建一个和Person.prototype一样的新的原型属性值(这个属性指向一个包括属性和方法的对象),然后将其作为Teacher.prototype的属性值。这意味着Teacher.prototype现在会继承Person.prototype的所有属性和方法。
  2. 在我们接下去做之前,还需要完成一件事 — 现在Teacher()prototypeconstructor属性指向的是Person(), 这是因为我们生成Teacher()的方式决定的。(这篇 Stack Overflow post 文章会告诉你详细的原理) — 将你写的页面在浏览器中打开,进入JavaScript控制台,输入以下代码来确认:
    
    Teacher.prototype.constructor
  3. 这或许会成为很大的问题,所以我们需要将其正确设置——你可以回到源代码,在底下加上这一行代码来解决:
    
    Teacher.prototype.constructor = Teacher;
  4. 当你保存并刷新页面以后,输入Teacher.prototype.constructor就会得到Teacher()

译者注:每一个函数对象(Function)都有一个prototype属性,并且只有函数对象有prototype属性,因为prototype本身就是定义在Function对象下的属性。当我们输入类似var person1=new Person(...)来构造对象时,Javascript实际上参考的是Person.prototype指向的对象来生成person1。另一方面,Person()函数是Person.prototype的构造函数,也就是说Person===Person.prototype.constructor(不信的话可以试试)。

在定义新的构造函数Teacher时,我们通过function.call来调用父类的构造函数,但是这样无法自动指定Teacher.prototype的值,这样Teacher.prototype就只能包含在构造函数里构造的属性,而没有方法。因此我们利用Create方法将Person的原型对象复制给Teacher的原型对象,并改变其构造器指向,使之与Teacher关联。

任何你想要被继承的方法都应该定义在构造函数的prototype对象里,并且永远使用父类的prototype来创造子类的prototype,这样才不会打乱类继承结构。

向 Teacher() 增加新的函数 greeting()

为了完善代码,你还需在构造函数Teacher()上定义一个新的函数greeting()。最简单的方法是在Teacher的原型上定义它—把以下代码添加到你代码的底部:


Teacher.prototype.greeting = function() {
  var prefix;
  if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
    prefix = 'Mr.';
  } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
    prefix = 'Mrs.';
  } else {
    prefix = 'Mx.';
  }
  alert('Hello. My name is ' + prefix + ' ' + this.name.last + ', and I teach ' + this.subject + '.');
};

This alerts the teacher's greeting, which also uses an appropriate name prefix for their gender, worked out using a conditional statement.

范例尝试

Now you've entered all the code, try creating an object instance from Teacher() by putting the following at the bottom of your JavaScript (or something similar of your choosing):


var teacher1 = new Teacher('Dave', 'Griffiths', 31, 'male', ['football', 'cookery'], 'mathematics');

Now save and refresh, and try accessing the properties and methods of your new teacher1 object, for example:


teacher1.name.first;
teacher1.interests[0];
teacher1.bio();
teacher1.subject;
teacher1.greeting();

These should all work just fine; the first three access members that were inherited from the generic Person() constructor (class), while the last two access members that are only available on the more specialized Teacher() constructor (class).

Note: If you have trouble getting this to work, compare your code to our finished version (see it running live also).

The technique we covered here is not the only way to create inheriting classes in JavaScript, but it works OK, and it gives you a good idea about how to implement inheritance in JavaScript.

You might also be interested in checking out some of the new ECMAScript features that allow us to do inheritance more cleanly in JavaScript (see Classes). We didn't cover those here, as they are not yet supported very widely across browsers. All the other code constructs we discussed in this set of articles are supported as far back as IE9 or earlier, and there are ways to achieve earlier support than that.

A common way is to use a JavaScript library — most of the popular options have an easy set of functionality available for doing inheritance more easily and quickly. CoffeeScript for example provides class, extends, etc.

更多练习

In our OOP theory section, we also included a Student class as a concept, which inherits all the features of Person, and also has a different greeting() method to Person that is much more informal than the Teacher's greeting. Have a look at what the student's greeting looks like in that section, and try implementing your own Student() constructor that inherits all the features of Person(), and implements the different greeting() function.

Note: If you have trouble getting this to work, have a look at our finished version (see it running live also).

对象成员总结

To summarize, you've basically got three types of property/method to worry about:

  1. Those defined inside a constructor function that are given to object instances. These are fairly easy to spot — in your own custom code, they are the members defined inside a constructor using the this.x = x type lines; in built in browser code, they are the members only available to object instances (usually created by calling a constructor using the new keyword, e.g. var myInstance = new myConstructor()).
  2. Those defined directly on the constructor themselves, that are available only on the constructor. These are commonly only available on built-in browser objects, and are recognized by being chained directly onto a constructor, not a instance. For example, Object.keys().
  3. Those defined on a constructor's prototype, which are inherited by all instances and inheriting object classes. These include any member defined on a Constructor's prototype property, e.g. myConstructor.prototype.x().

If you are not sure which is which, don't worry about it just yet — you are still learning, and familiarity will come with practice.

何时在 JavaScript 中使用继承?

特别是在读完这段文章内容之后,你也许会想 "天啊,这实在是太复杂了". 是的,你是对的,原型和继承代表了Javascript这门语言里最复杂的一些方面,但是Javascript的强大和灵活性正是来自于它的对象体系和继承方式,这很值得花时间去好好理解下它是如何工作的。

在某种程度上来说,你一直都在使用继承 - 无论你是使用WebAPI的不同特性还是调用字符串、数组等浏览器内置对象的方法和属性的时候,你都在隐式地使用继承。

就在自己代码中使用继承而言,你可能不会使用的非常频繁,特定是在小型项目中或者刚开始学习时 - 因为当你不需要对象和继承的时候,仅仅为了使用而使用它们只是在浪费时间而已。但是随着你的代码量的增大,你会越来越发现它的必要性。如果你开始创建一系列拥有相似特性的对象时,那么创建一个包含所有共有功能的通用对象,然后在更特殊的对象类型中继承这些特性,将会变得更加方便有用。

译者注: 考虑到Javascript的工作方式,由于原型链等特性的存在,在不同对象之间功能的共享通常被叫做 委托 - 特殊的对象将功能委托给通用的对象类型完成。这也许比将其称之为继承更为贴切,因为“被继承”了的功能并没有被拷贝到正在“进行继承”的对象中,相反它仍存在于通用的对象中。

在使用继承时,建议你不要使用过多层次的继承,并仔细追踪定义方法和属性的位置。很有可能你的代码会临时修改了浏览器内置对象的原型,但你不应该这么做,除非你有足够充分的理由。过多的继承会在调试代码时给你带来无尽的混乱和痛苦。

总之,对象是另一种形式的代码重用,就像函数和循环一样,有他们特定的角色和优点。如果你发现自己创建了一堆相关的变量和函数,还想一起追踪它们并将其灵活打包的话,对象是个不错的主意。对象在你打算把一个数据集合从一个地方传递到另一个地方的时候非常有用。这些都可以在不使用构造器和继承的情况下完成。如果你只是需要一个单一的对象实例,也许使用对象常量会好些,你当然不需要使用继承。

总结

This article has covered the remainder of the core OOJS theory and syntax that we think you should know now. At this point you should understand JavaScript object and OOP basics, prototypes and prototypal inheritance, how to create classes (constructors) and object instances, add features to classes, and create subclasses that inherit from other classes.

In the next article we'll have a look at how to work with JavaScript Object Notation (JSON), a common data exchange format written using JavaScript objects.

另见

  • ObjectPlayground.com — A really useful interactive learning site for learning about objects.
  • Secrets of the JavaScript Ninja, Chapter 6 — A good book on advanced JavaScript concepts and techniques, by John Resig and Bear Bibeault. Chapter 6 covers aspects of prototypes and inheritance really well; you can probably track down a print or online copy fairly easily.
  • You Don't Know JS: this & Object Prototypes — Part of Kyle Simpson's excellent series of JavaScript manuals, Chapter 5 in particular looks at prototypes in much more detail than we do here. We've presented a simplified view in this series of articles aimed at beginners, whereas Kyle goes into great depth and provides a more complex but more accurate picture.

文档标签和贡献者