在 JavaScript 中,class 和 prototype 是用来实现对象继承的核心机制。class 是 ES6 引入的语法糖,本质上是对传统基于原型继承的简化和封装,背后仍然依赖于 prototype 原型链机制。理解 class 和 prototype 的底层机制,有助于更好地掌握 JavaScript 中的继承与面向对象编程。
1. prototype 的底层机制
在 JavaScript 中,每个函数(包括构造函数)都默认拥有一个 prototype 属性,它指向一个对象,这个对象包含了所有通过这个构造函数创建的实例可以共享的方法和属性。
• 当创建一个对象(通过构造函数或者 Object.create),这个对象会隐式地拥有一个内部属性 [[Prototype]],通常称作 __proto__,它指向构造函数的 prototype 对象。
• 当访问对象的属性或方法时,JavaScript 会首先在这个对象自身的属性中查找;如果没有找到,则会沿着 [[Prototype]] (即 __proto__) 指向的原型链向上查找,直到找到该属性或方法,或者直到原型链的尽头 null。
示例:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
const person1 = new Person('lisi');
console.log(person1.getName()); // "lisi"
解释:
- Person 是一个构造函数,Person.prototype 是一个对象,存放共有的方法 getName。
- 当我们通过 new Person('lisi') 创建 person1 对象时,person1.__proto__ 指向 Person.prototype。
- 当调用 person1.getName() 时,JavaScript 引擎会先在 person1 自身找 getName,找不到时就会在 Person.prototype 上查找并找到这个方法。
原型链:
person1 的 __proto__ 指向 Person.prototype,Person.prototype.__proto__ 指向 Object.prototype,而 Object.prototype.__proto__ 指向 null。
2. class 的底层机制
class 是 ES6 引入的一种用于创建对象的更简洁的语法,它提供了更加面向对象的语法形式,但其背后的实现仍然依赖于 prototype 和原型链。
示例:
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
const person1 = new Person('lisi');
console.log(person1.getName()); // "lisi"
解释:
- class 定义了一个构造函数 Person,相当于之前的 function Person(name) { this.name = name; } 。
- getName() 被定义在 Person.prototype 上,和传统的构造函数一样。
- 当创建 person1 实例时,person1.proto 指向 Person.prototype,继承了 getName 方法。
本质:
// 下面两段代码在底层的执行结果是一样的
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
// 等价于
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
3. class 和 prototype 的关系
在 ES6 之前,通过构造函数与 prototype 来实现继承,class 只是对这种机制的进一步封装。它主要在语法上提供了更直观的面向对象编程模式,但它的底层依然依赖 prototype。
主要区别:
- 语法糖:class 是语法糖,它简化了传统构造函数和原型方法的定义方式,使代码更清晰、更类似于其他面向对象编程语言。
- 严格模式:所有在 class 语法中定义的代码都在严格模式下运行,比如不能隐式地创建全局变量。
- 不可枚举的类方法:通过 class 定义的方法是不可枚举的,它们不会出现在 for...in 循环中。这和手动将方法挂在 prototype 上有所不同。
4. class 的继承
ES6 提供了更简单的继承语法,通过 extends 关键字实现继承,并且在子类构造函数中可以使用 super 调用父类的构造函数。
示例:
class Parent {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的构造函数
this.age = age;
}
getAge() {
return this.age;
}
}
const child1 = new Child('lisi', 18);
console.log(child1.getName()); // "lisi"
console.log(child1.getAge()); // 18
解释:
- Child 通过 extends Parent 实现继承,Child.prototype.__proto__ 指向 Parent.prototype。
- 在 Child 的构造函数中,必须使用 super(name) 调用父类构造函数,这相当于 Parent.call(this, name)。
5. 原型链中的继承关系
无论是 class 继承还是传统的构造函数继承,底层的继承关系都通过原型链来实现。
- 子类的 __proto__ 指向父类的构造函数,确保子类可以访问父类的静态方法。
- 子类的 prototype.proto 指向父类的 prototype,确保子类实例可以访问父类的实例方法。
console.log(Child.__proto__ === Parent); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true