WinddSnow

JavaScript-Objects-and-Prototypes-Part2-Prototype-Chain-Inheritance-Class

字数统计: 3.9k阅读时长: 16 min
2026/07/30

第51课:对象与原型(下)——原型链、prototype__proto__、继承与 class 语法糖

在 JavaScript 中,继承不是通过“类”实现的,而是通过原型(Prototype) 实现的。每个对象都有一个内部属性 [[Prototype]],指向另一个对象,这个被指向的对象就是原型。当试图访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript 就会沿着原型链向上查找,直到找到该属性或到达原型链的终点 null。这套机制是所有现代框架和语言特性的底层基础。本节课将系统讲解原型链的构建方式、构造函数与 prototype 的关系、ES5 继承模式以及 ES6 class 语法的本质。


1. 构造函数、prototype__proto__ 的关系

1.1 构造函数与 new 操作符

在 JavaScript 中,任何普通函数都可以作为构造函数——只需在调用时加上 new 关键字。使用 new 调用函数时,引擎会执行以下步骤:

  1. 创建一个新的空对象
  2. 将该空对象的 [[Prototype]] 链接到构造函数的 prototype 属性所指的对象。
  3. 将构造函数的 this 绑定到新对象上,执行函数体。
  4. 如果函数没有显式返回对象,则返回这个新对象。
1
2
3
4
5
6
function Person(name) {
this.name = name;
}

const alice = new Person('Alice');
console.log(alice.name); // 'Alice'

new Person('Alice') 执行后,alice 对象的内部 [[Prototype]] 指向 Person.prototype

1.2 prototype 属性(属于函数)

每个函数(箭头函数除外)都有一个 prototype 属性,它是一个普通对象,包含一个 constructor 属性指回函数自身。这个 prototype 对象就是通过 new 创建的实例的原型。

1
2
3
4
5
6
function Person(name) {
this.name = name;
}

console.log(typeof Person.prototype); // 'object'
console.log(Person.prototype.constructor === Person); // true

prototype 属性只存在于函数对象上,且只有函数作为构造函数时其 prototype 才有意义。

1.3 __proto__[[Prototype]] 的访问器)

每个普通对象都有一个内部属性 [[Prototype]],现代浏览器通过 __proto__ 访问器暴露它(ES6 将其标准化为遗留特性)。更推荐使用 Object.getPrototypeOf(obj)Object.setPrototypeOf(obj, proto)

1
2
3
4
5
const alice = new Person('Alice');

// 访问实例的原型
console.log(alice.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true

1.4 关系总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');

// 构造函数 → 原型对象
console.log(Person.prototype); // Person 的原型对象

// 原型对象 → 构造函数
console.log(Person.prototype.constructor === Person); // true

// 实例 → 原型对象
console.log(alice.__proto__ === Person.prototype); // true

// 实例 → 构造函数(通过原型链)
console.log(alice.constructor === Person); // true(因为 alice.__proto__.constructor === Person)

关系图

1
2
3
4
5
6
7
8
9
Person (构造函数)
|
| .prototype

Person.prototype (原型对象)
↑ |
| .__proto__ | .constructor
| ↓
alice (实例) Person

2. 原型链查找机制

当访问 obj.prop 时,引擎按照以下顺序查找:

  1. 检查 obj 自身是否有名为 prop自有属性hasOwnProperty('prop') 返回 true)。
  2. 如果没有,检查 obj.__proto__(即 Object.getPrototypeOf(obj))。
  3. 如果还没有,继续沿着 __proto__ 向上查找,直到找到该属性或 __proto__null
  4. 若直到 null 都未找到,则返回 undefined
1
2
3
4
5
6
7
8
9
10
const grandparent = { family: 'Smith' };
const parent = Object.create(grandparent);
parent.job = 'Engineer';
const child = Object.create(parent);
child.name = 'Alice';

console.log(child.name); // 'Alice' —— 自有属性
console.log(child.job); // 'Engineer' —— 来自 parent
console.log(child.family); // 'Smith' —— 来自 grandparent
console.log(child.age); // undefined —— 整个原型链都没有

**原型链的终点是 null**。默认情况下,所有普通对象的原型链最终都会指向 Object.prototype,而 Object.prototype.__proto__ === null

1
console.log(Object.prototype.__proto__); // null

3. Object.create() —— 以指定原型创建对象

Object.create(proto, propertiesObject?) 创建一个新对象,并将该对象的 [[Prototype]] 设置为 proto。这是比构造函数更纯粹的原型继承方式。

1
2
3
4
5
6
7
8
9
10
11
const user = {
greet() {
return `Hello, I'm ${this.name}`;
}
};

const alice = Object.create(user);
alice.name = 'Alice';

