从「似懂非懂」到「了如指掌」:Promise 与原型链全维度拆解

前言

在前端世界里,Promise原型链(Prototype) 是两个看似毫不相干,却又互相影响、甚至能相互解释的重要概念。

很多人学习 Promise 时,会关注它的使用:thencatchfinallyPromise.all 等;

学习原型链时,又会关注 __proto__prototype、构造函数与实例之间的关系。

但鲜有人把 Promise 本身也是一个对象,它也依赖原型链运作 这件事真正联系起来讲透。

本文将以一次完整的 Promise 异步流程为主线,把"原型链 + 状态机 + 微任务"融合讲解,让你完全理解 Promise 到底是怎么在底层"跑"起来的。


一、Promise 为什么是"对象"?

我们常常写:

javascript 复制代码
const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve('OK'), 1000)
})

很多人知道 Promise 是"异步解决方案",但忽略了一个基本事实:

Promise 是一个构造函数(类),你创建的是它的实例。

也就是说:

  • Promise ------ 构造函数(带 prototype
  • p ------ 实例对象(带 __proto__

打开控制台试试:

javascript 复制代码
console.log(p.__proto__ === Promise.prototype) // true

这里马上就把原型链扯进来了。

🔍 Promise.prototype 上都有啥?

输入:

javascript 复制代码
console.log(Promise.prototype)

你会看到:

java 复制代码
then: ƒ then()
catch: ƒ catch()
finally: ƒ finally()
constructor: ƒ Promise()
...

这说明:

所有 Promise 实例都是通过原型链访问 then/catch/finally 的。

也就是说 p.then() 并不是实例自身有,而是:

javascript 复制代码
p ---> Promise.prototype ---> Object.prototype ---> null

这为后文理解 Promise "链式调用"机制奠定基础。


二、原型链视角下,看懂 Promise 的执行流

我们直接看一个你提供的代码精简版:

javascript 复制代码
const p = new Promise((resolve, reject) => {
  console.log(111)
  setTimeout(() => {
    reject('失败1')
  }, 1000)
})

console.log(222)

p.then(data => {
  console.log(data)
}).catch(err => {
  console.log(err)
}).finally(() => {
  console.log('finally')
})

输出顺序:

csharp 复制代码
111
222
失败1
finally

要理解为什么 Promise 能这样执行,必须从两个角度讲:

  • (1)Promise 内部是状态机(pending → fulfilled / rejected)
  • (2)then/catch/finally 是通过原型链挂载的"回调注册器"

我们分开看看。


1)Promise 内部是一个状态机

内部状态(无法手动修改):

状态 描述 何时出现
pending 初始状态 执行 executor 期间
fulfilled resolve 被调用 成功
rejected reject 被调用 失败

也就是说:

javascript 复制代码
new Promise(executor)

执行后:

  • 立即执行 executor
  • executor 只在同步阶段运行
  • 真正的 resolve/reject 回调是"挂起来",等事件循环驱动

所以你看到:

csharp 复制代码
111(executor 同步执行)
222(外部同步执行)
失败1(异步到点后 reject)
finally(状态 settled 后触发)

2)then/catch/finally:它们不是魔法,是原型链的方法

看看这段链式调用:

scss 复制代码
p.then(...).catch(...).finally(...)

为什么可以一直"链式"?

因为每次调用 then 都 返回一个新的 Promise 实例

scss 复制代码
p.then(...) → p2
p2.catch(...) → p3
p3.finally(...) → p4

这几个实例的原型链依然是:

ini 复制代码
p2.__proto__ === Promise.prototype
p3.__proto__ === Promise.prototype
...

因此:

链式本质 = 每次链式都返回一个新的 Promise 实例,然后继续在原型链上查找 then/catch/finally。

这就是原型链在 Promise 底层的重要性。


三、原型链的类比:Promise 就像"火车头 + 车厢"系统

你提到的类比非常棒,我把它整理成完整模型:

✨ Promise = 火车系统

  • 构造函数(Promise) = 火车制造厂
  • 原型对象(Promise.prototype) = "火车车厢模板"
  • 实例(p) = 火车头
  • then/catch/finally = 可以接在车头后的"车厢类型"

