# 面试官再问原型链,就把这篇文章甩给他!一图胜千言,彻底搞懂JavaScript继承的奥秘

75 阅读8分钟

面试官再问原型链,就把这篇文章甩给他!一图胜千言,彻底搞懂JavaScript继承的奥秘

本文正在参加「金石计划」

前言

你是否曾在面试中被"简述原型链"一问击倒?是否曾对 prototype__proto__ 感到头晕目眩?别担心,这是每个前端人的必经之路。

原型链不是用来死记硬背的,它是JavaScript精妙的设计模式。理解它,你才能真正理解这门"基于原型的语言"。很多人靠死记硬背过关,但真正理解的很少——这就是我们这篇文章的价值所在。

本文将带你:

  1. 🎯 亲手"画出"原型链,搞清所有关键属性的关系
  2. 🔧 理解 newinstanceof 的底层逻辑
  3. 💻 掌握从ES5到ES6的继承实现方案
  4. 🚀 看透 class 语法糖背后的真相

前置知识:为什么需要原型?

在开始之前,我们先明确一个基本概念:JavaScript中的对象就是一组键值对的集合。

现在思考一个问题:如果我们有1000个Person对象,每个对象都需要一个sayHello方法,你会怎么做?

// 低效的做法:每个实例都有自己的方法副本
function Person(name) {
  this.name = name;
  this.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
  };
}

const p1 = new Person('Alice');
const p2 = new Person('Bob');
console.log(p1.sayHello === p2.sayHello); // false - 两个不同的函数!

这样会创建1000个相同的函数,极度浪费内存。原型就是为了解决这个问题而生的:让多个对象共享同一个方法

第一章:核心概念"三巨头"

要理解原型链,必须先搞清楚三个关键属性:prototype__proto__constructor

1. prototype(显式原型)

prototype 是函数的属性,只有函数才有。

function Person(name) { 
  this.name = name; 
}

// 函数Person天生就有一个prototype属性
console.log(Person.prototype); // 输出一个对象
console.log(typeof Person.prototype); // "object"

// 我们可以在prototype上添加方法,这些方法将被所有实例共享
Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

关键点prototype 是构造函数的"蓝图",它定义了由该构造函数创建的所有实例会"继承"什么。

2. __proto__(隐式原型)

__proto__ 是对象的属性,几乎所有对象都有。

const p1 = new Person('Alice');

// 实例p1的__proto__指向其构造函数的prototype
console.log(p1.__proto__ === Person.prototype); // true

// 通过__proto__,p1可以访问到Person.prototype上的方法
p1.sayHello(); // "Hello, I'm Alice"

关键点__proto__ 是实例对象的"血脉",它指向创建该对象的构造函数的 prototype

3. constructor(构造函数)

constructor 是原型对象的属性,它指向该原型对象关联的构造函数本身。

// Person.prototype的constructor指回Person函数本身
console.log(Person.prototype.constructor === Person); // true

// 实例p1本身没有constructor,但会沿着原型链找到它
console.log(p1.constructor === Person); // true
console.log(p1.hasOwnProperty('constructor')); // false - constructor不在p1自身上

三巨头关系总结表

属性归属指向说明
prototype函数原型对象构造函数的蓝图
__proto__对象原型对象实例对象的"血脉"
constructor原型对象构造函数说明"我是谁造的"

记忆口诀函数有prototype,对象有__proto__,原型有constructor

第二章:图解!什么是原型链?

现在来到最核心的部分:原型链到底是什么?

原型链的查找机制

当访问一个对象的属性时,JavaScript引擎会:

  1. 先在对象自身属性中查找
  2. 如果找不到,就去它的 __proto__ 上找
  3. 如果还找不到,就去 __proto__.__proto__ 上找
  4. 依此类推,直到找到属性或者到达 null

这条由 __proto__ 连接起来的链路,就是原型链

终极原型链关系图

让我们用Mermaid图来可视化这个关系:

graph TD
    A[实例 p1] -->|__proto__| B[Person.prototype];
    B -->|__proto__| C[Object.prototype];
    C -->|__proto__| D[null];
    E[函数 Person] -->|prototype| B;
    B -->|constructor| E;
    F[函数 Object] -->|prototype| C;
    C -->|constructor| F;
    
    style A fill:#e1f5fe
    style E fill:#f3e5f5
    style F fill:#f3e5f5
    style B fill:#fff3e0
    style C fill:#fff3e0

图解说明

  • p1.__proto__ 指向 Person.prototype
  • Person.prototype.__proto__ 指向 Object.prototype
  • Object.prototype.__proto__ 指向 null(原型链的尽头)
  • 这就是完整的原型链!