console.log(alice.greet()); // 'Hello, I'm Alice'
console.log(alice.__proto__ === user); // true

与构造函数模式相比,Object.create 不需要定义构造函数,直接指定原型对象。它常被用于实现原型式继承行为委托

3.1 使用 Object.create(null) 创建无原型对象

1
2
3
4
5
const dict = Object.create(null);
dict.apple = '苹果';
console.log(dict.toString); // undefined —— 没有原型,无 toString 方法
console.log('apple' in dict); // true
console.log(Object.prototype.hasOwnProperty.call(dict, 'apple')); // true

这种“纯净”对象适合用作哈希表,不受原型属性污染(如 constructortoString 等)。


4. ES5 继承模式

class 语法出现之前,开发者使用原型实现继承。主要有以下几种模式。

4.1 原型链继承

直接将父类实例赋值给子类原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};

function Dog(name, breed) {
this.name = name;
this.breed = breed;
}
Dog.prototype = new Animal(); // 继承 Animal
Dog.prototype.constructor = Dog; // 修复 constructor 指向

Dog.prototype.bark = function() {
console.log('Woof!');
};

const buddy = new Dog('Buddy', 'Golden');
buddy.eat(); // 'Buddy is eating.'(来自 Animal.prototype)
console.log(buddy instanceof Dog); // true
console.log(buddy instanceof Animal); // true

缺陷

  • 父类构造函数中的引用类型属性会被所有子类实例共享。
  • 无法向父类构造函数传递参数(Dog.prototype = new Animal() 时没有参数)。

4.2 组合继承(伪经典继承)

结合构造函数窃取和原型链继承,解决上述两个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};

function Dog(name, breed) {
Animal.call(this, name); // 第二次调用 Animal
this.breed = breed;
}
Dog.prototype = new Animal(); // 第一次调用 Animal
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log('Woof!');
};

缺陷:父类构造函数被调用了两次(一次原型继承,一次构造窃取),导致性能浪费和原型上存在多余的属性。

4.3 寄生组合继承(最理想的 ES5 继承)

使用 Object.create 来避免重复调用父类构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};

function Dog(name, breed) {
Animal.call(this, name); // 仅调用一次 Animal
this.breed = breed;
}
// 创建一个以 Animal.prototype 为原型的空对象,作为 Dog.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复 constructor

Dog.prototype.bark = function() {
console.log('Woof!');
};

const buddy = new Dog('Buddy', 'Golden');
console.log(buddy.name); // 'Buddy'
buddy.eat(); // 'Buddy is eating.'
console.log(buddy instanceof Animal); // true
console.log(buddy instanceof Dog); // true

这是 ES5 下最优雅的继承方式,也是 Babel 转译 ES6 class extends 所采用的模式。


5. ES6 class 语法:原型继承的语法糖

ES6 引入了 classconstructorextendssuper 关键字。它们并没有改变 JavaScript 基于原型的本质,只是提供了一种更清晰、更接近传统 OOP 的书写方式。

5.1 类声明与构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
constructor(name) {
this.name = name;
}

eat() {
console.log(`${this.name} is eating.`);
}
}

const a = new Animal('Leo');
a.eat(); // 'Leo is eating.'
console.log(typeof Animal); // 'function' —— 类本质上是函数
console.log(Animal.prototype.constructor === Animal); // true

底层真相class 定义的 eat 方法存在于 Animal.prototype 上,是不可枚举的(enumerable: false),这与传统原型方法不同。constructor 是构造函数本身。

1
2
const descriptor = Object.getOwnPropertyDescriptor(Animal.prototype, 'eat');
console.log(descriptor.enumerable); // false

5.2 extendssuper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Dog extends Animal {
constructor(name, breed) {
super(name); // 必须调用 super() 才能访问 this
this.breed = breed;
}

bark() {
console.log('Woof!');
}

eat() {
super.eat(); // 调用父类方法
console.log('Dog is eating happily.');
}
}

const buddy = new Dog('Buddy', 'Golden');
buddy.eat();
// 'Buddy is eating.'
// 'Dog is eating happily.'

super 的两层含义

  • 作为函数调用 super(args):调用父类的构造函数,等价于 Animal.call(this, args)必须在 this 使用之前调用
  • 作为对象使用 super.method():访问父类原型上的方法。

extends 背后的原型链

1
2
3
4
5
6
7
8
class Animal {}
class Dog extends Animal {}

// 实例原型链:实例 → Dog.prototype → Animal.prototype → Object.prototype → null
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true

// 构造函数的原型链(静态属性和方法的继承):
console.log(Object.getPrototypeOf(Dog) === Animal); // true

