原型、继承与类
在 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 的面向对象编程提供了更友好的语法,但我们仍需深入理解其背后的原型和继承机制。只有这样,才能在复杂场景下灵活运用、游刃有余。