前言
继承是面向对象编程思想中很重要的一个概念,它与多态、封装共为面向对象的三个基本特征。继承不但帮助我们拓展了代码的可解释性,还能提升代码的可复用性。A 对象通过继承 B 对象,就能直接拥有 B 对象的属性和方法,这就是继承。大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。而 JavaScript 是一门基于原型链的语言,JavaScript 的继承就是通过原型链实现的(ES6 才引入了 class 语法)。
构造函数、原型属性与实例对象
如果想搞清如何在 JavaScript 中实现继承,我们首先要搞懂构造函数、原型属性与实例对象三者之间的关系,首先先看一段代码:
function Person(name, age) {
var gender = girl; // (1)
this.name = name; // (1)
this.age = age; // (1)
}
// (2)
Person.prototype.sayName = function() {
alert(this.name);
};
// (3)
var Tom = new Person('Tom', 20);
kitty.sayName(); // kitty
function Person(name, age) {
var gender = girl; // (1)
this.name = name; // (1)
this.age = age; // (1)
}
// (2)
Person.prototype.sayName = function() {
alert(this.name);
};
// (3)
var Tom = new Person('Tom', 20);
kitty.sayName(); // kitty
这段代码中有几个概念需要注意:
Person
是一个构造函数,它是一个函数,用来“构造”对象。(1)处的gender
是该构造函数的私有属性。- (2)处的
prototype
是Person
的原型对象,它是一个对象,并且是实例对象的“原型”,同时也是构造函数的“属性”,所以也有人称它为原型属性。原型对象上定义的所有属性(和方法)都会被实例对象所“继承”。 - (3)处声明的变量
Tom
的值是构造函数Person
的实例对象,它是由构造函数生成的一个实例,同时它也是一个对象。实例对象可以访问到两种属性,一种是通过构造函数生成的自有属性,一种是原型对象可以访问的所有属性。
你是否觉得奇怪,为什么我们的实例对象可以访问到构造函数原型对象上的属性?这是因为每个对象自身都拥有一个隐式的 __proto__
属性(私有属性),该属性默认指向其构造函数的原型对象。该原型对象也有一个自己的原型对象,层层向上直到一个对象的原型对象为 null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
当 JavaScript 引擎发现一个对象访问一个属性时,会首先查找对象的自有属性,如果没有找到,则会在 __proto__
属性指向的原型对象中继续查找。如果在原型对象中也没有找到的话,由于原型对象也是一个对象,所以会查找原型对象的 __proto__
属性指向的对象,也就是原型对象的原型对象。如果一直没有找到该属性,JavaScript 会一直这样找下去,直到找到最顶部构造函数 Object
的 prototype
原型对象,如果还没有找到,则会返回 undefined
。
继承的几种方式
JavaScript 实现继承的方式不止一种。这是因为 JavaScript 中的继承机制并不是明确规定的,而是通过模仿实现的。这意味着所有的继承细节并非完全由解释程序处理。这里总结几种继承的方式,具体使用哪一种还需要根据使用场景考虑。
Object.create() 组合继承
ECMAScript5(ES5)中引入了一个方法:Object.create()
。可以调用这个方法来基于已有的对象创建新对象。
function Person(name, age) {
(this.name = name), (this.age = age);
}
Person.prototype.setAge = function() {
console.log('111');
};
function Student(name, age, price) {
Person.call(this, name, age);
this.price = price;
this.setScore = function() {};
}
Student.prototype = Object.create(Person.prototype); // 核心代码
Student.prototype.constructor = Student; // 核心代码
var s1 = new Student('Tom', 20, 15000);
console.log(s1 instanceof Student, s1 instanceof Person); // true true
console.log(s1.constructor); // Student
console.log(s1);
function Person(name, age) {
(this.name = name), (this.age = age);
}
Person.prototype.setAge = function() {
console.log('111');
};
function Student(name, age, price) {
Person.call(this, name, age);
this.price = price;
this.setScore = function() {};
}
Student.prototype = Object.create(Person.prototype); // 核心代码
Student.prototype.constructor = Student; // 核心代码
var s1 = new Student('Tom', 20, 15000);
console.log(s1 instanceof Student, s1 instanceof Person); // true true
console.log(s1.constructor); // Student
console.log(s1);
Student 继承了所有的 Person 原型对象的属性和方法:

