阅读视图

发现新文章,点击刷新页面。

🧠 深入理解 JavaScript Promise 与 `Promise.all`:从原型链到异步编程实战

在现代 JavaScript 开发中,Promise 是处理异步操作的核心机制之一。ES6 引入的 Promise 极大地简化了“回调地狱”(Callback Hell)问题,并为后续的 async/await 语法奠定了基础。而 Promise.all 则是并发执行多个异步任务并统一处理结果的强大工具。

本文将结合 原型链原理Promise 基础用法实际示例代码,带你系统掌握 Promise 及其静态方法 Promise.all 的使用与底层逻辑。


🔗 一、JavaScript 的面向对象:原型链而非“血缘”

在深入 Promise 之前,我们先厘清一个关键概念:JavaScript 的继承不是基于“类”的血缘关系,而是基于原型(prototype)的链式查找机制

1.1 🏗️ 构造函数与原型对象

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

Person.prototype.speci = '人类';

let zhen = new Person('张三', 18);
console.log(zhen.speci); // 输出: "人类"
  • Person 是构造函数。
  • Person.prototype 是所有 Person 实例共享的原型对象。
  • zhen.__proto__ 指向 Person.prototype
  • Person.prototype.constructor 又指回 Person,形成闭环。

🚂 小比喻:可以把 constructor 看作“车头”,prototype 是“车身”。实例通过 __proto__ 连接到车身,而车身知道自己的车头是谁。

1.2 ⚡ 动态修改原型链(不推荐)

const kong = {
    name: '孔子',
    hobbies: ['读书', '喝酒']
};

zhen.__proto__ = kong;
console.log(zhen.hobbies);     // ✅ 输出: ['读书', '喝酒']
console.log(kong.prototype);   // ❌ undefined!普通对象没有 prototype 属性

⚠️ 注意:

  • 只有函数才有 prototype 属性;
  • 普通对象(如 kong)只有 __proto__,没有 prototype
  • 在这里kong是object的一个实例kong.__prpto__ == object.prototype

💡 虽然可以动态修改 __proto__,但会破坏代码可预测性,影响性能,应避免使用。


⏳ 二、Promise:ES6 的异步解决方案

2.1 🧩 Promise 基本结构

<script>
const p = new Promise((resolve, reject) => {
    console.log(111); // 同步执行
    setTimeout(() => {
        console.log(333);
        // resolve('结果1');  // 成功
        reject('失败1');      // 失败
    }, 1000);
});

console.log(222);
console.log(p, '////////'); // 此时 p 状态仍是 pending
console.log(p.__proto__ == Promise.prototype); // true
</script>

📋 执行顺序分析:

  1. 111 立即输出(executor 函数同步执行)✅
  2. 222 紧接着输出 ✅
  3. p 此时处于 pending(等待) 状态 ⏳
  4. 1 秒后,333 输出,调用 reject('失败1'),状态变为 rejected
  5. .catch() 捕获错误,.finally() 无论成功失败都会执行 🔁

2.2 🎯 Promise 的三种状态

  • ⏳ pending:初始状态,既不是成功也不是失败。
  • ✅ fulfilled:操作成功完成(通过 resolve 触发)。
  • ❌ rejected:操作失败(通过 reject 触发)。

🔒 核心特性:一旦状态改变,就不可逆。这是 Promise 的设计基石。

2.3 🔍 原型关系验证

console.log(p.__proto__ === Promise.prototype); // ✅ true
  • pPromise 的实例。
  • 所有 Promise 实例的 __proto__ 都指向 Promise.prototype
  • Promise.prototype 上定义了 .then(), .catch(), .finally() 等方法。
  • Promise.prototype.__proto__ == object.prototype

🚀 三、Promise.all:并发处理多个异步任务

3.1 ❓ 什么是 Promise.all

Promise.all(iterable) 接收一个可迭代对象(如数组),其中包含多个 Promise。它返回一个新的 Promise:

  • ✅ 全部成功 → 返回一个包含所有结果的数组(顺序与输入一致)。
  • ❌ 任一失败 → 立即 rejected,返回第一个失败的原因。

3.2 💻 使用示例

