ES6 Class类继承机制深度解析:从extends到原生构造函数

作者:代码拾荒

时间:2026-10-22

浏览:494

本文深入解析ES6中Class类的继承机制,详细阐述extends关键字与super关键字的具体用法及区别。重点对比ES5与ES6在实例this创建顺序上的差异,解析prototype与__proto__两条继承链的原理。此外,文章探讨了继承Object、null等特殊情况,并重点说明ES6如何突破限制实现Array、Error等原生构造函数的正确继承。

文章目录

ES6 Class类继承机制概述

本文通过具体实例,详细解析ES6中JavaScript Class类的继承用法,供读者参考学习。

Class基本继承用法与super关键字

1. 基本用法

Class类之间可通过extends关键字实现继承,相比ES5中通过修改原型链实现继承的方式,这种写法更加清晰且便捷。


class ColorPoint extends Point {}

上述代码定义了一个ColorPoint类,该类利用extends关键字继承了Point类的所有属性和方法。由于未添加额外代码,这两个类完全一致,相当于复制了一个Point类。接下来,我们在ColorPoint内部添加具体代码。


class ColorPoint extends Point {
 constructor(x, y, color) {
  super(x, y); // 调用父类的 constructor(x, y)
  this.color = color;
 }
 toString() {
  return this.color + ' ' + super.toString(); // 调用父类的 toString()
 }
}

在上述代码中,constructor方法和toString方法内均出现了super关键字,它在此处代表父类的构造函数,用于创建父类的this对象。

子类必须在constructor方法中调用super方法,否则创建实例时会报错。原因在于子类自身没有this对象,而是继承自父类的this对象并对其进行加工。若不调用super方法,子类将无法获得this对象。


class Point { /* ... */ }
class ColorPoint extends Point {
 constructor() {}
}
let cp = new ColorPoint(); // ReferenceError

上述代码中,ColorPoint继承了父类Point,但其构造函数未调用super方法,导致创建实例时抛出错误。

ES5的继承机制实质是先创造子类的实例对象this,再将父类的方法添加到this上(即Parent.apply(this))。而ES6的继承机制截然不同,它实质是先创造父类的实例对象this(因此必须先调用super方法),随后再用子类的构造函数修改this。

若子类未定义constructor方法,该方法会被默认添加,代码如下。这意味着无论是否显式定义,任何子类都必然包含constructor方法。


constructor(...args) {
 super(...args);
}

另一个需注意的要点是,在子类的构造函数中,只有在调用super之后,才能使用this关键字,否则将报错。这是因为子类实例的构建基于对父类实例的加工,唯有super方法能返回父类实例。


class Point {
 constructor(x, y) {
  this.x = x;
  this.y = y;
 }
}
class ColorPoint extends Point {
 constructor(x, y, color) {
  this.color = color; // ReferenceError
  super(x, y);
  this.color = color; // 正确
 }
}

上述代码中,若子类的constructor方法在调用super之前使用this关键字,结果会报错;而将其置于super方法之后则是正确的做法。

以下是生成子类实例的代码示例。


let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true

上述代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与ES5的行为完全一致。

类的prototype与__proto__属性及继承链

2. 类的 prototype 属性和 __proto__ 属性

在大多数浏览器的ES5实现中,每个对象都拥有__proto__属性,指向对应构造函数的prototype属性。Class作为构造函数的语法糖,同时具备prototype属性和__proto__属性,因此存在两条继承链。

(1)子类的__proto__属性表示构造函数的继承,始终指向父类。 (2)子类prototype属性的__proto__属性表示方法的继承,始终指向父类的prototype属性。


class A {}
class B extends A {}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上述代码中,子类B的__proto__属性指向父类A,子类B的prototype属性的__proto__属性指向父类A的prototype属性。

产生上述结果的原因是,类的继承按照以下模式实现。


class A {}
class B {}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

《对象的扩展》一章中给出了Object.setPrototypeOf方法的实现。


Object.setPrototypeOf = function(obj, proto) {
 obj.__proto__ = proto;
 return obj;
}

由此便得到了上述结果。


Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;

这两条继承链可这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。


Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

Extends继承目标与特殊情况分析

3. Extends 的继承目标

extends关键字后方可以跟随多种类型的值。


class B extends A {}

上述代码中的A,只要是一个拥有prototype属性的函数,就能被B继承。由于函数均拥有prototype属性(除了Function.prototype函数),因此A可以是任意函数。

接下来,讨论三种特殊情况。

第一种特殊情况:子类继承Object类。


class A extends Object {}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true

在此情况下,A实际上是构造函数Object的复制,A的实例即为Object的实例。

第二种特殊情况:不存在任何继承。


class A {}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

在此情况下,A作为一个基类(即不存在任何继承),是一个普通函数,因此直接继承Funciton.prototype。然而,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。

第三种特殊情况:子类继承null。


class A extends null {}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true

此情况与第二种情况非常相似。A同样是一个普通函数,因此直接继承Funciton.prototype。但是,A调用后返回的对象不继承任何方法,所以其__proto__指向Function.prototype,即实质上执行了以下代码。