实际查找示例

// 1. 查找自身属性
console.log(p1.name); // "Alice" - 在p1自身上找到

// 2. 查找原型上的方法
console.log(p1.sayHello); // function - 在Person.prototype上找到

// 3. 查找更上层的方法
console.log(p1.toString); // function - 在Object.prototype上找到

// 4. 查找不存在的属性
console.log(p1.neverExist); // undefined - 一直找到null也没找到

// 验证原型链
console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

第三章:实战!手写一个new操作符

理解了原型链,我们来看看与之密切相关的 new 操作符。很多人天天用 new,却不知道它背后做了什么。

new 操作符的四步魔法

当你执行 new Person('Alice') 时,实际上发生了四件事:

  1. 创建新对象:创建一个空对象
  2. 设置原型链:将新对象的 __proto__ 指向构造函数的 prototype
  3. 绑定this执行:执行构造函数,将 this 绑定到新对象
  4. 返回结果:如果构造函数返回对象则返回该对象,否则返回新对象

手写 myNew 函数

function myNew(constructorFn, ...args) {
  // 1. 创建一个新对象,并将其__proto__指向构造函数的prototype
  const obj = Object.create(constructorFn.prototype);
  
  // 2. 执行构造函数,并将this绑定到新创建的对象上
  const result = constructorFn.apply(obj, args);
  
  // 3. 如果构造函数返回了一个对象,则返回该对象;否则,返回新创建的对象
  return result instanceof Object ? result : obj;
}

// ---------------- 测试我们的myNew ----------------
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

const p2 = myNew(Person, 'Bob');
console.log(p2.name); // 'Bob'
p2.sayHello(); // 'Hello, I'm Bob'
console.log(p2.__proto__ === Person.prototype); // true
console.log(p2 instanceof Person); // true

关键点解析

  • Object.create(constructorFn.prototype) 一步完成了创建对象和设置原型链
  • apply 方法将构造函数的 this 绑定到新对象
  • 判断返回值确保与原生 new 行为一致

第四章:instanceof 的底层原理

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

手写 myInstanceof 函数

function myInstanceof(obj, constructorFn) {
  // 基本类型直接返回false
  if (obj === null || typeof obj !== 'object') {
    return false;
  }
  
  let proto = obj.__proto__;
  
  while (proto) {
    // 如果找到了构造函数的prototype
    if (proto === constructorFn.prototype) {
      return true;
    }
    // 沿着原型链继续向上查找
    proto = proto.__proto__;
  }
  
  // 找到原型链顶端(null)还没找到,返回false
  return false;
}

// ---------------- 测试 ----------------
const p1 = new Person('Alice');

console.log(myInstanceof(p1, Person)); // true
console.log(myInstanceof(p1, Object)); // true  
console.log(myInstanceof(p1, Array)); // false

// 与原生instanceof对比
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Object); // true
console.log(p1 instanceof Array); // false

工作原理instanceof 就是沿着对象的 __proto__ 链向上查找,如果能找到右边构造函数的 prototype 属性,就返回 true

第五章:现代继承的实现(从ES5到ES6)

理解了原型链,我们现在可以来看看如何在JavaScript中实现继承。

5.1 ES5时代的继承演变与"圣杯模式"

在ES6之前,开发者们探索了多种继承方式,最终"寄生组合式继承"被认为是ES5时代的最优解。

各种继承方式的比较
  • 原型链继承:引用属性被所有实例共享(大问题!)
  • 构造函数继承:方法无法复用(也是问题!)
  • 组合继承:调用两次父类构造函数(效率问题)
圣杯模式:寄生组合式继承
// 目标:让 SubType 继承 SuperType 的属性和方法

// 父类构造函数
function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

// 父类原型方法
SuperType.prototype.sayName = function() {
  console.log(this.name);
};

// 子类构造函数
function SubType(name, age) {
  // 1. 继承属性:调用父类构造函数,绑定子类this
  SuperType.call(this, name); // 第二次(也是唯一一次)调用SuperType
  this.age = age;
}

// 2. 继承方法:这是核心,建立子类与父类原型的链接
function inheritPrototype(subType, superType) {
  // 创建一个以父类原型为原型的新对象,作为子类的原型
  var prototype = Object.create(superType.prototype); // 创建对象
  prototype.constructor = subType; // 增强对象(修复constructor指向)
  subType.prototype = prototype; // 指定对象
}

// 调用函数,完成继承
inheritPrototype(SubType, SuperType);