const task1 = fetch('/api/user');       // 假设返回 { id: 1, name: 'Alice' }
const task2 = fetch('/api/posts');      // 假设返回 [{ title: 'JS' }]
const task3 = new Promise(resolve => setTimeout(() => resolve('done'), 500));

Promise.all([task1, task2, task3])
  .then(([user, posts, msg]) => {
    console.log('全部完成:', user, posts, msg);
  })
  .catch(err => {
    console.error('某个任务失败:', err);
  });

🌐 适用场景:需要同时加载用户信息、文章列表、配置数据等,全部就绪后再渲染页面。

3.3 ⚠️ 错误处理演示

const p1 = Promise.resolve('成功1');
const p2 = Promise.reject('失败2');
const p3 = Promise.resolve('成功3');

Promise.all([p1, p2, p3])
  .then(results => console.log('不会执行'))
  .catch(err => console.log('捕获错误:', err)); // 输出: "失败2"

关键点:只要有一个失败,整个 Promise.all 就失败,其余成功的 Promise 结果会被丢弃。

3.4 🛡️ 替代方案:Promise.allSettled(ES2020)

如果你希望无论成功失败都等待所有任务完成,可以使用 Promise.allSettled

Promise.allSettled([p1, p2, p3])
  .then(results => {
    results.forEach((res, i) => {
      if (res.status === 'fulfilled') {
        console.log(`✅ 任务${i} 成功:`, res.value);
      } else {
        console.log(`❌ 任务${i} 失败:`, res.reason);
      }
    });
  });

✅ 适用于:批量上传、日志收集、非关键资源加载等场景。


📚 四、总结:从原型到实践

概念 说明
🔗 原型链 JS 对象通过 __proto__ 查找属性,constructor 指回构造函数
Promise 表示异步操作的最终完成或失败,具有 pending/fulfilled/rejected 三种状态
🧩 Promise.prototype 所有 Promise 实例的方法来源(.then, .catch 等)
🚀 Promise.all 并发执行多个 Promise,全成功则成功,任一失败则整体失败
🛡️ 最佳实践 使用 Promise.all 提升性能;用 allSettled 处理非关键任务

💭 五、思考题

  1. 🤔 为什么 console.log(p)setTimeout 之前打印时,状态是 pending
  2. 🛠️ 能否通过修改 Promise.prototype.then 来全局拦截所有 Promise 的成功回调?这样做有什么风险?
  3. 📦 如果 Promise.all 中传入空数组 [],结果会是什么?

💡 答案提示

  1. 因为异步任务尚未执行,状态未改变。
  2. 技术上可行,但会破坏封装性、可测试性和团队协作,强烈不推荐
  3. 立即 resolved,返回空数组 [] —— 这是符合规范的!

通过本文,你不仅掌握了 PromisePromise.all 的用法,还理解了其背后的 原型机制异步执行模型。这将为你编写健壮、高效的异步代码打下坚实基础。🌟

Happy Coding! 💻✨

🧱 深入理解栈(Stack):原理、实现与实战应用

一、什么是栈?🤔

(Stack)是一种经典的线性数据结构,其核心特性是 “先进后出” (Last In First Out, LIFO)。
你可以把它想象成一摞盘子🍽️:每次只能从顶部放入或取出盘子,最晚放进去的盘子最先被拿出来。

在计算机科学中,栈广泛应用于以下场景:

  • 🔁 函数调用(调用栈)
  • 🧮 表达式求值与转换(如中缀转后缀)
  • ✅ 括号匹配验证
  • 🖥️ 浏览器前进/后退历史
  • 🔄 撤销(Undo)操作

💡 栈的核心思想:只操作一端(栈顶),另一端封闭


二、栈的抽象数据类型(ADT)🧩

一个标准的栈应具备以下属性和方法

方法 / 属性 说明
push(item) ➕ 入栈:将元素压入栈顶
pop() ➖ 出栈:移除并返回栈顶元素
peek() / top() 👀 查看栈顶元素但不移除
isEmpty() ❓ 判断栈是否为空
size 🔢 获取栈中元素数量
toArray()(可选) 📤 将栈内容转为数组(用于调试或展示)

⚠️ 注意:栈不允许随机访问中间元素,只能操作栈顶


三、ES6 Class 与栈的封装 🛠️

