原型链与继承

系统讲解 JavaScript 原型链的 [[Prototype]] 查找机制、各类继承模式的优劣,以及 ES6 class 语法糖的本质,帮助你在面试中精准阐述 JS 面向对象模型。

为什么原型链是 JavaScript 的基石

JavaScript 是一门基于原型(prototype-based) 的语言,不同于 Java/C++ 的基于类(class-based)继承。即使 ES2015 引入了 class 关键字,底层仍然是原型链机制。理解原型链,才能真正理解 instanceofObject.createclass extends 的行为。


一、原型基础

1.1 两个关键属性

属性 归属 含义
prototype 函数对象 该构造函数创建的实例的原型对象
[[Prototype]] / __proto__ 所有对象 指向该对象的原型(即继承来源)
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function () {
  return `Hi, I'm ${this.name}`;
};

const alice = new Person('Alice');

// 关系链
alice.__proto__ === Person.prototype;           // true
Person.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null;             // true — 原型链终点

注意__proto__ 是非标准的访问器属性(虽然已被大多数引擎实现)。推荐使用 Object.getPrototypeOf(obj)Object.setPrototypeOf(obj, proto)

1.2 [[Prototype]] 查找机制

当访问对象的属性时,引擎遵循以下流程:

  1. 在对象自身属性(own properties)中查找
  2. 未找到 → 沿 [[Prototype]] 链向上查找
  3. 到达 Object.prototype 仍未找到 → 返回 undefined
alice.greet();    // 在 Person.prototype 上找到
alice.toString(); // 在 Object.prototype 上找到
alice.foo;        // 沿链到 null,返回 undefined

属性遮蔽(Property Shadowing):如果对象自身和原型链上同时存在同名属性,自身属性优先(遮蔽原型上的)。

alice.greet = function () { return 'Override!'; };
alice.greet(); // 'Override!' — 自身属性遮蔽了原型方法
delete alice.greet;
alice.greet(); // "Hi, I'm Alice" — 恢复从原型链查找

1.3 hasOwnPropertyin 的区别

alice.hasOwnProperty('name');  // true  — 自身属性
alice.hasOwnProperty('greet'); // false — 来自原型
'greet' in alice;              // true  — in 会遍历原型链

// ES2022 推荐:Object.hasOwn()
Object.hasOwn(alice, 'name');  // true

二、构造函数与 new 运算符

2.1 new 的四步过程

当执行 new Foo(args) 时,引擎内部执行:

  1. 创建一个新的空对象 obj
  2. obj.__proto__ 设为 Foo.prototype
  3. obj 作为 this 执行 Foo(args)
  4. 如果 Foo 返回了一个对象,则 new 表达式的结果就是该对象;否则返回 obj
// 手动模拟 new
function myNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype); // 步骤 1+2
  const result = Constructor.apply(obj, args);       // 步骤 3
  return result instanceof Object ? result : obj;    // 步骤 4
}

2.2 constructor 属性

Person.prototype.constructor === Person; // true
alice.constructor === Person;            // true(沿原型链找到)

如果重写了 prototype 对象,需要手动恢复 constructor

Person.prototype = {
  greet() { /* ... */ }
  // ❌ constructor 丢失
};

// 修复:
Person.prototype = {
  constructor: Person,
  greet() { /* ... */ }
};

三、继承模式演进

3.1 原型链继承

function Animal(name) { this.name = name; }
Animal.prototype.speak = function () { return `${this.name} speaks`; };

function Dog(name, breed) {
  Animal.call(this, name); // 借用构造函数
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype); // 建立原型链
Dog.prototype.constructor = Dog;                  // 修复 constructor

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

这是 ES5 时代的寄生组合式继承(Parasitic Combination Inheritance),被认为是最理想的经典继承方案。

3.2 各继承模式对比

模式 核心思路 缺陷
原型链继承 Child.prototype = new Parent() 引用类型属性共享;无法向父构造函数传参
借用构造函数 Parent.call(this) 方法无法复用(每次实例化都创建)
组合继承 原型链 + 借用构造函数 父构造函数被调用两次
寄生组合式继承 Object.create(Parent.prototype) + Parent.call(this) ✅ 最佳方案
Object.create 纯原型委托 没有构造函数的初始化逻辑

3.3 Object.create 深入

// Object.create 的简化实现
function objectCreate(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}

// 创建一个没有原型的"纯净"对象
const dict = Object.create(null);
// dict 没有 toString、hasOwnProperty 等方法
// 适合做纯粹的键值存储

四、ES6 class — 语法糖的本质

4.1 class 到 prototype 的映射

class Animal {
  constructor(name) {
    this.name = name;        // → 实例属性
  }