寄生组合继承
寄生组合式继承算是 ES6 之前,最常用来做继承的方案,其除了语法不太合理外,没有其他不足之处。
// 定义父类型
function Person(name){
this.name = name;
}
Person.prototype.sayHello = function(){
console.log('I am', this.name);
}
// 封装继承方法
function inherit(subType, superType) {
// 申明一个类用于组合
// 在组合类实例化的时候将构造函数指向子类
function InheritFn() {
this.constructor = subType
}
// 组合类的原型指向父类原型
InheritFn.prototype = superType.prototype;
// 将子类的原型指向组合类的一个副本
subType.prototype = new InheritFn();
}
// 定义子类类型
function Son(name, age){
Person.call(this, name); // 借用父类构造函数
this.age = age;
}
// 使用继承方法让子类继承父类的原型
// 注意:要执行该动作后才能在子类的原型上定义方法,否则没用
inherit(Son, Person);
// 定义子类方法
Son.prototype.sayAge = function(){
console.log(this.age)
}
// 定义父类型
function Person(name){
this.name = name;
}
Person.prototype.sayHello = function(){
console.log('I am', this.name);
}
// 封装继承方法
function inherit(subType, superType) {
// 申明一个类用于组合
// 在组合类实例化的时候将构造函数指向子类
function InheritFn() {
this.constructor = subType
}
// 组合类的原型指向父类原型
InheritFn.prototype = superType.prototype;
// 将子类的原型指向组合类的一个副本
subType.prototype = new InheritFn();
}
// 定义子类类型
function Son(name, age){
Person.call(this, name); // 借用父类构造函数
this.age = age;
}
// 使用继承方法让子类继承父类的原型
// 注意:要执行该动作后才能在子类的原型上定义方法,否则没用
inherit(Son, Person);
// 定义子类方法
Son.prototype.sayAge = function(){
console.log(this.age)
}
class 的继承
ECMAScript6(ES6)引入了一套新的关键字来实现 class。这些新的关键字包括 class
、constructor
、static
、extends
和 super
。class
可以通过 extends
关键字实现继承,还可以通过 static
关键字定义类的静态方法,这比 ES5 通过修改原型链实现继承要清晰和方便很多。使用过基于类语言的开发人员对这些关键字和结构会感到熟悉,但需要注意的是,class
关键字只是原型的语法糖,JavaScript 继承仍然是基于原型实现的。
class Person {
// 调用类的构造方法
constructor(name, age) {
this.name = name;
this.age = age;
}
// 定义一般的方法
showName() {
console.log('调用父类的方法');
console.log(this.name, this.age);
}
}
let p1 = new Person('kobe', 39);
console.log(p1);
// 定义一个子类
class Student extends Person {
constructor(name, age, salary) {
super(name, age); // 通过super调用父类的构造方法
this.salary = salary;
}
showName() {
// 在子类自身定义方法
console.log('调用子类的方法');
console.log(this.name, this.age, this.salary);
}
}
let s1 = new Student('wade', 38, 1000000000);
console.log(s1);
class Person {
// 调用类的构造方法
constructor(name, age) {
this.name = name;
this.age = age;
}
// 定义一般的方法
showName() {
console.log('调用父类的方法');
console.log(this.name, this.age);
}
}
let p1 = new Person('kobe', 39);
console.log(p1);
// 定义一个子类
class Student extends Person {
constructor(name, age, salary) {
super(name, age); // 通过super调用父类的构造方法
this.salary = salary;
}
showName() {
// 在子类自身定义方法
console.log('调用子类的方法');
console.log(this.name, this.age, this.salary);
}
}
let s1 = new Student('wade', 38, 1000000000);
console.log(s1);
Person 的打印如下:

Student 的打印如下:
