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

266 阅读10分钟

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

“你以为你在访问对象属性,其实你是在走一条原型修炼者的查找之路。”
——一个被 __proto__ 绊倒三次的前端人

前几日你学了作用域、上下文、闭包……
今天要解封的,是 JavaScript 的江湖绝学:

🌀 原型 & 原型链

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

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

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


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

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」的概念

1、先搞清楚三个核心对象及属性所在位置

对象/函数有哪些关键属性含义
函数对象 Foo.prototype, .__proto__.prototype 是函数特有,用于创建实例的原型;__proto__ 指向 Function.prototype(函数的原型)
实例对象 obj.__proto__指向创建它的函数的 .prototype,用于属性查找的“原型链”
函数的 prototype 对象.constructor, .__proto__.constructor 指向函数本身;__proto__ 指向 Object.prototype

2、关系图(箭头指向是“指向”)

实例对象 obj
      │
      └── __proto__ ──▶ Foo.prototype 对象
                           │
                           ├── constructor ──▶ Foo 函数对象
                           │
                           └── __proto__ ──▶ Object.prototype
                           
Foo 函数对象
      │
      └── __proto__ ──▶ Function.prototype

3、它们之间的核心逻辑

名称角色作用和指向
obj.__proto__实例的隐式原型链指针指向 Foo.prototype,查找属性时用
Foo.prototype构造函数的显式原型对象用作实例对象的原型,保存共享属性和方法
Foo.prototype.constructor连接回构造函数的引用指向 Foo,标识该原型对象属于哪个构造函数
Foo.__proto__函数的隐式原型链指针指向 Function.prototype,因为函数也是对象

4、巧记口诀

  • 「实例的 __proto__ 指向构造函数的 prototype
  • 「构造函数的 prototype.constructor 指回构造函数」
  • 「构造函数(函数)本身的 __proto__ 指向 Function.prototype

一句话记忆:

实例指向原型,原型指回函数,函数指向函数原型。


5、补充:为什么函数有 prototype 而普通对象没有?

  • prototype 是函数创建实例时的“模板”,普通对象没法 new,自然没有 prototype
  • 所以访问普通对象的 prototypeundefined

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

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

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

🔁 属性查找路径图示:

obj → obj.__proto__Object.prototypenull

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


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

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

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

V8 做了两件事:

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

🔍 什么是 Hidden Class?

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

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

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

HiddenClass_H0: { x }
      |
     添加 y
      ↓
HiddenClass_H1: { x, y }

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


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

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 可以共享优化。

🧨 但如果你动态加属性:

p1.z = 5;

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


⚙️ Hidden Class 的存在意义?

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

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

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

“p1 是 HiddenClass_H1 类型,x 在 offset 0 上,走你!”

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

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

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

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 中:

%HaveSameMap(obj1, obj2) // 判断是否拥有相同 Hidden Class

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


✅ 血与泪教训总结

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

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

🔧 原型链继承

function Animal() {}
Animal.prototype.eat = function () {
  console.log("吃饭");
};
function Dog() {}
Dog.prototype = new Animal();

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

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

🧪 组合继承(最常见)

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

function Foo() {}
const f = new Foo();
console.log(f instanceof Foo); // true

👉 实际上做了这件事:

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 的“运行时灵魂双剑”。


📦 八、class 是糖?不,是精装 prototype


class Animal {
  say() {
    console.log("hi");
  }
}
const a = new Animal();

等价于:


function Animal() {}
Animal.prototype.say = function () {
  console.log("hi");
};

📌 class 实际创建的是构造函数,其方法挂在 Class.prototype 上。

✅ 结构仍是原型链:

a.__proto__ === Animal.prototype
Animal.prototype.constructor === Anima

这是一个非常关键且有深度的问题,尤其涉及 ES6 class 继承背后的原型链机制和 super 的工作原理。下面我详细解析 super 的继承机制,帮你理解它到底是怎么“跑”的。


九、ES6 super 的继承机制解析

1. super 在类中的含义

  • super 关键字主要有两种用途:

    1. 在子类构造函数中调用父类构造函数: super(...) 用来调用父类的构造器,负责初始化父类部分的属性。
    2. 在子类方法中调用父类同名方法: super.method() 调用父类原型上的对应方法。

2. ES6 class 继承本质

  • ES6 class 的继承本质是通过原型链和内部机制实现的,继承了原型链与构造函数链的双重机制。
  • 子类的 prototype.__proto__ 指向父类的 prototype,保证实例方法继承。
  • 子类的构造函数本身(即函数对象)的 __proto__ 指向父类构造函数,保证静态属性继承。

3. 详细示意

class Parent {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    console.log('Hi, I am ' + this.name);
  }
  static staticMethod() {
    console.log('Parent static method');
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);  // 调用父类构造函数
    this.age = age;
  }
  sayHi() {
    super.sayHi(); // 调用父类方法
    console.log('And I am ' + this.age + ' years old');
  }
  static staticMethod() {
    super.staticMethod(); // 调用父类静态方法
    console.log('Child static method');
  }
}

原型链结构示意:

Child.prototype.__proto__ === Parent.prototype

Child.__proto__ === Parent
  • 实例方法继承: 子类原型链链上指向父类原型,实例访问方法时会顺着链找到父类方法。
  • 静态方法继承: 子类构造函数对象链上指向父类构造函数对象,所以静态方法也被继承。

4. super 的调用底层机制

  • super 实际上是通过访问**父类原型对象([[HomeObject]].[[GetPrototypeOf]])**来实现的。
  • 例如,super.sayHi() 实际调用的是 Parent.prototype.sayHi.call(this)
  • 在构造函数中,super() 是调用父类构造函数,确保父类实例属性被正确初始化。

5. super 和执行上下文

  • super 调用绑定的是当前实例对象的 this,即调用时上下文不会变。
  • 这保证了即使是调用父类方法,this 依然是子类实例。

6. 总结

关系描述
Child.prototype.__proto__指向 Parent.prototype,实现实例方法继承
Child.__proto__指向 Parent 构造函数,继承静态方法
super()在子类构造函数中调用父类构造函数,初始化父类属性
super.method()调用父类原型上的同名方法,this 绑定当前子类实例

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

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

✅ Day 5 总结打卡

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

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

在千层原型链、闭包作用域、上下文栈、V8 执行模型的交错中
能准确找到变量和属性的来源
能合理设计继承链避免性能陷阱
能解释 JS 为什么是世界上最“灵活”的语言之一!