  speak() {                  // → Animal.prototype.speak
    return `${this.name} speaks`;
  }

  static create(name) {     // → Animal.create(构造函数本身的属性)
    return new Animal(name);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);             // → Animal.call(this, name)
    this.breed = breed;
  }

  bark() {                   // → Dog.prototype.bark
    return 'Woof!';
  }
}

等价关系

typeof Animal;                            // 'function'
Dog.prototype.__proto__ === Animal.prototype; // true — 实例方法继承
Dog.__proto__ === Animal;                    // true — 静态方法继承

4.2 class 与传统函数构造器的差异

虽然 class 是语法糖,但有一些重要的行为差异

特性 class 传统函数构造器
必须使用 new 调用 ✅ 否则 TypeError ❌ 可直接调用
方法不可枚举 enumerable: false ❌ 默认可枚举
严格模式 ✅ 自动启用 ❌ 需要手动声明
存在 TDZ ✅ 不会被提升 ❌ 函数声明会提升
super 关键字 ✅ 支持 ❌ 需要手动 Parent.call(this)

4.3 super 的两种用法

class Dog extends Animal {
  constructor(name, breed) {
    // super() 作为函数调用 — 必须在使用 this 之前调用
    super(name);
    this.breed = breed;
  }

  speak() {
    // super.method() — 访问父类原型上的方法
    return `${super.speak()} — and barks!`;
  }
}

规范细节:在 extends 的子类构造函数中,thissuper() 调用之前是未初始化的(处于 TDZ 状态),提前访问会抛出 ReferenceError


五、instanceof 与类型检测

5.1 instanceof 的原理

a instanceof B 实质上是检查 B.prototype 是否存在于 a 的原型链上。

// 简化实现
function myInstanceOf(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

5.2 Symbol.hasInstance 自定义

class EvenNumber {
  static [Symbol.hasInstance](num) {
    return typeof num === 'number' && num % 2 === 0;
  }
}

4 instanceof EvenNumber; // true
3 instanceof EvenNumber; // false

六、面试高频题型

题型 1:画出原型链关系图

alice → Person.prototype → Object.prototype → null
         ↑ constructor: Person     ↑ constructor: Object

题型 2:手写 new 操作符

见 2.1 节的 myNew 实现。

题型 3:手写 instanceof

见 5.1 节的 myInstanceOf 实现。

题型 4:class 的本质是什么

"ES6 的 class 是语法糖,底层仍是 prototype 机制。class 定义的方法挂在 .prototype 上,extends 通过 Object.setPrototypeOf 建立两条原型链(实例链和构造函数链),super() 等价于 Parent.call(this) 但有更严格的 TDZ 约束。"

题型 5:如何实现多继承(Mixins)

JavaScript 不支持多继承,但可以用 Mixin 模式

const Serializable = (Base) => class extends Base {
  serialize() {
    return JSON.stringify(this);
  }
};

const Validatable = (Base) => class extends Base {
  validate() {
    return Object.keys(this).every(key => this[key] != null);
  }
};

class User extends Serializable(Validatable(Animal)) {
  // 同时获得 serialize() 和 validate() 方法
}

七、易错点与最佳实践

7.1 修改内置原型(Monkey Patching)

// ❌ 永远不要这样做
Array.prototype.sum = function () {
  return this.reduce((a, b) => a + b, 0);
};

修改内置原型可能导致:命名冲突、不可预测的行为、与未来 ECMAScript 标准冲突。

7.2 for...in 会遍历原型链

for (const key in alice) {
  console.log(key); // name, greet — 包括原型上的属性
}

// 过滤方式
for (const key in alice) {
  if (Object.hasOwn(alice, key)) {
    console.log(key); // 只有 name
  }
}

// 更好的选择:Object.keys() 只返回自身可枚举属性
Object.keys(alice); // ['name']

八、与其他主题的关联

关联主题 关系
this-binding new 绑定的 this 指向新创建的实例
proxy-reflect Proxy 可以拦截 [[GetPrototypeOf]] 等内部操作
design-patterns-js 工厂模式、策略模式等常依赖原型链实现
es6-features class 语法、Symbol.hasInstance 均为 ES2015+ 特性

参考资料

  • Kyle Simpson, You Don't Know JS: this & Object Prototypes (2nd Edition) — 对原型委托最深入的讨论
  • MDN Web Docs — Inheritance and the prototype chain
  • ECMA-262 — OrdinaryGetPrototypeOf
  • Nicholas C. Zakas, Professional JavaScript for Web Developers — 继承模式章节
  • Axel Rauschmayer, Speaking JavaScript — 原型链可视化讲解

延展阅读