ES6 引入了 class 语法,使面向对象编程更清晰。结合私有字段(#)、get/set 访问器等新特性,我们可以优雅地实现栈。

下面深入解析这些关键特性👇:


1️⃣ class:定义类的模板 📐

在 ES6 之前,JavaScript 通过构造函数 + 原型链模拟类:

// 🕰️ ES5 风格
function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() {
  console.log('Hello, ' + this.name);
};

ES6 的 class 是对上述模式的语法糖,但结构更清晰、更接近传统 OOP:

// ✨ ES6 class
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log('Hello, ' + this.name);
  }
}

关键点

  • 底层仍基于 原型(prototype)
  • 提供声明式、结构化的代码组织方式
  • 显著提升可读性与可维护性,尤其适合大型项目

2️⃣ #privateField:私有属性 🔒

传统 JS 中所有属性都是公开的,容易被外部篡改:

class Counter {
  constructor() {
    this.count = 0; // 😱 外部可随意修改!
  }
}
const c = new Counter();
c.count = 999; // 破坏封装!

ES2022 引入 私有字段(以 # 开头),仅限类内部访问:

class Counter {
  #count = 0; // 🔒 私有属性

  increment() { this.#count++; }
  getCount() { return this.#count; } // ✅ 安全暴露
}

const c = new Counter();
c.increment();
console.log(c.getCount()); // 1

// c.#count; // ❌ SyntaxError!

优势

  • 封装性:隐藏实现细节
  • 安全性:防止外部误操作
  • 可维护性:内部逻辑变更不影响外部调用

⚠️ 私有字段必须显式声明,不能动态添加。


3️⃣ constructor():初始化实例 🧬

constructor 是类的构造函数,在 new 实例时自动调用:

class Stack {
  #items;
  constructor(initialItems = []) {
    this.#items = [...initialItems]; // 初始化私有数组
  }
}

作用

  • 初始化实例属性(包括私有属性)
  • 接收参数设置初始状态
  • 若未定义,JS 会提供空默认构造函数

🔁 注意:每个类最多只能有一个 constructor


4️⃣ get size():只读属性访问器 📏

有时我们希望暴露某个值,但禁止修改。这时可用 get 定义访问器:

class ArrayStack {
  #stack = [];
  get size() {
    return this.#stack.length; // 📊 像读属性一样使用
  }
}

const stack = new ArrayStack();
console.log(stack.size); // 0
// stack.size = 10; // ❌ 无效(严格模式报错)

好处

  • 语义清晰:size 看似属性,实为计算值
  • 可加入校验、日志、缓存等逻辑
  • 实现只读接口,避免误写

💡 同理,set 可拦截赋值:

set maxSize(value) {
  if (value < 0) throw new Error('maxSize 不能为负');
  this._maxSize = value;
}

5️⃣ 方法共享于原型链,节省内存 🧠

这是 class 最重要的性能优势!

所有实例方法(非静态、非箭头函数)都定义在类的原型上:

class Stack {
  push() { /* ... */ }
  pop() { /* ... */ }
}

const s1 = new Stack();
const s2 = new Stack();

console.log(s1.push === s2.push); // ✅ true!

这意味着

  • 方法只在内存中存在一份
  • 所有实例通过原型链共享方法
  • 极大节省内存,尤其适合创建大量对象(如游戏实体、UI 组件)

❌ 对比反模式(ES5 常见陷阱):

function BadStack() {
  this.push = function() { /* 每次 new 都新建函数!*/ };
}

📌 建议:现代项目优先使用 ES6+ class,善用私有字段与访问器,构建高内聚、低耦合的组件。


四、两种实现方式:数组 vs 链表 ⚖️

栈可以用数组链表实现,各有优劣:


1️⃣ 基于数组的栈(ArrayStack)📦

class ArrayStack {
  #stack = [];
  get size() { return this.#stack.length; }
  isEmpty() { return this.size === 0; }
  push(num) { this.#stack.push(num); }
  pop() {
    if (this.isEmpty()) throw new Error('栈为空');
    return this.#stack.pop();
  }
  peek() {
    if (this.isEmpty()) throw new Error('栈为空');
    return this.#stack[this.size - 1];
  }
  toArray() { return [...this.#stack]; }
}

✅ 优点:

  • 时间效率高push/pop 在尾部操作,平均 O(1)
  • 内存连续,缓存友好(CPU 更快访问)
  • 代码简洁,JS 数组原生支持

❌ 缺点:

  • 扩容成本高:容量不足时需复制所有元素 → O(n)
  • 可能存在空间浪费(预分配未用完)

💡 实际中,扩容是低频事件均摊时间复杂度仍为 O(1)


2️⃣ 基于链表的栈(LinkedListStack)⛓️

class ListNode {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}

class LinkedListStack {
  #stackPeek = null;
  #size = 0;

  get size() { return this.#size; }
  isEmpty() { return this.size === 0; }

  push(num) {
    const node = new ListNode(num);
    node.next = this.#stackPeek;
    this.#stackPeek = node;
    this.#size++;
  }

  peek() {
    if (!this.#stackPeek) throw new Error('栈为空');
    return this.#stackPeek.val;
  }

  pop() {
    const num = this.peek();
    this.#stackPeek = this.#stackPeek.next;
    this.#size--;
    return num;
  }

  toArray() {
    const arr = new Array(this.size);
    let node = this.#stackPeek;
    let i = this.size - 1;
    while (node) {
      arr[i--] = node.val;
      node = node.next;
    }
    return arr;
  }
}

✅ 优点:

  • 动态扩容:每次插入只需 O(1) ,无复制开销
  • 空间按需分配,无浪费

❌ 缺点:

  • 每个节点需额外存储 next 指针 → 内存开销更大
  • 节点在内存中离散分布 → 缓存局部性差
  • 实例化 ListNode 有一定性能损耗

总结

  • 🚀 日常开发、轻量场景 → 数组实现
  • 🏗️ 大数据、稳定性要求高 → 链表实现

五、实战应用:有效的括号匹配 ✅

栈的经典应用场景之一!

📌 问题描述:

给定字符串 s,仅含 '(', ')', '[', ']', '{', '}',判断是否有效:

  • 左右括号必须正确闭合
  • 顺序必须匹配(如 "([)]" ❌ 无效)

🧠 解题思路:

  1. 遇到左括号 → 将其对应的右括号压入栈
  2. 遇到右括号 → 检查是否与栈顶匹配
  3. 遍历结束 → 栈必须为空

💻 代码实现:

const leftToRight = {
  "(": ")",
  "[": "]",
  "{": "}",
};

function isValid(s) {
  if (!s) return true;
  const stack = [];
  for (let ch of s) {
    if (ch in leftToRight) {
      stack.push(leftToRight[ch]); // 压入期望的右括号
    } else {
      if (!stack.length || stack.pop() !== ch) {
        return false; // 不匹配或栈空
      }
    }
  }
  return stack.length === 0; // 栈空则有效
}

🧪 测试

console.log(isValid("()"));       // ✅ true
console.log(isValid("()[]{}"));   // ✅ true
console.log(isValid("(]"));       // ❌ false
console.log(isValid("([)]"));     // ❌ false
console.log(isValid("{[]}"));     // ✅ true

🔍 为什么压入“右括号”?
这样遇到右括号时可直接比较 stack.pop() === ch无需二次查表,逻辑更简洁高效!


六、总结对比 📊

维度 数组栈 📦 链表栈 ⛓️
时间复杂度(平均) O(1) O(1)
扩容开销 O(n)(低频) O(1)
空间效率 可能浪费 指针开销(约 +50%)
实现难度 ⭐ 简单 ⭐⭐ 中等
适用场景 通用、轻量级 大数据、稳定性要求高

🎯 结语

栈虽简单,却是理解程序运行机制(如调用栈)和解决算法问题(DFS、表达式解析、回溯)的基石数据结构

掌握其两种实现方式及典型应用,不仅能写出更高效的代码,还能在面试中展现扎实的基本功!

📌 终极建议

  • 日常开发 → 优先用 数组实现(简单高效)
  • 面试/性能敏感场景 → 主动讨论 链表方案,展现深度思考 💡

📚 延伸思考:你能用栈实现“浏览器后退”功能吗?或者用两个栈实现一个队列?欢迎动手尝试!

❌