class C extends null {
 constructor() {
  return Object.create(null);
 }
}

4. Object.getPrototypeOf()

Object.getPrototypeOf方法可用于从子类上获取父类。


Object.getPrototypeOf(ColorPoint) === Point
// true

因此,可利用此方法判断一个类是否继承了另一个类。

Object.getPrototypeOf与super关键字详解

5. super 关键字

super关键字拥有两种用法,含义各不相同。

(1)作为函数调用时(即super(...args)),super代表父类的构造函数。

(2)作为对象调用时(即super.prop或super.method()),super代表父类。需注意,此时super既可引用父类实例的属性和方法,也可引用父类的静态方法。


class B extends A {
 get m() {
  return this._p * super._p;
 }
 set m() {
  throw new Error(' 该属性只读 ');
 }
}

上述代码中,子类通过super关键字调用父类实例的_p属性。 由于对象总是继承其他对象,因此可在任意一个对象中使用super关键字。


var obj = {
 toString() {
  return "MyObject: " + super.toString();
 }
};
obj.toString(); // MyObject: [object Object]

实例__proto__属性与原型链修改

6. 实例的 __proto__ 属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。换言之,子类的原型的原型,即为父类的原型。


var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

上述代码中,ColorPoint继承了Point,导致前者原型的原型是后者的原型。

因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。


p2.__proto__.__proto__.printName = function() {
 console.log('Ha');
};
p1.printName() // "Ha"

上述代码在ColorPoint的实例p2上向Point类添加方法,结果影响了Point的实例p1。

原生构造函数的继承与ES6改进

原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用于生成数据结构。ECMAScript的原生构造函数大致包括以下这些。


Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

以往,这些原生构造函数是无法继承的,例如,无法自行定义Array的子类。


function MyArray() {
 Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
 constructor: {
  value: MyArray,
  writable: true,
  configurable: true,
  enumerable: true
 }
});

上述代码定义了一个继承Array的MyArray类。然而,该类的行为与Array完全不一致。


var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red"

发生这种情况的原因是,子类无法获取原生构造函数的内部属性,通过Array.apply()或分配给原型对象均无效。原生构造函数会忽略apply方法传入的this,即原生构造函数的this无法绑定,导致无法获取内部属性。

ES5是先新建子类的实例对象this,再将父类的属性添加到子类上。由于父类的内部属性无法获取,导致无法继承原生的构造函数。例如,Array构造函数有一个内部属性[[DefineOwnProperty]],用于在定义新属性时更新length属性。该内部属性无法在子类中获取,导致子类的length属性行为异常。

下面的示例中,我们尝试让一个普通对象继承Error对象。


var e = {};
Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]
Object.getOwnPropertyNames(e)
// []

上述代码中,我们试图通过Error.call(e)这种写法,使普通对象e具有Error对象的实例属性。然而,Error.call()完全忽略传入的第一个参数,而是返回一个新对象,e本身没有任何变化。这证明了Error.call(e)这种写法无法继承原生构造函数。

ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为均可继承。以下是一个继承Array的例子。


class MyArray extends Array {
 constructor(...args) {
  super(...args);
 }
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined

上述代码定义了一个MyArray类,继承了Array构造函数,因此可以从MyArray生成数组的实例。这意味着,ES6可以自定义原生数据结构(如Array、String等)的子类,这是ES5无法做到的。

上述示例还表明,extends关键字不仅可用于继承类,还可用于继承原生的构造函数。因此,可以在原生数据结构基础上定义自己的数据结构。以下便是定义了一个带版本功能的数组。


class VersionedArray extends Array {
 constructor() {
  super();
  this.history = [
   []
  ];
 }
 commit() {
  this.history.push(this.slice());
 }
 revert() {
  this.splice(0, this.length, ...this.history[this.history.length - 1]);
 }
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.revert();
x // [1, 2]

上述代码中,VersionedArray结构会通过commit方法,将当前状态存入history属性;随后通过revert方法,可撤销当前版本,回到上一个版本。此外,VersionedArray依然是一个数组,所有原生数组方法均可在其上调用。

以下是一个自定义Error子类的示例。


class ExtendableError extends Error {
 constructor(message) {
  super();
  this.message = message;
  this.stack = (new Error()).stack;
  this.name = this.constructor.name;
 }
}
class MyError extends ExtendableError {
 constructor(m) {
  super(m);
 }
}
var myerror = new MyError('ll');
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
 // Error
 // at MyError.ExtendableError
 // ...

需注意,继承Object的子类存在一个行为差异。


class NewObj extends Object {
 constructor() {
  super(...arguments);
 }
}
var o = new NewObj({
 attr: true
});
console.log(o.attr === true); // false

上述代码中,NewObj继承了Object,但无法通过super方法向父类Object传参。原因在于ES6改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6规定Object构造函数会忽略参数。

更多相关内容可查看本站专题:《ECMAScript6(ES6)入门教程》、《JavaScript数组操作技巧总结》、《JavaScript字符与字符串操作技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript错误与调试技巧总结》及《javascript面向对象入门教程》

希望本文所述对大家基于ECMAScript的程序设计有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章