这是 class extends 与 ES5 寄生组合继承的主要区别——class 同时建立了实例原型链构造函数原型链(静态方法继承),而 ES5 继承只关注实例原型链。

5.3 静态方法与静态属性

1
2
3
4
5
6
7
8
9
10
class MathUtils {
static PI = 3.14159; // ES2022 静态属性

static add(a, b) {
return a + b;
}
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(2, 3)); // 5

静态方法和属性直接定义在类(构造函数)自身上,而不在原型上。实例无法访问。

5.4 私有字段(ES2022)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Counter {
#count = 0; // 私有字段

increment() {
this.#count++;
}

get value() {
return this.#count;
}
}

const c = new Counter();
c.increment();
console.log(c.value); // 1
// console.log(c.#count); // SyntaxError: Private field '#count' must be declared in an enclosing class

私有字段 # 是真正的硬隐私,无法在类外部访问或通过原型链获取,这与 TypeScript 的 private 关键字(仅在编译时检查)不同。


6. instanceof 运算符的原理

obj instanceof Constructor 检查 Constructor.prototype 是否出现在 obj 的原型链上。

1
2
3
4
5
6
7
8
9
10
11
12
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}

console.log(myInstanceof([], Array)); // true
console.log(myInstanceof([], Object)); // true
console.log(myInstanceof({}, Array)); // false

由于原型链可被动态修改(不推荐),instanceof 的结果在极少数情况下可能不准确。准确判断对象类型通常使用 Object.prototype.toString.call(obj)


7. 最佳实践与常见陷阱

7.1 不要直接操作 __proto__

__proto__ 是遗留特性,现代代码应使用 Object.getPrototypeOfObject.setPrototypeOf。直接修改 [[Prototype]] 会导致严重的性能退化,因为引擎会丢弃所有优化过的内联缓存。

7.2 使用 class 还是原型直接操作?

  • 新项目:推荐使用 class 语法。它清晰、易读、不易出错,且工具链(TypeScript、ESLint)支持更好。
  • 遗留代码维护:必须理解原型链,因为大量老代码使用构造函数和原型模式。
  • 框架/库开发:部分场景下 Object.create 更灵活(如 Vue 3 的响应式系统大量使用 Object.create(null) 创建纯净对象)。

7.3 for...in 会遍历原型链上的可枚举属性

1
2
3
4
5
6
7
8
9
function Person() {}
Person.prototype.species = 'human';

const alice = new Person();
alice.name = 'Alice';

for (const key in alice) {
console.log(key); // 'name', 'species'
}

为了避免意外遍历原型属性,始终配合 hasOwnProperty 过滤:

1
2
3
4
5
for (const key in alice) {
if (alice.hasOwnProperty(key)) {
console.log(key); // 仅 'name'
}
}

或者使用 Object.keys(),它只返回自有可枚举属性。


课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 关于 JavaScript 中 class 的说法,正确的是?
    A. class 是 JavaScript 的全新类型,不同于函数。
    B. class 中定义的方法默认是可枚举的。
    C. class 声明会被提升(类似于函数声明)。
    D. class 本质上是基于原型继承的语法糖。

  2. (单选) 以下代码的输出是?

    1
    2
    3
    4
    5
    6
    const parent = { value: 1 };
    const child = Object.create(parent);
    child.value = 2;
    console.log(child.value);
    delete child.value;
    console.log(child.value);

    A. 2 然后 undefined
    B. 2 然后 1
    C. 1 然后 2
    D. 1 然后 undefined

  3. (填空) 要创建一个没有任何原型([[Prototype]]null)的对象,应使用 Object.create(______)

  4. (多选) 以下哪些是 class extends 相比 ES5 寄生组合继承的额外特性?
    A. 静态方法的继承(Object.getPrototypeOf(Dog) === Animal)。
    B. 构造函数体内必须先调用 super() 才能使用 this
    C. 实例方法的不可枚举性。
    D. 可以继承 ArrayError 等内置对象。

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要使用 class 语法实现一个简单的事件发布-订阅模式(EventEmitter)。要求如下:

  • 使用 class EventEmitter 定义类,内部用私有字段 #events 存储事件名到回调数组的映射。
  • 提供 on(eventName, callback) 方法:注册事件监听器,返回取消订阅的函数(调用该函数可移除监听器)。
  • 提供 emit(eventName, ...args) 方法:触发事件,按注册顺序同步调用监听器。
  • 提供 off(eventName, callback) 方法:移除指定监听器。
  • 提供 once(eventName, callback) 方法:注册一次性监听器,触发一次后自动移除。
  • 在代码末尾写一个简短的使用示例。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定 class 语法、私有字段的使用、以及各方法的行为。

三、面试真题与参考答案

题目(美团前端面试题):

