【每日一面】深入理解 JavaScript 原型链

2026-09-09 20 min 6862 字 -- 次阅读
摘要

原型链是靠 __proto__ 把实例、原型与构造函数串起来的一条查找链:属性沿它一路查到 null,把 prototype 与 constructor 的关系理顺,class extends 的本质也就通了。

基础问答

Q1:什么是原型和原型链?

答: JavaScript 中每个对象都有一个内部引用 [[Prototype]](大多数环境通过 __proto__ 访问),指向它的原型对象。当访问对象的属性或方法时,如果自身没有,就会沿着 [[Prototype]] 一路向上查找,直到找到或到达 null。这条由原型引用串联起来的链,就是原型链

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

const p = new Person('Alice');
p.sayHi();    // 自身没有 sayHi,沿原型链在 Person.prototype 上找到
p.toString(); // Person.prototype 也没有,继续向上在 Object.prototype 上找到

Q2:__proto__prototype 是什么关系?

答:

  • prototype函数独有的属性,指向以该函数作为构造函数创建出来的实例的原型对象;

  • __proto__所有对象都有的属性(即 [[Prototype]] 的访问器),指向创建它的构造函数的 prototype

  • 可以简单的认为 实例.__proto__ === 构造函数.prototype

扩展延伸

概念

理解原型链前,先分清四个核心概念:

概念归属含义
prototype仅函数函数的原型对象,供所有实例共享的属性/方法挂载于此
__proto__[[Prototype]]所有对象指向自身原型,是原型链的链条
constructor所有对象指向创建该对象的构造函数
构造函数函数new 调用并初始化实例的函数

两个易混易错点:

  1. __proto__ 是历史产物(一开始是浏览器的实现,后被标准化为附录),ES6 推荐用 Object.getPrototypeOf() / Object.setPrototypeOf() / Object.create() 操作原型;

  2. constructor 可以被改写、不可靠,判断类型更推荐 instanceofObject.prototype.toString.call()

核心机制

属性查找

访问 obj.x 时,引擎沿原型链逐级向上查找:

flowchart TD
    A[访问 obj.x] --> B{obj 自身有 x ?}
    B -->|有| C[返回自身属性]
    B -->|没有| D{obj.__proto__ 上有 x ?}
    D -->|有| E[返回原型上的属性]
    D -->|没有| F[继续沿原型链向上查找]
    F --> G{到达 Object.prototype ?}
    G -->|有 x| E
    G -->|仍没有| H[原型链终点为 null<br/>返回 undefined]

原型链

实例、原型对象、构造函数三者构成一个三角关系,并且函数本身也是对象,也有自己的原型链:

flowchart LR
    P[实例 p] -->|__proto__| PP[Person.prototype]
    PP -->|__proto__| OP[Object.prototype]
    OP -->|__proto__| N([null 原型链终点])
    PP -.->|constructor| F["function Person()"]
    OP -.->|constructor| OF["function Object()"]
    F -->|__proto__| FP[Function.prototype]
    OF -->|__proto__| FP
    FF["function Function"] -->|__proto__| FP
    FP -->|__proto__| OP

两个特殊的节点:

  • 所有函数(包括 ObjectFunction 自身)都是 Function 的实例,即 Object.__proto__ === Function.prototype

  • Object.prototype.__proto__ === null,这是整条原型链的终点。

代码示例

验证原型链的关键等式

javascript
function Person(name) { this.name = name; }
const p = new Person('Alice');

// 三角关系
p.__proto__ === Person.prototype;            // true
Person.prototype.constructor === Person;     // true

// 函数也是对象,也有原型链
Person.__proto__ === Function.prototype;     // true
Object.__proto__ === Function.prototype;     // true
Function.__proto__ === Function.prototype;   // true(Function 是自身的实例)

// 原型链终点
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null;         // true

// instanceof 的本质:沿 p 的原型链查找 Person.prototype
p instanceof Person;   // true
p instanceof Object;   // true

寄生组合式继承(ES5 继承的最优解)

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

function Student(name, grade) {
  Person.call(this, name); // 第一步:借用构造函数,继承实例属性
  this.grade = grade;
}

// 第二步:寄生式继承,以 Person.prototype 为原型创建干净的原型对象
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; // 第三步:修复 constructor 指向
Student.prototype.study = function () {
  console.log(`${this.name} is studying`);
};

const s = new Student('Bob', 3);
s.sayHi();  // Hi, I'm Bob    ← 来自 Person.prototype
s.study();  // Bob is studying

ES6 的 class Student extends Person 本质上就是这套寄生组合式继承的语法糖。

对比

prototype __proto__ 对比

