------小Dora 的 JavaScript 修炼日记 · Day 5
"你以为你在访问对象属性,其实你是在走一条原型修炼者的查找之路。"
------一个被
__proto__
绊倒三次的前端人
前几日你学了作用域、上下文、闭包......
今天要解封的,是 JavaScript 的江湖绝学:
🌀 原型 & 原型链
这不是江湖新词,它藏在你每天敲的代码里:
- 你以为
obj.toString()
是obj
自己的? - 你以为
instanceof
是个魔法? - 你以为
extends
是 ES6 的福音?其实它背后也是 prototype。
本期关键词:
__proto__
、prototype
、原型链
、属性查找
、继承
、instanceof
、V8 Hidden Class
🧠 一、原型系统初体验:你以为你 new 出的是空对象?
ini
function Person(name) {
this.name = name;
}
const p = new Person("小Dora");
执行完上面代码后,实际上发生了以下仪式感十足的流程:
- 创建一个空对象
p
- 把
p.__proto__
指向Person.prototype
- 执行
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 引擎会做:
- 先在
obj
自身找属性 - 找不到就看
obj.__proto__
- 再往
obj.__proto__.__proto__
查找 - 最后到达
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 做了两件事:
- 将对象抽象为 Hidden Class(隐藏类) ------ 类似于 Java/C++ 的结构定义
- 为对象访问构建 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 发生变异,优化中断,p1
和 p2
不再共享结构。
⚙️ 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 为什么是世界上最"灵活"的语言之一!