原型与原型链 · 千年武学秘籍终解封!

------小Dora 的 JavaScript 修炼日记 · Day 5

"你以为你在访问对象属性,其实你是在走一条原型修炼者的查找之路。"

------一个被 __proto__ 绊倒三次的前端人

前几日你学了作用域、上下文、闭包......

今天要解封的,是 JavaScript 的江湖绝学:

🌀 原型 & 原型链

这不是江湖新词,它藏在你每天敲的代码里:

  • 你以为 obj.toString()obj 自己的?
  • 你以为 instanceof 是个魔法?
  • 你以为 extends 是 ES6 的福音?其实它背后也是 prototype。

本期关键词:__proto__prototype原型链属性查找继承instanceofV8 Hidden Class


🧠 一、原型系统初体验:你以为你 new 出的是空对象?

ini 复制代码
function Person(name) {
  this.name = name;
}
const p = new Person("小Dora");

执行完上面代码后,实际上发生了以下仪式感十足的流程:

  1. 创建一个空对象 p
  2. p.__proto__ 指向 Person.prototype
  3. 执行 Person.call(p, "小Dora") 把 name 挂到 p

👉 注意!__proto__ 是每个实例对象身上的隐式原型属性,指向构造函数的 prototype。


🔍 二、搞清楚三个「Prototype」的概念

名称 属性名 说明
隐式原型 obj.__proto__ 每个对象自带,指向构造函数的 prototype
显式原型 Fn.prototype 函数对象特有,创建实例时用作原型
构造器引用 obj.constructor 通常指向创建它的构造函数(可被改)
javascript 复制代码
function Foo() {}
const f = new Foo();
console.log(f.__proto__ === Foo.prototype); // ✅ true
console.log(Foo.prototype.constructor === Foo); // ✅ true

💡 __proto__ 决定了原型链的走向,而 prototype 决定了你能不能成为师门传人。


🧭 三、原型链是怎么查找属性的?

你执行 obj.xxx 时,JS 引擎会做:

  1. 先在 obj 自身找属性
  2. 找不到就看 obj.__proto__
  3. 再往 obj.__proto__.__proto__ 查找
  4. 最后到达 Object.prototype,否则返回 undefined
ini 复制代码
const obj = {};
console.log(obj.toString); // 来自 Object.prototype

🔁 属性查找路径图示:

javascript 复制代码
obj → obj.__proto__ → Object.prototype → null

⚠️ 注意:原型链是对象间的链,而不是函数调用栈!


🏗️ 四、V8 里的原型链:不是链,其实是哈希结构 + Hidden Class!

你指出得非常准确。关于 V8 中原型链的优化部分(特别是 Hidden Class 和属性访问机制),确实可以讲得更底层系统化。下面我将重新撰写这一节,以高级前端工程师的视角出发,结合 V8 引擎实现细节和实际性能陷阱,进一步深入原型机制背后的执行逻辑:


🏗️ 四、V8 中的原型链真的还是"链"吗?其实是 Hidden Class + Inline Cache 的高速结构!

💥 表面看是链,其实 V8 早已偷偷换上"喷气引擎"

你以为你访问 obj.xxx 是一层一层爬原型链,其实 V8 早就不那样做了。

V8 做了两件事:

  1. 将对象抽象为 Hidden Class(隐藏类) ------ 类似于 Java/C++ 的结构定义
  2. 为对象访问构建 Inline Cache(内联缓存) ------ 快速记忆属性查找路径

🔍 什么是 Hidden Class?

V8 为每个对象动态生成隐藏类(内部结构类似状态机):

ini 复制代码
function Foo() {
  this.x = 1;
}
const a = new Foo(); // 分配 HiddenClass_H0
a.y = 2;             // 变为 HiddenClass_H1(状态转移)

类图结构(类似状态跳转):

yaml 复制代码
HiddenClass_H0: { x }
      |
     添加 y
      ↓
HiddenClass_H1: { x, y }

🧠 每次对象结构改变(新增属性,顺序不同)都会触发 Hidden Class 转移。这就是为什么 动态添加属性会影响性能


🧪 举个经典优化与反优化例子:

ini 复制代码
function Point(x, y) {
  this.x = x;
  this.y = y;
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);

上面代码中,p1 和 p2 拥有相同的 Hidden Class,内存布局一致,V8 可以共享优化。

🧨 但如果你动态加属性:

ini 复制代码
p1.z = 5;

此时 p1 的 Hidden Class 发生变异,优化中断,p1p2 不再共享结构。


⚙️ Hidden Class 的存在意义?

V8 是为性能设计的,它要:

  • 避免 JS 的动态特性带来的频繁查找
  • 让对象像 C++ 一样高效定位属性偏移量(offset)

V8 为每个 Hidden Class 分配属性偏移表(Property Descriptor),这样当你访问 p1.x 时,V8 不需要在原型链上一层一层查找,而是:

sql 复制代码
"p1 是 HiddenClass_H1 类型,x 在 offset 0 上,走你!"

🧠 再讲讲 Inline Cache(IC):属性访问怎么越来越快?

当 JS 引擎第一次遇到 obj.prop 时,它会去原型链查找,并把查找路径缓存下来

下次遇到同样的对象结构,就可以直接命中缓存,不必重复找。

arduino 复制代码
obj.a; // 第一次找,全链查找
obj.a; // 第二次找,直接命中 Inline Cache,性能飞起 🚀

这就是为什么 结构稳定的对象能让 V8 优化到极致


⚠️ 面试 / 项目优化建议

场景 推荐做法 原因
构造函数里动态加属性 在构造函数内统一定义 避免 Hidden Class 转换
对象属性顺序 保持一致 保证对象结构一致性
原型继承 使用 Object.create 避免不必要的继承层干扰 Hidden Class 构建
批量对象创建 不要乱改结构(比如后期加字段) 防止 V8 回退到 dictionary 模式(超慢)

🧠 原型链与 Hidden Class 联动小结

机制 本质 V8 优化方式
原型链查找 多层对象间 __proto__ 缓存路径 + 属性偏移量优化
对象属性结构 动态 Hidden Class + 状态跳转图
属性访问优化 原始为线性搜索 Inline Cache 内联缓存
不规范结构对象 动态新增属性、结构不一致 回退为 dictionary 模式,极慢 ⚠️

🧪 补充 Debug 工具推荐(深入分析 Hidden Class):

在 Chrome DevTools 中:

scss 复制代码
%HaveSameMap(obj1, obj2) // 判断是否拥有相同 Hidden Class

或者使用 V8 Inspector Protocol + --trace_maps 参数启动 Node。


✅ 血与泪教训总结

  • 动态添加属性 ≠ 万能扩展,而是打断优化之刃
  • 属性查找 ≠ 原型链爬树,而是 V8 提前铺路的内联查找高速公路
  • 如果你能保证构造函数结构稳定、使用标准继承方式,你的代码就能吃到 V8 优化的大餐 🍖

🧬 五、手写继承经典场景:组合继承 + 原型链继承

🔧 原型链继承

javascript 复制代码
function Animal() {}
Animal.prototype.eat = function () {
  console.log("吃饭");
};
function Dog() {}
Dog.prototype = new Animal();

const dog = new Dog();
dog.eat(); // 吃饭

🧨 问题:所有实例共享同一个父类实例!

🧪 组合继承(最常见)

javascript 复制代码
function Animal(name) {
  this.name = name;
}
Animal.prototype.sayHi = function () {
  console.log("Hi", this.name);
};

function Dog(name) {
  Animal.call(this, name); // 继承属性
}
Dog.prototype = Object.create(Animal.prototype); // 继承方法
Dog.prototype.constructor = Dog;

const d = new Dog("小哈");
d.sayHi(); // Hi 小哈

🔍 六、instanceof 判断原理:沿着 proto

javascript 复制代码
function Foo() {}
const f = new Foo();
console.log(f instanceof Foo); // true

👉 实际上做了这件事:

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

🧠 所以判断的是:某对象的原型链上是否存在某构造函数的 prototype


🧠 七、结合上下文与词法环境的理解

虽然原型链不属于执行上下文的范畴,但它会在 V8 引擎进行属性查找时和作用域链一起生效:

类型 查找路径 存储位置
变量查找 词法环境链(作用域链) 执行上下文
对象属性查找 原型链(proto 链) 对象 + Hidden Class

💥 闭包保存词法环境,而原型链保存的是构造函数继承路径,两者共同构成 JS 的"运行时灵魂双剑"。


📋 原型系统专属 Checklist 自检清单(进阶)

  • 我能区分 prototype / proto / constructor?
  • 我能准确解释对象属性的查找路径?
  • 我能手写 instanceof 实现?
  • 我了解原型链查找过程和终点?
  • 我理解组合继承 / 原型继承的实现细节?
  • 我知道 V8 会生成 Hidden Class 优化对象访问?
  • 我知道原型链 ≠ 作用域链,两者分工明确?
  • 我能 debug 原型链中的属性覆盖和查找?
  • 我能用图画出多个构造函数继承的链条?
  • 我理解 ES6 class 背后仍是 prototype 的语法糖?

✅ Day 5 总结打卡

概念 说明
__proto__ 每个对象的隐式原型,指向其构造函数的 prototype
prototype 每个函数对象自带,用于构造实例的原型链
原型链 属性查找的路径,由对象逐层连接其 __proto__ 构成
Hidden Class V8 优化用的"隐藏类型系统",加快属性访问
继承实现 JS 使用原型链 + 构造函数组合式继承
instanceof 检查构造函数 prototype 是否在对象原型链上

🎯 你不是学会了原型,而是:

在千层原型链、闭包作用域、上下文栈、V8 执行模型的交错中

能准确找到变量和属性的来源

能合理设计继承链避免性能陷阱

能解释 JS 为什么是世界上最"灵活"的语言之一!

相关推荐
绝无仅有20 分钟前
OSS文件上传解析失败,错误:文件下载失败的排查与解决
后端·面试·架构
LaoZhangAI1 小时前
Kiro vs Cursor:2025年AI编程IDE深度对比
前端·后端
止观止1 小时前
CSS3 粘性定位解析:position sticky
前端·css·css3
爱编程的喵1 小时前
深入理解JavaScript单例模式:从Storage封装到Modal弹窗的实战应用
前端·javascript
lemon_sjdk1 小时前
Java飞机大战小游戏(升级版)
java·前端·python
G等你下课1 小时前
如何用 useReducer + useContext 构建全局状态管理
前端·react.js
欧阳天羲1 小时前
AI 增强大前端数据加密与隐私保护:技术实现与合规遵
前端·人工智能·状态模式
慧一居士1 小时前
Axios 和Express 区别对比
前端
I'mxx2 小时前
【html常见页面布局】
前端·css·html