JavaScript 中的继承

404 阅读7分钟

参考链接:developer.mozilla.org/en-US/docs/…

一、原型式的继承

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

1、定义一个 Person() 构造器,里面有一些属性:

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

2、所有的方法都定义在构造器的原型上,比如 greeting()

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

现在我们需要创建一个Teacher类,这个类会继承Person的所有成员,同时也包括:

  1. 一个新的属性subject—— 这个属性包含了教师教授的学科;
  2. 一个被更新的greeting()方法,这个方法是跟教授打招呼用的。

3、定义 Teacher() 构造器函数:

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

  this.subject = subject;
}

在这个例子里,我们在Teacher()构造函数里运行了Person()构造函数,得到了和在Teacher()里定义的一样的属性,我们把这里的this作为传给call()this,意味着this指向Teacher()函数。

在构造器里的最后一行代码,定义了一个新的subject属性,这是教师独有的属性。

4、从无参构造函数继承

如果你继承的构造函数不从传入的参数中获取其属性值,则不需要在call()中为其指定其他参数。例如:

function Brick() {
  this.width = 10;
  this.height = 20;
}

可以这样继承widthheight属性:

function BlueGlassBrick() {
  Brick.call(this);

  this.opacity = 0.5;
  this.color = 'blue';
}

5、设置 Teacher 的原型和构造器引用

您有没有发现一个问题,我们定义了一个新的构造函数,它有一个prototype属性,默认情况下它只包含一个引用构造函数本身的对象,不包含Person构造函数的prototype属性的方法。要查看这一点,请在控制台中输入Object.getOwnPropertyNames(Teacher.prototype),然后再次输入,将教师替换为人员。新构造函数没有继承这些方法。比较Person.prototype.greeting和Teacher.prototype.greeting的输出。如果我们需要让Teacher()继承Person()原型上定义的方法,要怎么做呢?

  1. 添加下面这行代码:
Teacher.prototype = Object.create(Person.prototype);

我们使用create创建一个新对象,并使新对象成为Teacher.prototype的值,新对象的原型为Person.prototype,因此,将继承Person.prototype上可用的所有方法。

  1. 现在,Teacher.prototype的构造函数属性现在等于Person(),因为我们刚刚将Teacher.prototype设置为引用从Person.prototype继承其属性的对象!在控制台中输入Teacher.prototype.constructor进行验证。

  2. 这样是有问题的,可以添加以下行来纠正这一点:

Object.defineProperty(Teacher.prototype, 'constructor', {
    value: Teacher,
    enumerable: false, // 这样它就不会出现在“for in”循环中
    writable: true 
});

现在输入Teacher.prototype.constructor将会返回Teacher(),并且继承了Person

6、给 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 + '.');
};

这样就会出现老师打招呼的弹窗,会根据条件判断性别从而使用正确的称呼打招呼。

7、验证我们的代码

// 创建一个 Teacher() 对象实例
var teacher1 = new Teacher('Dave', 'Griffiths', 31, 'male', ['football', 'cookery'], 'mathematics');

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

前面三个进入到从Person()的构造器 继承的属性和方法,后面两个则只有Teacher()的构造器才有的属性和方法。

二、class(ECMAScript 2015)继承

ECMAScript 2015将class语法引入JavaScript,以使用更简单、更干净的语法来编写可重用类,这更类似于C++或java中的类。接下来,我们用class实现PersonTeacher的继承。

1、使用 class 定义 Person 构造函数:

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

  greeting() {
    console.log(`Hi! I'm ${this.name.first}`);
  };

  farewell() {
    console.log(`${this.name.first} has left the building. Bye for now!`);
  };
}

class语句表示我们正在创建一个新类。在这里,我们定义了类的所有功能:

  • constructor() 方法定义表示Person类的构造函数。
  • greeting()farewell() 是类方法。与类关联的任何方法都是在构造函数之后在类内部定义。在本例中,我们使用了模板文本而不是字符串串联,以使代码更易于阅读。

2、使用 new 操作符实例化对象:

let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);
han.greeting();
// Hi! I'm Han

let leia = new Person('Leia', 'Organa', 19, 'female', ['Government']);
leia.farewell();
// Leia has left the building. Bye for now

注意:在后台,类被转换为原型继承模型

上面我们创建了一个类来表示一个人。他们有一系列所有人都具有的特征;接下来,我们将创建专门的教师类,使用现代类语法从Person继承,也就是创建子类。

3、创建子类

为了创建一个子类,我们使用extends关键字告诉JavaScript我们要基于的类:

class Teacher extends Person {
  constructor(subject, grade) {
    this.subject = subject;
    this.grade = grade;
  }
}

但是有一个小陷阱:与传统的构造函数不同,new操作符对new分配的对象进行初始化,而不是为extends关键字定义的类(即子类)自动初始化。因此,运行上述代码将出现错误:

Uncaught ReferenceError: Must call super constructor in derived class before
accessing 'this' or returning from derived constructor

对于子类,对new分配对象的初始化始终依赖于父类构造函数,即要从中扩展的类的构造函数。

这里我们扩展Person类-教师子类是Person类的扩展。所以对于教师来说,这个初始化是由Person构造函数完成的。

4、使用 super() 运算符调用父构造函数

class Teacher extends Person {
  constructor(subject, grade) {
    super(); // 通过调用父构造函数来初始化“this”
    this.subject = subject;
    this.grade = grade;
  }
}

如果子类不继承父类的属性,那么拥有子类是没有意义的。所以,我们让super()操作符也接受父构造函数的参数就好了。

回顾我们的Person构造函数,可以看到它的构造函数方法中有以下代码块:

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

5、super() 传参

由于super()运算符实际上是父类构造函数,因此将父类构造函数的必要参数传递给它也将初始化子类中的父类属性,从而继承它:

class Teacher extends Person {
  constructor(first, last, age, gender, interests, subject, grade) {
    super(first, last, age, gender, interests);

    // subject and grade are specific to Teacher
    this.subject = subject;
    this.grade = grade;
  }
}

6、验证代码

现在,当我们实例化教师对象实例时,可以调用在TeacherPerson上定义的方法和属性:

let snape = new Teacher('Severus', 'Snape', 58, 'male', ['Potions'], 'Dark arts', 5);
snape.greeting(); // Hi! I'm Severus.
snape.farewell(); // Severus has left the building. Bye for now.
snape.age // 58
snape.subject; // Dark arts

就像我们对教师所做的那样,我们可以创建Person的其他子类,使它们更加专业化,而无需修改基类。

7、Getters and Setters

有时,我们可能想要更改所创建类中属性的值,或者不知道属性的最终值是什么。以教师为例,我们可能在创建之前不知道教师将教授什么科目,或者他们的科目可能在学期之间发生变化。可以用gettersetter来处理这种情况。

getterssetters 是一对。getter返回变量的当前值,setter将变量的值更改为它定义的值。

class Teacher extends Person {
  constructor(first, last, age, gender, interests, subject, grade) {
    super(first, last, age, gender, interests);
    // subject and grade 是教师特有的
    this._subject = subject;
    this.grade = grade;
  }

  get subject() {
    return this._subject;
  }

  set subject(newSubject) {
    this._subject = newSubject;
  }
}

在上面的类中,我们有一个subject属性的gettersetter。我们使用_创建一个单独的值来存储属性名。如果不使用此约定,每次调用getset时都会出现错误:

  • 要显示snape对象的_subject属性的当前值,我们可以使用snape.subject
  • 要为_subject属性设置新值,可以使用snap.subject=“new value”
// Check the default value
console.log(snape.subject) // Returns "Dark arts"

// Change the value
snape.subject = "Balloon animals" // Sets _subject to "Balloon animals"

// Check it again and see if it matches the new value
console.log(snape.subject) // Returns "Balloon animals"