维度prototype__proto__
归属只有函数拥有所有对象都拥有
作用作为实例的原型模板实例访问原型链的入口
谁指向谁构造函数.prototype 是被指向方实例.__proto__ 是指向方
规范性ES 规范属性历史属性(ES6 建议用 Object.getPrototypeOf

继承方式对比

继承方式实例属性独立原型方法复用主要缺点
原型链继承父类引用类型属性被所有实例共享,互相污染
构造函数继承无法访问父类原型方法,无法复用
组合继承父类构造函数被调用两次,性能浪费
寄生组合继承ES5 最优解,仅写法稍繁琐
class extends语法糖,本质是寄生组合式继承

如何决策

如果被问到该如何实现继承,怎么判断选择哪种方案,这里也提供一个决策标准,但是实际开发中,我们基本都考虑的是 class ,走面向对象风格,因为比较容易阅读,并且引擎也做出了很多优化,大多数情况下不需要真这样决策。

flowchart TD
    A[需要实现继承] --> B{环境支持 ES6+ ?}
    B -->|是| C[class / extends<br/>本质是寄生组合式继承]
    B -->|否| D{需要继承父类实例属性?}
    D -->|否| E[直接原型链继承]
    D -->|是| F[构造函数借用<br/>Person.call this]
    F --> G{需要复用父类原型方法?}
    G -->|否| H[仅构造函数继承]
    G -->|是| I[寄生组合式继承<br/>Object.create 组合两者]
    C --> J[注意:子类构造函数<br/>必须先调用 super]
    I --> K[记得修复 constructor 指向]

面试追问

追问 1:new 操作符具体做了什么?能手写一个吗?

new 共做 4 件事:创建空对象 → 链接原型 → 绑定 this 执行构造函数 → 根据返回值决定结果。

flowchart TD
    A["new Person('Alice')"] --> B[创建空对象 obj]
    B --> C["obj.__proto__ = Person.prototype<br/>链接原型链"]
    C --> D[以 obj 为 this 执行构造函数<br/>初始化实例属性]
    D --> E{构造函数显式返回了对象?}
    E -->|是| F[返回该对象<br/>覆盖默认结果]
    E -->|否| G[返回 obj]

实现代码:

javascript
function myNew(Constructor, ...args) {
  // 1. 创建空对象并链接原型
  const obj = Object.create(Constructor.prototype);
  // 2. 绑定 this 执行构造函数
  const result = Constructor.apply(obj, args);
  // 3. 构造函数若返回对象则使用它,否则返回新对象
  return result instanceof Object ? result : obj;
}

追问 2:分析以下代码的输出,并解释原因

javascript
function F() {}
const f = new F();

console.log(f.__proto__ === F.prototype);
console.log(F.__proto__ === Function.prototype);
console.log(Function.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Function.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__);

依次输出:true, true, true, true, true, null

关键在于理解:所有函数都是 Function 的实例,包括 ObjectFunction 自身(Function.__proto__ === Function.prototype 是著名的鸡生蛋现象);而 Function.prototype 作为对象,它的原型是 Object.prototype;原型链最终在 Object.prototype.__proto__ === null 处终止。

追问 3:instanceof 的原理是什么?手写实现一个

instanceof 的原理是:沿左侧对象的原型链逐级查找,看能否找到右侧构造函数的 prototype

javascript
function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left); // 取 left 的原型
  const prototype = right.prototype;

  while (true) {
    if (proto === null) return false;        // 走到原型链终点
    if (proto === prototype) return true;    // 找到了
    proto = Object.getPrototypeOf(proto);    // 继续向上
  }
}

myInstanceof(new Person('A'), Person); // true
myInstanceof(123, Number);             // true(包装对象语义)

typeof 的区别: typeof 返回基本类型字符串,但 typeof null === 'object'、无法区分具体引用类型;instanceof 能判断引用类型的继承关系,但对基本类型需依赖包装对象,且跨 iframe 场景会失效。

追问 4:为什么说组合继承调用了两次父类构造函数?如何证明?

组合继承 = 构造函数继承 + 原型链继承:

javascript
function Student(name, grade) {
  Person.call(this, name);   // 第二次调用:实例属性覆盖
  this.grade = grade;
}
Student.prototype = new Person(); // 第一次调用:name 挂到了 Student.prototype 上

证明代码:

javascript
const s = new Student('Bob', 3);
s.name; // 'Bob',来自实例属性(第二次调用的结果)

delete s.name;
s.name; // 仍然是 'Bob'!说明 Student.prototype 上还残留一份 name
        // 这就是第一次调用产生的冗余属性

寄生组合式继承用 Object.create(Person.prototype) 替代 new Person(),只继承原型、不执行构造函数,从而消除了这次多余调用。

追问 5:ES6 classextends 相比 ES5 继承,有哪些本质区别和增强?

extends 的核心机制与寄生组合式继承一致,但语义更严格:

javascript
class Person {
  static type = 'human';          // 静态属性
  constructor(name) { this.name = name; }
  sayHi() { console.log(this.name); }
}

class Student extends Person {
  constructor(name, grade) {
    super(name); // 必须先调用 super,否则 this 不可用(TDZ)
    this.grade = grade;
  }
}

本质区别:

  1. 静态属性/方法也被继承:ES5 中 Student.__proto__ 指向 Function.prototype,而 class 中 Student.__proto__ === Person,所以 Student.type 可用;

  2. 必须 new 调用:class 不能像普通函数那样直接执行,否则报错;

  3. 方法不可枚举:class 原型上的方法默认不可枚举,ES5 直接赋值的方式是可枚举的;

  4. 不存在变量提升:class 声明有暂时性死区(TDZ);

  5. 内部严格模式:class 声明体默认运行在严格模式下。

小结

要点内容
等价关系实例.__proto__ === 构造函数.prototype
查找流程沿 [[Prototype]] 逐级向上,终点为 Object.prototype.__proto__ === null
特殊类型所有函数都是 Function 的实例;Function.__proto__ === Function.prototype
继承最优解ES5 寄生组合式继承;ES6 class extends 是其语法糖
经典考点手写 new、手写 instanceof、组合继承的两次调用问题

关注本系列,每天进步一点点,面试不再慌。

评论