前端原型与继承全景学习图解版

🧭 前端原型与继承全景学习图解版(整合 + 深入底层原理)


1. 为什么会有"原型链"?(设计动机 & 用处)

背景(一句话) :JavaScript 在早期没有"类(class)"语法时,需要一种轻量、动态的方式让对象复用逻辑 ------ 所以用"对象指向对象"的方式实现继承(prototype-based)。

主要用处

  • 方法复用:把方法放在原型上,所有实例共享,节省内存。
  • 动态扩展:运行时可以给原型新增方法,所有实例自动可用。
  • 实现继承:通过链式引用把"父对象"的行为借给子对象(实现"is-a"或"has-shared-behaviour")。
  • 类型识别 :通过 instanceofisPrototypeOf 来判断关系(基于原型链)。

直观比喻:每个对象背后有一个"模板(prototype)"当做影子,访问属性时先看自己,没找到就看影子,影子的影子,依此类推。


2. 核心三角:__proto__prototypeconstructor

ini 复制代码
function Person(name) {
  this.name = name;
}
const person = new Person('Alice');

ASCII 图(概念关系):

javascript 复制代码
          ┌────────────────────┐
          │   构造函数 Person  │
          │────────────────────│
          │ function Person()  │
          │                    │
          │  ┌───────────────┐ │
          │  │ prototype 对象│◀─────────────┐
          │  └───────────────┘ │            │
          └────────────────────┘            │
                     ▲                      │
                     │ constructor          │
                     │                      │
          ┌────────────────────┐            │
          │  实例对象 person   │────────────┘
          │────────────────────│
          │ { name: "Alice" }  │
          │                    │
          │ __proto__ → Person.prototype
          └────────────────────┘

重要说明

  • prototype:只有函数(通常作为构造函数)有这个属性。它是用来放共享方法/属性的对象。
  • __proto__(规范名为内部槽 [[Prototype]]):每个对象都有,指向其原型对象
  • constructor:只是 prototype 上的一个普通属性,指回构造函数(可被覆盖,非"内置魔法")。

3. 原型链总览(图 + 解释)

javascript 复制代码
person (实例)
  ├─ own: name: "Alice"
  └─ __proto__ → Person.prototype
         ├─ sayName()
         └─ __proto__ → Object.prototype
                 ├─ toString()
                 ├─ hasOwnProperty()
                 └─ __proto__ → null

要点

  • Object.prototype 是绝大多数对象在原型链上的最终祖先(除了 Object.create(null) 创建的无原型对象)。
  • 访问属性时会沿这条链 逐级向上 搜索,直到找到或走到 null

4. 属性查找与赋值:底层行为(非常重要)

4.1 读取属性(概念化流程)

访问 obj.prop 时(简化伪逻辑):

  1. 如果 obj 本身有名为 propown(自有)属性 (数据属性或访问器),返回它或调用其 getter(getter 中的 this 指向访问者,即 obj)。
  2. 否则,令 proto = Object.getPrototypeOf(obj)。若 protonull,返回 undefined
  3. proto 上重复步骤 1(即沿 __proto__ 向上查找)。
  4. 直到找到或到 null

ECMAScript 规范:这由内部方法 [[Get]] / OrdinaryGet / HasProperty 等实现。

4.2 写入属性(赋值)要小心

行为取决于是否存在 setter 以及赋值目标:

  • 若在原型链上某处找到一个 set(访问器属性),则调用该 setter,this 为赋值时的目标(receiver,通常是最初的 obj)。
  • 若没有 setter,赋值总是在 当前对象本身 创建/更新一个 own 数据属性(即会遮蔽原型上的同名属性),不会修改原型上的属性。

示例(展示 setter 行为):

ini 复制代码
const proto = {
  get x(){ return this._x || 0; },
  set x(v){ this._x = v; }
};
const obj = Object.create(proto);
obj.x = 5;             // 调用了 proto 上的 setter,this 指向 obj
console.log(obj._x);  // 5

4.3 property descriptor(属性描述符)

属性有两类:

  • 数据属性{ value, writable, enumerable, configurable }
  • 访问器(getter/setter)属性{ get, set, enumerable, configurable }

Object.getOwnPropertyDescriptor(obj, 'p') 查看。理解这些字段对调试原型问题非常关键(例如不可写/不可配置会阻止覆盖等)。

4.4 instanceof 的底层实现(伪代码)

obj instanceof Ctor 的逻辑大意是:检查 Ctor.prototype 是否出现在 obj 的原型链上(逐级比较 Object.getPrototypeOf(current))。可模拟:

ini 复制代码
function myInstanceof(obj, Ctor) {
  let proto = Object.getPrototypeOf(obj);
  const prototype = Ctor.prototype;
  while (proto !== null) {
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

5. 继承的五种实现(逐一看底层差别、优缺点与示例)

下面给出每种方式的实现原理图、代码与底层问题


5.1 原型链继承(Prototype Chain)

实现

javascript 复制代码
function Parent(){ this.colors = ['red']; }
function Child(){}
Child.prototype = new Parent();

ini 复制代码
Child.prototype  === Parent 实例
    ↑
 child.__proto__

底层问题

  • Parent 构造被用于创建 Child.prototype(也就是 Parent 的实例),因此引用类型(colors)被共享给所有 child 实例。
  • 无法传参给 Parent。

适用场景:很少单独使用;教学用途。


5.2 构造函数继承(Constructor Borrowing)

实现

javascript 复制代码
function Parent(name){ this.name = name; }
function Child(name){ Parent.call(this, name); }

核心:在子构造内部执行父构造,把父构造里的 own 属性复制到当前实例(this)。

优点 :每个实例都有独立属性,能传参。
缺点 :无法继承 Parent.prototype 上的方法(不能复用)。


5.3 组合继承(Combination)

实现

javascript 复制代码
function Child(name){
  Parent.call(this, name);      // 继承属性(实例化)
}
Child.prototype = new Parent(); // 继承方法(但会再次执行 Parent)
Child.prototype.constructor = Child;

本质 :把构造函数继承和原型链继承合并。
问题Parent 被调用两次(一次用于 Child.prototype = new Parent(),另一次用于 Parent.call(this)),导致性能或副作用(如果 Parent 初始化有开销或副作用)。


5.4 寄生组合继承(Parasitic Combination --- 最优的 ES5 方式)

实现(推荐)

javascript 复制代码
function inherit(Child, Parent) {
  Child.prototype = Object.create(Parent.prototype);
  Child.prototype.constructor = Child;
}

function Child(name){
  Parent.call(this, name); // 只调用一次
}
inherit(Child, Parent);

javascript 复制代码
child.__proto__ → Child.prototype
                 └── __proto__ → Parent.prototype

优点

  • Parent 只执行一次(在 Parent.call(this)),避免两次调用;
  • 原型方法复用;
  • 属性独立。

结论 :这是 ES5 下最常用、最合理的继承模式;ES6 class extends 本质上以此为基础实现语义。


5.5 ES6 class 继承(现代语法糖)

scala 复制代码
class Parent { ... }
class Child extends Parent {
  constructor() { super(); }
}

底层关系

  • Child.prototype.__proto__ === Parent.prototype(实例链)
  • Child.__proto__ === Parent(构造函数链,支持静态方法继承)

super() 行为

  • 在子类构造中必须先调用 super()(它执行父构造函数并设置 this)。
  • 在方法中使用 super.method() 会按 [[HomeObject]] 和原型关系查找父方法并以当前实例作为 this 调用。

优点:语义清晰、可读性高、同时支持私有字段、静态字段等现代特性。底层仍是原型链。


6. ES6 class 进阶细节(底层语义 & 私有字段)

6.1 class 与原型的等价关系

class 的实例方法被放到 prototype 上,静态方法直接放在构造函数上。类声明只是语法糖,背后仍是 function + prototype 的组合(但有更严格的内部行为,如类方法默认不可枚举、类构造函数不能被当作普通函数直接调用等)。

6.2 私有字段(#x

  • 私有字段不是在 prototype 上,它们是按实例 存放在引擎内部私有字段表中(规范中为 PrivateFields)。外部无法通过 obj['#x'] 访问。
  • 私有字段的初始化发生在构造执行期间(类字段初始化阶段),并遵从 temporal behavior (不能在 super() 前使用 this)。

6.3 super 在方法中的查找(简述)

  • 每个类方法有内部 [[HomeObject]](home object)关联,用于 super 时确定查找起点。
  • super.foo() 实际上在父类原型上查找 foo,以当前实例作为 this 进行调用(即 super 的 receiver 是当前实例)。

7. 优缺点汇总 & 能否"打破"原型链?

7.1 优点回顾

  • 内存节约:方法放原型只存一份。
  • 动态特性:运行时可扩展、更灵活。
  • 自然继承:链式结构直观支持"继承"。

7.2 缺点回顾

  • 查找成本:每次属性访问可能沿链多次查找(层级深性能差)。
  • 调试难:属性来源可能在原型上,难以追踪。
  • 共享副作用:错误地把可变数据放在原型会被所有实例共享(常见坑)。
  • 变更影响范围大:改 prototype 会影响所有实例。

7.3 是否可以打破原型链?(可以,但需权衡)

方法 A:创建无原型对象

ini 复制代码
const dict = Object.create(null);

dict__proto__ === null,没有 hasOwnPropertytoString 等方法,适合"纯字典"。

方法 B:把某对象的原型设为 null

javascript 复制代码
Object.setPrototypeOf(obj, null); // 或 obj.__proto__ = null

→ 断开继承链,但会失去默认行为且性能差(Object.setPrototypeOf 很慢,应避免在热路径中使用)。

方法 C(不是真正"打破",只是限制)

  • Object.freeze(Object.prototype) 可以防止在 Object.prototype 加入新属性(但不会移除已有原型链),通常不常用。

结论 :可以打破(Object.create(null) 最常用),但通常不建议随意打断原型链。现代代码更倾向用模块化、组合(composition)替代复杂的深继承。


8. 常用命令/查验速查表(代码片段)

javascript 复制代码
// 获取原型
Object.getPrototypeOf(obj);

// 检查 own 属性
obj.hasOwnProperty('p');

// 检查 in(会查原型)
'p' in obj;

// instanceof
obj instanceof Constructor;

// 创建以 proto 为原型的新对象
const o = Object.create(proto);

// 设置原型(慢)
Object.setPrototypeOf(obj, proto);

// 查看 own property 描述符
Object.getOwnPropertyDescriptor(obj, 'p');

// 列出 own keys (不包含原型)
Object.getOwnPropertyNames(obj);
Object.keys(obj);

// 列出 prototype 上的方法
Object.getOwnPropertyNames(Object.getPrototypeOf(obj));

9. 调试建议与练习题

调试技巧

  • 在浏览器控制台使用 console.dir(obj),展开查看 __proto__ 链。
  • 使用 Object.getPrototypeOf(obj) 明确获取原型而不是 __proto__(后者为非标准但广泛支持)。
  • Object.getOwnPropertyDescriptors(obj) 一次查看所有 own 属性及其 descriptor。
  • 使用 Reflect.get(obj, 'p', receiver)Reflect.set 理解 getter/setter 的 receiver 行为。

练习题(推荐动手)

  1. 写出 myInstanceof(obj, Ctor) 的实现(见 §4)。
  2. Object.create 实现一个继承 helper 并验证 constructor 指向是否正确。
  3. 比较实例在以下两种写法下内存差异:方法定义在构造函数 vs 方法定义在 prototype 上(创建大量实例观察内存/性能)。
  4. 实现一个类 BankAccount(私有字段)并尝试从外部访问私有字段(应报错)。

10. 总结(学习路线与建议)

  • 先理解三角关系 (实例 → __proto__prototypeconstructor),再理解原型链查找(read)与赋值(write)差异。
  • 优先用寄生组合继承(ES5)或 class extends(ES6) 来实现继承。
  • 把可变状态放构造函数把方法放 prototype/class method,避免共享引用陷阱。
  • 不要频繁修改原型链Object.setPrototypeOf 对性能影响大)。
  • 用组合优于继承的思路(composition over inheritance)减少复杂继承层次。

附:三张 ASCII 图(可直接复制到你的 Markdown)

1) 原型链结构总览图

javascript 复制代码
📦 person (实例)
│
├── name: "Alice"           // own property
│
└── __proto__ → Person.prototype
         ├── sayName()      // prototype 方法
         └── __proto__ → Object.prototype
                 ├── toString()
                 └── __proto__ → null

2) 继承演化路线图

