类与原型继承的关系
ECMAScript 6 类与原型继承的关系
ECMAScript 6 引入了类的概念,但它本质上仍然是基于原型继承的语法糖。理解这一点对于深入掌握 JavaScript 的面向对象编程至关重要。
原型继承的基本原理
JavaScript 从诞生起就采用原型继承机制。每个对象都有一个内部属性 [[Prototype]]
(可通过 __proto__
访问),指向它的原型对象。当访问对象的属性时,如果对象本身没有该属性,就会沿着原型链向上查找。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person = new Person('Alice');
person.sayHello(); // 输出: Hello, my name is Alice
在这个例子中,Person
是一个构造函数,Person.prototype
是所有实例共享的原型对象。当调用 new Person()
时,新创建的对象的 [[Prototype]]
会指向 Person.prototype
。
ES6 类的本质
ES6 的 class
语法并没有引入新的继承模型,而是对原型继承的语法封装。类声明实际上创建了一个特殊的函数。
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
console.log(typeof Person); // 输出: "function"
可以看到,Person
的类型仍然是函数。类的方法实际上被添加到了构造函数的原型上:
console.log(Person.prototype.sayHello); // 输出: [Function: sayHello]
类继承与原型链
ES6 的 extends
关键字实现了基于原型的继承链。子类的 [[Prototype]]
指向父类,子类实例的原型指向子类的 prototype
,而子类 prototype
的原型又指向父类的 prototype
。
class Student extends Person {
constructor(name, grade) {
super(name);
this.grade = grade;
}
study() {
console.log(`${this.name} is studying in grade ${this.grade}`);
}
}
const student = new Student('Bob', 3);
student.sayHello(); // 继承自Person
student.study(); // Student自身的方法
// 原型链关系
console.log(Student.__proto__ === Person); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true
console.log(student.__proto__ === Student.prototype); // true
静态方法与原型
类中的静态方法不会被实例继承,而是直接作为构造函数的属性存在。
class MathUtils {
static add(a, b) {
return a + b;
}
}
console.log(MathUtils.add(1, 2)); // 3
const utils = new MathUtils();
console.log(utils.add); // undefined
这相当于在 ES5 中直接给构造函数添加属性:
function MathUtils() {}
MathUtils.add = function(a, b) { return a + b; };
类字段与原型
ES2022 引入了类字段语法,包括实例字段和静态字段。这些字段最终也会被转换为基于原型的实现。
class Counter {
count = 0; // 实例字段
static incrementStep = 1; // 静态字段
increment() {
this.count += Counter.incrementStep;
}
}
// 相当于
function Counter() {
this.count = 0;
}
Counter.incrementStep = 1;
Counter.prototype.increment = function() {
this.count += Counter.incrementStep;
};
原型方法的不可枚举性
类中定义的方法默认是不可枚举的,这与 ES5 中直接在原型上添加方法的行为不同。
class MyClass {
method() {}
}
console.log(Object.keys(MyClass.prototype)); // []
// ES5 等效代码
function MyClass() {}
MyClass.prototype.method = function() {};
console.log(Object.keys(MyClass.prototype)); // ['method']
super 关键字的实现
super
关键字在类中用于访问父类的属性和方法,它的行为依赖于原型链。
class Parent {
constructor() {
this.name = 'Parent';
}
}
class Child extends Parent {
constructor() {
super();
console.log(this.name); // 'Parent'
}
method() {
super.method(); // 调用Parent.prototype.method
}
}
super
在构造函数中相当于 Parent.call(this)
,在方法中相当于 Parent.prototype.method.call(this)
。
类与原型继承的性能考虑
由于类本质上是原型继承的语法糖,它们的性能特征相同。原型链查找会影响性能,特别是在深层继承结构中。
class A { method() {} }
class B extends A {}
class C extends B {}
// ...多级继承
const c = new C();
c.method(); // 需要沿着原型链查找
对于性能敏感的代码,可以考虑使用组合而非继承,或者扁平化原型链。
原型链与 instanceof 运算符
instanceof
运算符通过检查对象的原型链来确定对象是否是某个构造函数的实例。
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
这实际上检查的是 Dog.prototype
是否出现在 dog
的原型链上。
类与原型继承的局限性
虽然类语法更清晰,但它仍然受到 JavaScript 原型模型的限制:
- 无法直接访问
[[Prototype]]
(虽然可以通过__proto__
或Object.getPrototypeOf()
) - 多重继承难以实现
- 修改原型会影响所有实例
class MyClass {
method() { console.log('original'); }
}
const obj1 = new MyClass();
MyClass.prototype.method = () => console.log('modified');
obj1.method(); // 输出: modified
类与工厂函数的对比
类不是创建对象的唯一方式。工厂函数也可以创建对象,有时更灵活。
// 类方式
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
// 工厂函数方式
function createPoint(x, y) {
const obj = Object.create(pointMethods);
obj.x = x;
obj.y = y;
return obj;
}
const pointMethods = {
distance() {
return Math.sqrt(this.x**2 + this.y**2);
}
};
工厂函数可以完全控制对象的创建过程,包括是否使用原型、是否暴露构造函数等。
类私有字段与原型
ES2022 引入了真正的私有字段语法,使用 #
前缀。这些字段不会出现在原型链上。
class Counter {
#count = 0; // 私有字段
increment() {
this.#count++;
}
get count() {
return this.#count;
}
}
const counter = new Counter();
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 1
console.log(counter.#count); // 语法错误
私有字段是每个实例独有的,不会被继承,也不会出现在原型链上。
类与原型继承的调试
理解类与原型的关系有助于调试。例如,在 Chrome DevTools 中查看对象时,可以看到原型链关系。
class Parent { parentMethod() {} }
class Child extends Parent { childMethod() {} }
const child = new Child();
// 在控制台输入 child 并展开查看 __proto__ 链
原型链会显示为:child -> Child.prototype -> Parent.prototype -> Object.prototype
。
类与原型继承的 polyfill
由于类是语法糖,它们可以被转换为 ES5 代码以实现向后兼容。Babel 等转译器就是这样工作的。
// ES6
class Example {
constructor(value) {
this.value = value;
}
method() {}
}
// 转换为ES5
function Example(value) {
this.value = value;
}
Example.prototype.method = function() {};
这种转换展示了类与原型继承的直接对应关系。
原型继承的灵活性与类
虽然类提供了更清晰的语法,但原型继承在某些情况下更灵活。例如,可以动态修改原型:
class MyClass {
method() { console.log('original'); }
}
const obj = new MyClass();
// 动态修改原型方法
MyClass.prototype.method = function() { console.log('modified'); };
obj.method(); // 输出: modified
这种灵活性在某些高级模式中很有用,但也可能导致难以维护的代码。
类与原型继承的内存效率
由于类方法存储在原型上,它们被所有实例共享,这比每个实例都有自己的方法副本更节省内存。
class LargeClass {
method1() {}
method2() {}
// ...很多方法
}
// 创建多个实例
const instances = [];
for (let i = 0; i < 1000; i++) {
instances.push(new LargeClass());
}
// 所有实例共享相同的方法
相比之下,如果在构造函数中定义方法,每个实例都会有自己的一套方法副本,占用更多内存。
原型继承的历史背景
JavaScript 的原型继承模型与传统的类继承模型不同,这是由语言最初的设计目标决定的。Brendan Eich 在 10 天内创建了 JavaScript 的原型,受到了 Self 语言的影响。
理解这一历史背景有助于理解为什么 ES6 类被设计为原型继承的语法糖,而不是完全引入新的继承模型。
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:私有字段提案(#前缀)
下一篇:类中this的指向问题