// 子类原型方法
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

// ---------------- 测试 ----------------
var instance1 = new SubType('Nicholas', 29); // 第一次调用SuperType
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']
instance1.sayName(); // 'Nicholas' - 来自父类原型
instance1.sayAge(); // 29 - 来自子类原型

var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // ['red', 'blue', 'green'] (互不影响)
instance2.sayName(); // 'Greg'
instance2.sayAge(); // 27

// 验证原型链
console.log(instance1 instanceof SubType); // true
console.log(instance1 instanceof SuperType); // true
console.log(instance1 instanceof Object); // true
寄生组合式继承的原型链
graph TD
    A[instance1] -->|__proto__| B[SubType.prototype];
    B -->|__proto__| C[SuperType.prototype];
    C -->|__proto__| D[Object.prototype];
    D -->|__proto__| E[null];
    
    B -->|constructor| F[SubType];
    C -->|constructor| G[SuperType];
    
    style A fill:#e1f5fe
    style B fill:#fff3e0
    style C fill:#fff3e0

关键优势

  1. 只调用一次父类构造函数(在子类构造函数中)
  2. 原型链纯净,没有不必要的属性
  3. ** instanceof 和 isPrototypeOf 正常工作**

5.2 ES6的 classextends 语法糖

ES6引入了 class 关键字,让继承写起来像传统面向对象语言一样简单。但请记住:它只是语法糖

用 ES6 class 重写继承
class SuperType {
  constructor(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
  }

  sayName() {
    console.log(this.name);
  }
}

class SubType extends SuperType {
  constructor(name, age) {
    super(name); // 相当于之前的 SuperType.call(this, name)
    this.age = age;
  }

  sayAge() {
    console.log(this.age);
  }
}

// ---------------- 测试 ----------------
const instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']
instance1.sayName(); // 'Nicholas'
instance1.sayAge(); // 29

const instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // ['red', 'blue', 'green']
透过语法糖看本质

让我们验证一下 class 的底层依然是原型链:

// 原型链关系与寄生组合式继承完全一致!
console.log(instance1.__proto__ === SubType.prototype); // true
console.log(SubType.prototype.__proto__ === SuperType.prototype); // true
console.log(instance1 instanceof SubType); // true
console.log(instance1 instanceof SuperType); // true

// class 中定义的方法,都在原型上,是不可枚举的
console.log(Object.keys(SubType.prototype)); // [] (ES5写法的方法是可枚举的)
console.log(Object.getOwnPropertyNames(SubType.prototype)); // ['constructor', 'sayAge']

Babel转换验证: 如果你将上面的ES6代码粘贴到 Babel REPL,会发现转换后的ES5代码核心部分正是我们手写的寄生组合式继承

总结与升华

让我们回顾一下今天学到的核心内容:

一张图回顾核心关系

graph TD
    A[实例] -->|__proto__| B[构造函数.prototype];
    B -->|__proto__| C[Object.prototype];
    C -->|__proto__| D[null];
    E[构造函数] -->|prototype| B;
    B -->|constructor| E;
    
    style A fill:#e1f5fe
    style E fill:#f3e5f5
    style B fill:#fff3e0
    style C fill:#fff3e0

核心思想

JavaScript通过原型链实现了基于原型的继承和共享,这是一种极其灵活的对象创建和复用机制。与传统的类式继承不同,原型继承更直接、更动态。

关键一句话__proto__ 是对象的血脉,prototype 是函数的蓝图,它们共同编织了一张名为原型链的网,将所有对象联系在一起。

重要结论

  1. 每个函数都有 prototype 属性(除了箭头函数和Function.prototype)
  2. 每个对象都有 __proto__ 属性(除了Object.prototype)
  3. 原型链的尽头是 null
  4. class 是语法糖,底层还是原型链
  5. 理解原型链是理解JavaScript对象系统的关键

互动引导

你现在对原型链的理解清晰了吗?欢迎在评论区:

  1. 分享你曾经在理解原型链时遇到的最大困惑
  2. 谈谈你在实际项目中运用原型链的经验
  3. 尝试解释这个有趣的现象Function.__proto__ === Function.prototype (这是JavaScript中的"鸡生蛋蛋生鸡"问题!)

或者,你可以思考一下:在ES6的 class 中,如果子类构造函数里不写 super() 调用,为什么会报错?(提示:这和ES5的继承实现机制有何不同?)


希望这篇文章能帮你彻底搞懂JavaScript原型链!如果觉得有帮助,欢迎点赞收藏,让更多小伙伴看到。 Happy Coding! 🚀