深入理解 Promise 与 JavaScript 原型链:从执行顺序到对象继承

47 阅读4分钟

在前端开发中,Promise 是处理异步操作的核心工具,而 JavaScript 的 原型链机制 则是其面向对象特性的基石。本文将结合一段典型代码,深入剖析 Promise 的执行流程、状态变化,并同步讲解对象的原型继承关系,帮助你打通异步编程与面向对象两大核心概念。


一、Promise 执行流程详解

先看这段代码:

html
预览
<script>
const p = new Promise((resolve, reject) => {
  console.log(111);
  setTimeout(() => {
    console.log(333);
    reject('结果2');
  }, 1000);
});
console.log(222);
console.log(p, '////'); // 此时 p 仍为 pending
p.then(data => console.log(data))
 .catch(err => console.log(err))
 .finally(() => console.log('finally'));
</script>

控制台输出顺序:

text
编辑
111
222
Promise {<pending>} //// 
333
结果2
finally

关键点解析:

  • executor(执行器)是同步执行的
    new Promise(...) 中传入的函数会立即执行,因此先输出 111
  • Promise 状态初始为 pending
    setTimeout 触发前,p 的状态仍是 pending,所以 console.log(p) 显示 <pending>
  • .then().catch() 是异步注册的回调
    即使 reject 在 1 秒后才调用,.catch() 也能捕获错误,因为 Promise 内部会“记住”状态变化,并在状态确定后触发对应的回调。
  • .finally() 总是执行
    无论成功或失败,finally 都会运行,常用于清理资源(如关闭 loading 动画)。

注意resolve/reject 只能改变一次状态,后续调用无效。


二、JavaScript 原型链:实例、原型与 constructor

再看这段面向对象的代码:

javascript
编辑
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.special = '人类';
let zhen = new Person('zhen', 18);
console.log(zhen.special); // '人类'

const kong = {
  name: '孔子',
  hobbies: ['books', 'music'],
};

// 强制修改原型
zhen.__proto__ = kong;
console.log(zhen.hobbies, zhen.special); // ['books', 'music'] undefined

输出解释:

  • 初始时,zhen.__proto__ === Person.prototype,所以能访问 special
  • 手动赋值 zhen.__proto__ = kong 后,原型链被切断,指向 kong 对象。
  • 因此 zhen.hobbies 可读(来自 kong),但 zhen.special 变为 undefinedkong 没有该属性)。

核心概念梳理:

概念说明
实例对象(如 zhen通过 new 创建的对象,拥有自身属性(nameage
原型对象(如 Person.prototype构造函数的 prototype 属性,所有实例共享其方法/属性
__proto__实例的内部指针,指向其构造函数的 prototype(非标准但广泛支持)
constructor原型对象上的属性,指向构造函数本身(如 Person.prototype.constructor === Person

重要规则:

  • 原型链查找:访问属性时,先查自身,再沿 __proto__ 向上查找。

  • 修改原型对象会影响所有实例(若未重写 __proto__):

    js
    编辑
    Person.prototype.sayHi = function() { console.log('Hi'); };
    zhen.sayHi(); // 可调用
    
  • 直接替换整个 prototype 会丢失 constructor

    js
    编辑
    Person.prototype = { greet: 'hello' }; // ❌ constructor 指向 Object
    // 应手动修复:
    Person.prototype.constructor = Person; // ✅
    

⚠️ 不推荐直接修改 __proto__
虽然可行,但性能差且破坏封装性。应使用 Object.setPrototypeOf()(仍不推荐)或设计良好的继承结构。


三、Promise.all:批量处理异步任务

虽然你的代码未使用 Promise.all,但作为 Promise 的重要静态方法,值得简要介绍。

js
编辑
const p1 = Promise.resolve(1);
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 500));
const p3 = fetch('/api/data'); // 假设返回 Promise

Promise.all([p1, p2, p3])
  .then(results => console.log(results)) // [1, 2, response]
  .catch(err => console.error('任一失败:', err));

特点:

  • 全部成功才成功:任一 Promise reject,整体立即 reject
  • 保持顺序:结果数组顺序与输入数组一致。
  • 并发执行:所有 Promise 同时开始,非串行。

替代方案对比:

方法行为适用场景
Promise.all全成功才成功表单提交需所有字段验证通过
Promise.allSettled等待所有完成(无论成败)批量请求,需知道每个结果
Promise.race返回第一个完成的超时控制(如 race(fetch(), timeoutPromise)

四、总结要点

Promise 关键点

  • executor 同步执行,状态一旦变更不可逆。
  • .then/.catch 是微任务,会在当前宏任务结束后、下一轮事件循环前执行。
  • .finally 不接收参数,不影响 Promise 链的值传递。

原型链关键点

  • 实例通过 __proto__ 链接到原型对象。
  • 原型对象的 constructor 应指向构造函数(避免意外覆盖)。
  • “面向对象”在 JS 中本质是“委托行为” ,不是类继承。

最佳实践

  • 避免直接操作 __proto__,优先使用 class 或 Object.create()
  • 使用 Promise.allSettled 处理可容忍部分失败的批量请求。
  • 始终在 .catch 中处理错误,防止未捕获的 rejection。

五、拓展思考

  1. 如果 reject 后没有 .catch 会怎样?
    浏览器会抛出 “Uncaught (in promise)” 警告,但不会中断脚本(与 throw 不同)。

  2. 能否多次调用 resolvereject
    可以,但只有第一次有效,后续调用被忽略。

  3. zhen.special 为什么变成 undefined 而不是报错?
    JS 属性访问不存在时返回 undefined,这是语言设计特性,需用 hasOwnPropertyin 判断存在性。

  4. 现代替代方案

    • 用 async/await 替代 .then 链,提升可读性。
    • 用 class 语法替代构造函数 + prototype,更接近传统 OOP。