跳到主要内容

原型、继承与类

在 JavaScript 中,原型、继承和类是三个紧密相关又容易混淆的概念。本文将从原型链入手,深入探讨它们之间的关系,并通过大量代码示例加深理解。

原型链

在 JavaScript 中,每个对象都有一个原型对象,通过 __proto__ 指针指向它的原型。而原型对象也可能有自己的原型,由此形成了原型链。

构造函数的原型和函数的原型其实是同一个原型:

function Foo() {}
console.log(Foo.__proto__ === Function.prototype); // true

实例对象的原型由其构造函数的 prototype 属性决定:

console.log(Object.__proto__ === Function.prototype); // true

constructor

constructor 是原型对象上的一个属性,指向与之关联的构造函数。它是创建对象、初始化 class 的特殊方法。

console.log(Function.prototype.constructor === Function); // true
console.log(Object.prototype.constructor === Object); // true

proto

__proto__ 是实例对象的内部属性,指向该对象的原型:

console.log(foo.__proto__ === Foo.prototype); // true

值得注意的是,除了 Function.__proto__ === Function.prototype,其他对象的 __proto__ 都不等于 prototype:

console.log(Object.__proto__ === Object.prototype); // false
console.log(Function.__proto__ === Function.prototype); // true

原型链的顶端

JavaScript 中原型链的顶端是 Object.prototype:

console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

Object.prototype.__proto__null,表示原型链到此终止,不再有更高层级的原型。

继承

原型链继承

原型链继承是 JavaScript 中最基本的继承方式。通过将子类的 prototype 指向父类的实例,子类就可以继承父类的属性和方法。

对于原始值,原型链继承表现良好:

function Super() {
this.a = 1;
}
function Sub() {}
Sub.prototype = new Super();

const sub = new Sub();
console.log(sub.a); // 1
sub.a = 2;
console.log(sub.a); // 2

但对于引用值,原型链继承会导致所有子类实例共享同一个引用,产生意料之外的修改:

function Super() {
this.a = [1, 2, 3];
}
function Sub() {}
Sub.prototype = new Super();

const sub1 = new Sub();
const sub2 = new Sub();
sub1.a.push(4);
console.log(sub1.a); // [1, 2, 3, 4]
console.log(sub2.a); // [1, 2, 3, 4]

借用构造函数继承

为了解决原型链继承的引用值共享问题,可以在子类构造函数中调用父类构造函数,这样每个子类实例都有自己的属性副本:

function Super() {
this.a = [1, 2, 3];
}
function Sub() {
Super.call(this);
}

const sub1 = new Sub();
const sub2 = new Sub();
sub1.a.push(4);
console.log(sub1.a); // [1, 2, 3, 4]
console.log(sub2.a); // [1, 2, 3]

借用构造函数虽然解决了引用共享的问题,但父类原型上的方法无法被继承。

组合继承

组合继承结合了原型链继承和借用构造函数继承,既可以继承父类原型上的属性和方法,又可以避免引用值共享:

function Super() {
this.a = [1, 2, 3];
}
Super.prototype.foo = function () {
console.log('foo');
};

function Sub() {
Super.call(this);
}
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

const sub1 = new Sub();
const sub2 = new Sub();
sub1.a.push(4);
console.log(sub1.a); // [1, 2, 3, 4]
console.log(sub2.a); // [1, 2, 3]
sub1.foo(); // foo

组合继承是 JavaScript 中最常用的继承模式,但它的缺点是父类构造函数会被调用两次。

寄生组合继承

寄生组合继承对组合继承进行了优化,通过 Object.create() 方法避免了两次调用父类构造函数:

function inherit(sub, super) {
sub.prototype = Object.create(super.prototype);
sub.prototype.constructor = sub;
}

function Super() {
this.a = [1, 2, 3];
}
function Sub() {
Super.call(this);
}
inherit(Sub, Super);

const sub1 = new Sub();
const sub2 = new Sub();
sub1.a.push(4);
console.log(sub1.a); // [1, 2, 3, 4]
console.log(sub2.a); // [1, 2, 3]

寄生组合继承被认为是 ES6 class 语法的 polyfill,是目前最理想的继承方式。

class

ES6 引入了 class 关键字,提供了更接近传统面向对象语言的写法,本质上是寄生组合继承的语法糖。

使用 class 可以优雅地定义类、构造函数、继承关系,并且不会有引用值共享的问题:

class Super {
constructor() {
this.a = [1, 2, 3];
}
foo() {
console.log('foo');
}
}

class Sub extends Super {
constructor() {
super();
}
}

const sub1 = new Sub();
const sub2 = new Sub();
sub1.a.push(4);
console.log(sub1.a); // [1, 2, 3, 4]
console.log(sub2.a); // [1, 2, 3]
sub1.foo(); // foo

虽然 class 为 JavaScript 的面向对象编程提供了更友好的语法,但我们仍需深入理解其背后的原型和继承机制。只有这样,才能在复杂场景下灵活运用、游刃有余。