6.6 原型与继承:构造函数、prototype、原型链、Object.create、class、私有字段
构造函数、prototype、原型链、Object.create、class、私有字段
原理
JavaScript 的继承模型基于原型(Prototype),而非传统的类式继承。这一设计选择深受 Self 语言影响。ES6 引入的 class 关键字并非引入了新的继承模型,而是原型继承的语法糖,其底层机制与构造函数 + prototype 完全一致。
构造函数与 prototype
在 JavaScript 中,每个函数(除箭头函数外)在创建时都会自动获得一个 prototype 属性,该属性指向一个包含 constructor 属性的对象。constructor 指回函数本身。
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, ${this.name}`;
};
const alice = new Person('Alice');
alice.greet(); // "Hello, Alice"
当使用 new 调用构造函数时,引擎执行以下操作:
- 创建新对象
obj。 obj.[[Prototype]](在现代浏览器中可通过__proto__访问,标准方式为Object.getPrototypeOf)设置为构造函数的prototype。- 构造函数内部的
this绑定到obj。 - 执行构造函数体。
- 若构造函数未返回对象,则返回
obj。
原型链(Prototype Chain)
原型链是 JavaScript 实现继承的核心机制。当访问对象的属性或方法时,引擎首先查找对象自身,若未找到则沿 [[Prototype]] 向上查找,直到找到该属性或到达原型链顶端(Object.prototype,其 [[Prototype]] 为 null)。
alice → Person.prototype → Object.prototype → null
instanceof 操作符正是基于原型链工作:alice instanceof Person 检查 Person.prototype 是否存在于 alice 的原型链中。
Object.create
Object.create(proto) 以指定对象为原型创建新对象,是原型继承最纯粹的形式:
const animal = {
speak() {
return 'sound';
}
};
const dog = Object.create(animal);
dog.speak = function() {
return 'woof';
};
Object.create(null) 创建没有原型的对象,适合作为字典(Map 的轻量替代),避免 toString 等原型属性的干扰。
ES6 Class 的语法糖本质
ES6 的 class 语法在语义上几乎完全映射到原型机制:
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
static species() {
return 'Homo sapiens';
}
}
等价于(近似):
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, ${this.name}`;
};
Person.species = function() {
return 'Homo sapiens';
};
关键差异在于 class 语法下:
- 构造函数必须通过
new调用,否则抛出 TypeError。 - 类声明不会被提升(存在 TDZ)。
- 类体默认处于严格模式。
- 类方法不可枚举(
enumerable: false)。
继承与 super
extends 关键字建立了原型链关联,并自动处理 prototype 与 [[Prototype]] 的双向链接:
class Employee extends Person {
constructor(name, role) {
super(name); // 必须在使用 this 之前调用
this.role = role;
}
greet() {
return `${super.greet()} [${this.role}]`;
}
}
super 在构造函数中指向父类的构造函数(即 Person),在方法中通过 super.method() 调用时,引擎内部使用 Reflect.get(homeObject.[[Prototype]], 'method', this) 确保 this 绑定正确。
私有字段(ES2022)
ES2022 引入了真正的私有字段,以 # 为前缀:
class BankAccount {
#balance = 0;
deposit(amount) {
this.#balance += amount;
}
get #formatted() {
return `$${this.#balance.toFixed(2)}`;
}
}
私有字段是真正的语言级私有,不在原型上,无法通过 obj['#balance']、Reflect.ownKeys 或 Proxy 拦截访问。尝试从类外部访问会报语法错误(编译时)或 TypeError(运行时)。私有字段在每个实例中独立存储,不会共享。
私有字段与闭包私有
在私有字段之前,开发者使用 WeakMap 在模块级别模拟私有属性:
const _balance = new WeakMap();
class BankAccount {
constructor() { _balance.set(this, 0); }
}
私有字段的优势在于语法简洁、引擎可优化访问路径,且不需要额外的模块级闭包。
用法
// 原型继承的经典模式(ES5)
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
function Dog(name, breed) {
Animal.call(this, name); // 继承实例属性
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复 constructor 指向
Dog.prototype.speak = function() {
return `${this.name} barks`;
};
// 混入模式(Mixin)
const Flyable = (Base) => class extends Base {
fly() {
return `${this.name} is flying`;
}
};
class FlyingDog extends Flyable(Dog) {}
实践
避免原型污染
原型污染(Prototype Pollution)是一种安全漏洞,攻击者通过修改 Object.prototype 影响所有对象:
// 危险:合并对象时未过滤 __proto__
function merge(target, source) {
for (const key in source) {
target[key] = source[key];
}
}
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
// 所有对象的 isAdmin 都变成了 true
防御措施:
- 使用
Object.create(null)作为数据容器。 - 合并对象时过滤
__proto__、constructor、prototype键。 - 使用
Object.freeze(Object.prototype)冻结原型(Node.js 18+ 支持--disable-proto)。 - 优先使用
Map代替对象存储不可信键的数据。
类字段与性能
类字段(field = value)在语义上等价于在构造函数中赋值。但 Babel 转译后的代码可能将字段定义放在构造函数末尾,若子类构造函数中在 super() 之前访问字段会报错。原生实现中,实例字段在 super() 返回后、构造函数余下代码执行前初始化。
继承内置类型的限制
在 ES5 中继承 Array、Error 等内置类型存在问题,因为引擎对这些类型有特殊内部槽(如 [[DefineOwnProperty]])。ES6 的 class extends Array 通过 Symbol.species 等机制解决了大部分问题,但某些老旧转译目标仍可能行为异常。
陷阱
| 陷阱 | 现象 | 解决方案 |
|------|------|---------|
| 忘记调用 super() | 派生类构造函数中访问 this 前未调用 super,抛出 ReferenceError | 始终将 super() 作为派生类构造函数的第一条语句 |
| 原型链断裂 | 手动设置 Dog.prototype = Object.create(Animal.prototype) 后 constructor 丢失 | 显式修复 Dog.prototype.constructor = Dog |
| 方法中的 this | 提取原型方法后 this 丢失 | 使用箭头函数类字段,或在调用时 bind |
| 私有字段的 TDZ | 在声明前访问 #field 会报错 | 确保字段声明在访问之前(类字段按书写顺序初始化) |
| 原型污染 | 不可信数据修改了 Object.prototype | 冻结原型、使用 Map、过滤危险键 |