scala 复制代码
Prototype Chain  ──> Constructor Borrowing ──> Combination
       (早期)               (能传参)               (两次调用)
               ↓
    Parasitic Combination (Object.create + call)  ←── 推荐(ES5)
               ↓
           ES6 class extends (语法糖,底层相同)

3) 查找机制流程图

javascript 复制代码
访问 obj.prop
   │
   ▼
obj 是否有 own prop?
 ├─ 是:直接返回(若为 getter,则调用 getter,this 指 obj)
 └─ 否:proto = Object.getPrototypeOf(obj)
       ├─ proto === null → 返回 undefined
       └─ 在 proto 上重复同样步骤(直到找到或到 null)
相关推荐
palpitation972 小时前
iOS Universal Link 配置
前端
csgo打的菜又爱玩2 小时前
Vue 学习与实践大纲(后端视角)
前端·学习
柯南二号3 小时前
【大前端】Vue 和 React 的区别详解 —— 两大前端框架深度对比
前端·vue.js·前端框架
IT_陈寒3 小时前
「Redis性能翻倍的5个核心优化策略:从数据结构选择到持久化配置全解析」
前端·人工智能·后端
weixin_446938873 小时前
uniapp vue-i18n如何使用
前端·vue.js·uni-app
知识分享小能手4 小时前
微信小程序入门学习教程,从入门到精通,WXS语法详解(10)
前端·javascript·学习·微信小程序·小程序·vue·团队开发
excel4 小时前
Vue 组件与插件的区别详解
前端
JarvanMo5 小时前
Flutter 开发:应用颜色使用 Class 还是 Enum?—— 你应该选择哪一个?
前端
HBR666_5 小时前
AI编辑器(二) ---调用模型的fim功能
前端·ai·编辑器·fim·tiptap