请手写一个 JavaScript 的寄生组合继承,并详细解释它与 ES6 class extends 在原型链建立上的差异。包括 instanceof 的判断逻辑。

参考答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 寄生组合继承(ES5)
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};

function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

原型链差异

  • 寄生组合继承仅建立了实例原型链:dog → Dog.prototype → Animal.prototype → Object.prototype,但 DogAnimal 之间没有原型关联(Object.getPrototypeOf(Dog) === Function.prototype)。
  • class extends 不仅建立实例原型链,还建立构造函数之间的原型链:Object.getPrototypeOf(Dog) === Animal,这意味着静态属性和方法也能被继承。
  • class 方法自动设为不可枚举,class 声明不会提升,且内部默认严格模式。

instanceof 判断逻辑obj instanceof Constructor 检查 Constructor.prototype 是否在 obj 的原型链上。在两种方式中,dog instanceof Animal 都为 true,因为 Animal.prototype 都在 dog 的原型链上。


课后练习答案

一、概念自测答案

  1. D

    • 解析:class 本质上是原型继承的语法糖,底层仍是函数和原型。A 错误,typeof class{} === 'function';B 错误,class 方法默认可枚举为 false;C 错误,class 声明不会提升,与 let/const 行为一致。
  2. B

    • 解析:child.value = 2 在自有属性上设置值,输出 2。delete child.value 删除自有属性后,child.value 通过原型链访问 parent.value,得到 1。
  3. null

    • 解析:Object.create(null) 创建无原型的纯对象,常用于字典或哈希表。
  4. A、B、C、D

    • 解析:A 正确,extends 建立了构造函数的原型链用于静态方法继承;B 正确,super() 是必须的;C 正确,class 方法不可枚举;D 正确,ES6 class extends 可以继承内置对象(ES5 寄生组合继承无法完美继承 Array 等)。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请使用 JavaScript class 语法实现一个事件发布-订阅类 EventEmitter。要求:

  • 使用私有字段 #events#events = new Map())存储事件名到 Set<Function> 的映射。
  • on(eventName, callback):将 callback 加入 #events 对应的 Set,返回一个取消订阅的函数(调用该函数即从 Set 中删除该 callback)。
  • emit(eventName, ...args):获取该事件名的 Set,遍历并调用每个 callback 传入 ...args
  • off(eventName, callback):从对应 Set 中删除 callback。
  • once(eventName, callback):内部创建一个包装函数,该函数在调用后调用 off 移除自身,并调用原 callback。将此包装函数加入 #events
  • 使用 const 声明实例,提供使用示例:创建 emitter,注册事件,触发,取消订阅,再次触发验证。
  • 代码中注释清晰,使用 JSDoc 风格注释类和方法。输出完整代码。”
CATALOG
  1. 1. 第51课:对象与原型(下)——原型链、prototype、__proto__、继承与 class 语法糖
    1. 1.1. 1. 构造函数、prototype 与 __proto__ 的关系
      1. 1.1.1. 1.1 构造函数与 new 操作符
      2. 1.1.2. 1.2 prototype 属性(属于函数)
      3. 1.1.3. 1.3 __proto__([[Prototype]] 的访问器)
      4. 1.1.4. 1.4 关系总结
    2. 1.2. 2. 原型链查找机制
    3. 1.3. 3. Object.create() —— 以指定原型创建对象
      1. 1.3.1. 3.1 使用 Object.create(null) 创建无原型对象
    4. 1.4. 4. ES5 继承模式
      1. 1.4.1. 4.1 原型链继承
      2. 1.4.2. 4.2 组合继承(伪经典继承)
      3. 1.4.3. 4.3 寄生组合继承(最理想的 ES5 继承)
    5. 1.5. 5. ES6 class 语法:原型继承的语法糖
      1. 1.5.1. 5.1 类声明与构造函数
      2. 1.5.2. 5.2 extends 与 super
      3. 1.5.3. 5.3 静态方法与静态属性
      4. 1.5.4. 5.4 私有字段(ES2022)
    6. 1.6. 6. instanceof 运算符的原理
    7. 1.7. 7. 最佳实践与常见陷阱
      1. 1.7.1. 7.1 不要直接操作 __proto__
      2. 1.7.2. 7.2 使用 class 还是原型直接操作?
      3. 1.7.3. 7.3 for...in 会遍历原型链上的可枚举属性
    8. 1.8. 课后练习
      1. 1.8.1. 一、概念自测(选择题 / 填空题)
      2. 1.8.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.8.3. 三、面试真题与参考答案
    9. 1.9. 课后练习答案
      1. 1.9.1. 一、概念自测答案
      2. 1.9.2. 二、AI 编程任务参考答案(提示词示例)