于是我们看到:

csharp 复制代码
p(车头).then(挂一个车厢)
         .then(再挂一节)
         .catch(挂一个处理失败的车厢)
         .finally(挂尾部的清理车厢)

每次挂车厢(调用 then/catch)时,都会生成 新的火车车头(新的 Promise 实例)

整个火车最终沿着轨道(事件循环)开动到终点。

⚠️ 注意:为什么 finally 一定执行?

因为 finally 不关心结果,只关心火车是否开到终点(settled)。


四、Promise 与普通对象原型链的对比

你提供了一个经典例子:

ini 复制代码
function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.speci = '人类'

let zhen = new Person('白兰地空瓶', 18)
console.log(zhen.speci)

const kong = {
  name: '空瓶',
  hobbies: ['读书', '喝酒']
}

zhen.__proto__ = kong

console.log(zhen.hobbies, zhen.speci)

输出:

css 复制代码
人类
['读书','喝酒'] undefined

这个例子非常适合用来对比 Promise 的原型链逻辑。

对比 1:实例可以动态改原型(不推荐)

zhen.__proto__ = kong 改掉了原来的 Person.prototype

所以:

  • 能访问 hobbies:因为来自 kong
  • 不能访问 speci:因为已脱离 Person.prototype

Promise 则不能做这种事

你不能这样做:

ini 复制代码
p.__proto__ = {}

否则:

css 复制代码
p.then is not a function

因为 then/catch/finally 都来自 Promise.prototype。

这反而让我们更清楚地理解:

Promise 的能力几乎全部来自原型链。


五、Promise.all 的底层逻辑:一辆多车头的"联挂火车"

提到 Promise.all,这里正好顺便讲讲它的底层设计。

css 复制代码
Promise.all([p1, p2, p3])

机制可以用一个形象类比解释:

  • 假设有三辆火车(p1/p2/p3)
  • Promise.all 创建一辆"总火车头" pAll
  • pAll 盯着三个火车头,只要全部变成 fulfilled,就把所有结果一次性返回
  • 如果有一个 reject,则整个 pAll 变成 rejected(列车脱轨)

也就是说:

Promise.all = 多个 Promise 状态机的并联 + 一个新的总状态机。

为什么它能做到?

答案依旧在原型链:

  • Promise.all 本质是一个静态方法,返回新的 Promise 实例
  • 新的 Promise 实例依然沿用同一套路(prototype → then/catch)

六、用真实工程场景收尾:Promise 原型链为什么重要?

在真实项目里,理解 Promise 的原型机制有三个实际价值:

① debugger 时能看清原型链,定位异步回调来源

你能区分:

  • then 回调从哪里来的?(Promise.prototype.then)
  • promise 链断在哪一层?

② 手写 Promise 时必须实现 then/catch/finally

如果你手写 Promise A+:

ini 复制代码
MyPromise.prototype.then = function(onFulfilled, onRejected) {}

这里你就必须自己处理链式、状态机、回调队列。

③ 能理解 async/await 的底层依赖 Promise 链式调度

await 会把后续步骤注册到 promise.then 中。

理解 then 的原型链,就能理解 async/await 的机制本质。


七、总结:Promise + 原型链的全景图

javascript 复制代码
// 创建实例
const p = new Promise(executor)

// 原型链:调用能力来自这里
p.__proto__ = Promise.prototype

// 状态机:内部维护 pending → fulfilled/rejected

// then/catch/finally:注册微任务

// 链式调用:每次都返回一个新的 Promise 实例

// Promise.all:多个状态机的并联

一句话总结:

Promise 本质是一个基于"原型链 + 状态机 + 微任务队列"的异步调度框架。

它既是面向对象设计(通过原型链复用方法),又是异步控制核心工具(内部状态机)。

理解二者的融合,你就真正吃透了 Promise。

相关推荐
Nan_Shu_61417 小时前
学习: Threejs (2)
前端·javascript·学习
G_G#17 小时前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界17 小时前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路18 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug18 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213818 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中18 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路18 小时前
GDAL 实现矢量合并
前端
hxjhnct18 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星18 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript