【一文读懂】对原型链、事件循环、异步操作(Promise、async/await)、递归栈溢出的理解及其衍生问题

👉 个人博客主页 👈
📝 一个努力学习的程序猿


更多文章/专栏推荐:
HTML和CSS
JavaScript
Vue
Vue3
React
TypeScript
个人经历+面经+学习路线【内含免费下载初级前端面试题】
前端面试分享
前端学习+方案分享(VitePress、html2canvas+jspdf、vuedraggable、videojs)
前端踩坑日记(ElementUI)
【一文读懂】对闭包的理解及其衍生问题


前言

本文主要将回答以下问题:原型(链)、事件循环(宏任务、微任务)、异步操作(Promist、async/await)、递归以及解决递归栈溢出的方法。


1、原型+原型链

1.1 箭头函数和function函数

在进入正式内容前,先表述几个其他问题。箭头函数和 function 函数相信大家都不陌生。ES6 新增的箭头函数,主要就是提供更简洁的函数语法,且解决传统函数中 this 绑定的问题。

示例:

// 传统函数
function traditionalFunc(x) {
  return x * 2;
}
 
// 箭头函数
const arrowFunc = x => x * 2;

箭头函数和传统的 function 函数有以下主要区别

(1)this 绑定(this 指向问题,详见1.3节):
箭头函数没有自己的 this,它会捕获其所在上下文的 this 值。
传统函数的 this 由调用方式决定。

(2)构造函数(构造函数,详见1.4节):
箭头函数不能用作构造函数(不能使用new)。
传统函数可以作为构造函数。

(3)arguments 对象(详见1.2节):
箭头函数没有自己的 arguments 对象。
传统函数有自己的 arguments 对象。

(4)隐式返回
箭头函数在只有一个表达式时可以省略 return 和花括号。
传统函数总是需要显式的 return。

(5)prototype 属性(原型属性,详见1.5节):
箭头函数没有 prototype 属性。
传统函数有 prototype 属性。

相关具体内容将在下方按节叙述。


1.2 arguments 对象

arguments 对象是 JavaScript 中的一个特殊对象,被称为类数组对象。它存在于所有非箭头函数中,包含了传递给函数的所有参数,不需要显式声明,可以直接使用。

示例:

function example(a, b, c) {
  console.log(arguments.length);         // 输出参数的数量
  console.log(arguments[0]);             // 可以通过索引访问
  console.log(Array.isArray(arguments)); // false
  console.log(typeof arguments);         // object
}
 
example(1, 2, 3);

通过示例可以看出它的特殊点:
有数组的一部分特性,但又不完全是数组:有length属性,可以通过索引访问元素,但不是 Array 的实例(没有数组的内置方法,如 push、pop、slice…)。

如果你想把它当作数组使用,你只需要转化它为真正的数组即可:

function example() {
  const args = Array.from(arguments);
  // 或者
  // const args = [...arguments];
  
  args.forEach(arg => console.log(arg)); // 现在可以使用数组方法了
}

而为什么 arguments 不设计成真正的数组,或许是因为创建真正的数组会带来额外的内存和性能开销,且 arguments 对象的设计早于许多现代的 JS 特性。

使用场景:在不知道具体参数数量的情况下操作所有参数

不过现在如果要实现这样的功能,都会用如下的现代替代方案,这会让语法更加清晰和直观:

function modern(...args) {
  console.log(args);  // 是一个真正的数组
}

1.3 修改this的指向:call / apply / bind

(1)call(thisArg, arg1, arg2, …)
• 立即执行函数
• 第一个参数指定this,后续参数作为函数参数
• 适用于:明确知道参数数量,且需要借用其他对象的方法

function greet(name) {
 console.log(`Hello, ${name}! I'm ${this.name}`);
}
const person = { name: 'Alice' };
greet.call(person, 'Bob'); // 输出: Hello, Bob! I'm Alice

(2)apply(thisArg, [arg1, arg2, …])
• 与 call 类似,但第二个参数是数组
• 适用于:参数已经存在于数组的情况

function greet(name) {
 console.log(`Hello, ${name}! I'm ${this.name}`);
}
const person = { name: 'Alice' };
greet.apply(person, ['Bob']); // 输出: Hello, Bob! I'm Alice

const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers); // 返回 7

(3)bind(thisArg, arg1, arg2, …)
返回一个新函数,不立即执行
• 适用于:需要创建一个永久绑定特定 this 值的函数

function greet(name) {
 console.log(`Hello, ${name}! I'm ${this.name}`);
}
const person = { name: 'Alice' };
const boundGreet = greet.bind(person);
boundGreet('Bob'); // 输出: Hello, Bob! I'm Alice

const module = {
 x: 42,
 getX: function() {
   return this.x;
 }
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // 报错:TypeError: Cannot read properties of undefined (reading 'x')(因为 this 不存在,是undefined)

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42

1.4 构造函数

示例:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
 
const alice = new Person('Alice', 30);
console.log(alice); // Person { name: 'Alice', age: 30 }

在这里插入图片描述

分析下示例:
(1)console.log 中可以看到,它有 Prototype 属性,这将在1.5节中说明;
(2)在使用时,需要大写函数名,且使用 new 关键字,这将帮我们自动创建新对象并设置原型链(你可以暂时理解为出现了 Prototype),并确保 this 指向新创建的对象

=> 我们在 1.3 节关于 bind 的示例中可以发现,如果是个普通函数,你在不指定 this 时,this 将是 undefined。

使用构造函数可以更方便地创建具有相同结构的多个对象实例,并且可以通过原型链共享方法,提高代码的复用性和效率。


1.5 原型与原型链

接下来是最重要的内容,上述所有问题中,大部分都涉及到原型链,且也是原型链的相关基础。

原型是 JavaScript 中一个非常重要的概念,每个 JavaScript 对象都有一个原型对象。接下来用构造函数来演示:

// 创建一个 person 构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}
// 默认拥有 prototype 属性
// Person.prototype = { constructor: Person };
 
// 在 Person 的原型上添加一个方法
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};
 
// 创建一个 Person 实例
const john = new Person("John", 30);
// 相当于
// john = {};
// john.__proto__ = Person.prototype;
// Person.call(john, “John”, 30);
 
// john的原型链
// john { name: “John”, age: 30 } ---> Person.prototype ---> Object.prototype ---> null
 
john.sayHello(); // 输出: Hello, my name is John

在这个例子中:
• Person.prototype 是 Person 构造函数的原型对象
• sayHello 方法被添加到 Person.prototype 上
• john 对象可以直接访问 sayHello 方法,尽管它没有直接定义在 john 对象上

那么为什么没有直接定义在对象上,现在却也可以访问呢?原因如下:

当你试图访问一个对象的属性时,如果这个对象本身没有这个属性,JavaScript 会沿着原型链向上查找,直到找到该属性或到达原型链的末端 (通常是 Object.prototype)。

为了验证这一点,需要先介绍另一个概念“继承”。继承就是允许一个对象基于另一个对象(或类)来创建,继承其属性和方法。继承主要通过原型链来实现,实现的方式就是 1.3 节提到的 call。接下来扩展上面的例子。

// …
// 创建一个 Employee 构造函数,继承自 Person
function Employee(name, age, job) {
 Person.call(this, name, age);
 this.job = job;
}

// 设置 Employee 的原型为 Person 的一个实例
Employee.prototype = Object.create(Person.prototype);
// 因为重写了 prototype,所以需要重新设置,确保对象和构造函数之间关系正确
Employee.prototype.constructor = Employee;

// 在 Employee 的原型上添加一个方法
Employee.prototype.introduce = function() {
 console.log(`Hi, I'm ${this.name}, I work as a ${this.job}`);
};

// 创建一个 Employee 实例
const jane = new Employee("Jane", 25, "Developer");

jane.sayHello(); // 输出: Hello, my name is Jane
jane.introduce(); // 输出: Hi, I'm Jane, I work as a Developer

在这个例子中:
• Employee 继承自 Person

• Employee.prototype = Object.create(Person.prototype); 这里会出现一个现象:对 Person.prototype 的修改也会影响 Employee 的实例,而在 Employee.prototype 上添加或覆盖方法,不会影响 Person.prototype。

• 之所以需要 Employee.prototype.constructor = Employee; 主要是用于类型检查(不写这句话不会导致代码出错,但可能会在后续的开发中出现混淆和潜在的错误),比如:

console.log(jane.constructor === Person); // 如果不修正,这将返回 true
console.log(jane.constructor === Employee); // 如果不修正,这将返回 false
 
let newEmployee = new jane.constructor("Alice", 28, "Manager");
// 如果不修正,这将创建一个 Person 实例而不是 Employee 实例

• jane 对象可以访问 sayHello 方法 (来自 Person.prototype) 和 introduce 方法 (来自 Employee.prototype)

=> 当我们调用 jane.sayHello() 时,JavaScript 首先在 jane 对象上查找 sayHello 方法;因为没找到,所以它会在 jane 的原型 (Employee.prototype) 上查找;因为还没找到,它会继续在 Employee.prototype 的原型(Person.prototype) 上查找。最终在 Person.prototype 上找到 sayHello 方法并执行。示例图大致如下:
在这里插入图片描述


所有原型链的终点都是 Object.prototype。比如:

console.log(jane.__proto__); // Employee.prototype
console.log(jane.__proto__.__proto__); // Person.prototype
console.log(jane.__proto__.__proto__.__proto__); // Object.prototype
console.log(jane.__proto__.__proto__.__proto__.__proto__); // null

除此以外,还可以使用 Object.getPrototypeOf() 方法或 proto 属性检查对象的原型

console.log(Object.getPrototypeOf(jane) === Employee.prototype); // true
console.log(jane.__proto__ === Employee.prototype); // true

最后再给一个总结版的原型链与继承的例子:

function Animal(name) {
 this.name = name;
}
Animal.prototype.makeSound = function() {
 console.log('Some sound');
};

function Dog(name) {
 Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const dog = new Dog('Buddy');
dog.makeSound(); // 输出: Some sound

1.6 原型链的注意事项

在了解了原型链的基础概念后,需要注意:所有对象都有原型链:原型链的顶端是 Object.prototype。Object.prototype 的原型是 null。

示例:

const obj = {x: 10}
console.log(obj)
console.log(obj.toString())

在这里插入图片描述

toString 就是在原型链上的方法。这也就是为什么普通对象、数组对象及其他对象可以使用一个“看似”对象中不存在的方法,因为它们都在原型链上。这也就是原型链的好处:通过原型链这种共享方法,提高了代码的复用性和效率。(创建的每个对象,都有对应定义好的方法)


2、事件循环(宏任务、微任务)

感兴趣的话,可以先看一下我之前的其他文章,有助于你的理解:其中(互联网是如何运作的)目录

https://blog.youkuaiyun.com/qq_45613931/article/details/109028679

总结下就是:页面渲染 + JS 是单线程执行的。比如:

const num1 = 1 + 2 // 任务1
const num2 = 20 / 5 // 任务2
const num3 = 7 * 8 // 任务3

按顺序执行是我们认知中的。但如果出现需要耗费大量时间的函数(例如: setTimeout、 ajax)的时候,由于单线程的机制,理论上浏览器只能等待它们完成,从而被阻塞,导致浏览器停止渲染。但没出现这个问题,就是因为采用了异步回调函数的机制(事件循环机制)。

这个机制就是说:在代码从上到下执行的过程中,如果碰到了需要延后执行的任务,那么就会被存放到一个队列中(用来单独管理异步状态的事件队列)。在事件队列中,又会根据任务的类型分为微任务队列和宏任务队列

执行顺序就是:主线程代码先执行,执行过后,到事件队列中执行。在事件队列中,先去微任务队列中执行,没有微任务再去执行宏任务。执行过程中还会检查是否存在微任务,如果有就又会再去微任务队列执行。如此往复。这个流程就是事件循环,即eventloop。


宏任务包括:定时器(setTimeout、setInterval)、事件绑定、requestAnimationFrame、UI渲染、I/O操作

微任务包括:promise.then/catch/finally、 async/await、process.nextTick、MutationObserver
(如果对 Promise、async/await 不太了解,可以先跳转第3节)

现在用一个例子来验证这一点:

console.log('1. 脚本开始');
 
setTimeout(() => {
  console.log('2. 宏任务 (setTimeout)');
}, 0);
 
Promise.resolve().then(() => {
  console.log('3. 微任务 (Promise)');
});
 
console.log('4. 脚本结束');

输出结果为:

  1. 脚本开始
  2. 脚本结束
  3. 微任务 (Promise)
  4. 宏任务 (setTimeout)

执行流程:
• JavaScript 在开头碰到 console.log ,打印 1. 脚本开始

• 解析至 setTimeout,将其“回调”推入宏任务队列中执行

• 遇到 Promise,将其“回调”(也就是.then中的内容)推入微任务队列中执行

• 继续往下,碰到 console.log,打印 4. 脚本结束

• 至此,主线程空闲,开始从微任务队列里拿出任务,放入主线程执行。打印 3. 微任务 (Promise)

• 最后,主线程再次空闲,微任务队列为空,开始从宏任务队列里拿出任务,放入主线程执行。打印 2. 宏任务 (setTimeout)


注意上述关于“回调”的表述,现在写一个更复杂的例:

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
setTimeout(() => {
  console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
  console.log('promise1')
  resolve('resolve')
}).then(() => {
  console.log('promise2')
})
console.log('script end')

输出结果:

在这里插入图片描述

这里就需要注意执行顺序:

• JavaScript 先遇到两个 function 定义,随后碰到 console.log ,打印 script start

• 解析至 setTimeout,将其回调推入宏任务队列中执行

• 碰到调用 async1,因此进入 function 内,打印 async1 start。随后遇到 await async2 => 此时会先执行 async2 中的内容,打印 async2,随后其回调(也就是 await 后面的内容)会推入微任务队列

• 接下来遇到 Promise,执行其内容,打印 promise1 后,将其回调推入微任务队列(也就是 then 里的内容)

• 继续往下,打印 script end

• 至此,主线程空闲,开始从微任务队列里按照顺序拿出任务,放入主线程执行,依次打印:async1 end、promise2

• 最后,主线程再次空闲,微任务队列为空,开始从宏任务队列里拿出任务,放入主线程执行。打印 setTimeout


最后补一个问题:“Vue处理页面渲染卡顿问题”,也和以上内容息息相关。
https://blog.youkuaiyun.com/qq_45613931/article/details/143828634


3、Promise、async/await

3.1 Promise

通过第2节我们知道,Promise、async/await 都是微任务。它们的出现是因为页面的任务都在主线程上执行,如果耗时较长就会阻塞页面完成其他操作,所以为了提高性能,出现了异步操作,出现了事件循环机制。通常情况下,我们会用 Promise、async/await 处理接口请求。接下来看一个 Promise 示例:

// 模拟从API获取用户数据的函数
function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    // 模拟网络请求延迟
    setTimeout(() => {
      // 模拟成功和失败的情况
      if (userId === 1) {
        resolve({
          id: 1,
          name: "张三",
          email: "zhangsan@example.com"
        });
      } else {
        reject(new Error("用户不存在"));
      }
    }, 1000); // 1秒延迟
  });
}
 
// 使用Promise
console.log("开始获取用户数据...");
 
fetchUserData(1)
  .then(userData => {
    console.log("用户数据:", userData);
    return userData.name; // 可以继续链式调用
  })
  .then(name => {
    console.log("用户名:", name);
  })
  .catch(error => {
    console.error("错误:", error.message);
  })
  .finally(() => {
    console.log("数据获取操作完成");
  });
 
console.log("请求已发送,等待数据...");

输出结果:

开始获取用户数据…
请求已发送,等待数据…
用户数据: { id: 1, name: ‘张三’, email: ‘zhangsan@example.com’ }
用户名: 张三
数据获取操作完成


Promise 内,会有一个状态管理的机制,其中它有三种状态:

  1. Pending (待定)
    当 Promise 被创建时,它的初始状态是 Pending。表示异步操作尚未完成。
const promise = new Promise((resolve, reject) => {
  // 异步操作尚未完成,Promise处于Pending状态
});
  1. Fulfilled (已兑现)
    当异步操作成功完成时,Promise 从 Pending 转换为 Fulfilled。一旦变为 Fulfilled,状态就不能再改变。最后通过 .then 来处理后续内容。
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("操作成功"); // Promise变为Fulfilled状态
  }, 1000);
});
 
promise.then(value => console.log(value)); // 输出: 操作成功
  1. Rejected (已拒绝)
    当异步操作失败时,Promise 从 Pending 转换为 Rejected。一旦变为 Rejected,状态就不能再改变。最后通过 .catch 来处理后续内容。
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("操作失败")); // Promise变为Rejected状态
  }, 1000);
});
 
promise.catch(error => console.error(error.message)); // 输出: 操作失败

这里有几个关键特性:

(1)状态不可逆

一旦 Promise 的状态从 Pending 变为 Fulfilled 或 Rejected,就不能再改变。

const promise = new Promise((resolve, reject) => {
  resolve("成功");
  reject(new Error("失败")); // 这行不会有效果
});
 
promise.then(console.log); // 输出: 成功

(2)状态的传递

在Promise链中,状态会沿着链传递(错误会自动向下传播,直到被捕获)。

Promise.resolve(1)
  .then(value => value + 1)
  .then(value => { throw new Error("出错了"); })
  .then(value => console.log("不会执行"))
  .catch(error => console.error(error.message));

通过该特性你还可以发现,Promise 最大的好处就是通过一种更加线性和可读的方式,解决了传统回调函数嵌套代码导致的“回调地狱”。而且 catch 会统一处理异常,不用每次回调都重复处理。比如:

function getUserById(id, callback) {
  setTimeout(() => {
    console.log("获取用户信息...");
    callback({ id: id, name: "张三" });
  }, 1000);
}
 
function getOrdersByUser(user, callback) {
  setTimeout(() => {
    console.log("获取订单信息...");
    callback([
      { id: 1, amount: 200 },
      { id: 2, amount: 300 },
      { id: 3, amount: 500 }
    ]);
  }, 1000);
}
 
function calculateTotal(orders, callback) {
  setTimeout(() => {
    console.log("计算总额...");
    const total = orders.reduce((sum, order) => sum + order.amount, 0);
    callback(total);
  }, 1000);
}
 
// 回调地狱开始
getUserById(1, function(user) {
  console.log("用户:", user);
  getOrdersByUser(user, function(orders) {
    console.log("订单:", orders);
    calculateTotal(orders, function(total) {
      console.log("总金额:", total);
      // 可能还有更多嵌套...
    });
  });
});

改成 Promise 后:

// 将回调函数改造成返回Promise的函数
function getUserById(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("获取用户信息...");
      resolve({ id: id, name: "张三" });
    }, 1000);
  });
}
 
function getOrdersByUser(user) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("获取订单信息...");
      resolve([
        { id: 1, amount: 200 },
        { id: 2, amount: 300 },
        { id: 3, amount: 500 }
      ]);
    }, 1000);
  });
}
 
function calculateTotal(orders) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("计算总额...");
      const total = orders.reduce((sum, order) => sum + order.amount, 0);
      resolve(total);
    }, 1000);
  });
}
 
// 使用Promise链
getUserById(1)
  .then(user => {
    console.log("用户:", user);
    return getOrdersByUser(user);
  })
  .then(orders => {
    console.log("订单:", orders);
    return calculateTotal(orders);
  })
  .then(total => {
    console.log("总金额:", total);
  })
  .catch(error => {
    console.error("发生错误:", error);
  });

3.2 Promise.all

在上述例子中,因为上述调用存在先后关系,所以为了避免代码的深层嵌套,已是最优解。但如果没有先后关系,你可以使用 Promise.all 来避免使用很多 .then 或多次使用 Promise。如下示例:

const fetchUser = (id) => new Promise(resolve =>
  setTimeout(() => resolve(`User ${id}`), 1000 * id))

Promise.all([
  fetchUser(3),
  fetchUser(1),
  fetchUser(2),
])
  .then(([user1, user2, user3]) => {
    console.log('所有用户:', user1, user2, user3)
  })
  .catch(error => {
    console.error('获取用户失败:', error)
  })
 
const p1 = new Promise((resolve) => {
  setTimeout(() => {
    console.log("P1 完成");
    resolve("P1");
  }, 1000);
});
 
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("P2 失败");
    reject("P2 错误");
  }, 500);
});
 
const p3 = new Promise((resolve) => {
  setTimeout(() => {
    console.log("P3 完成");
    resolve("P3");
  }, 1500);
});
 
Promise.all([p1, p2, p3])
  .then((results) => {
    console.log("所有 Promise 成功:", results);
  })
  .catch((error) => {
    console.error("至少一个 Promise 失败:", error);
  });

// 输出顺序:
// P2 失败
// 至少一个 Promise 失败: P2 错误
// P1 完成
// P3 完成

该用法将会并行处理多个任务如果其中有一项失败,则会立即触发 catch 中内容,但不会结束其他正在进行的任务。此时任务均处理完成后,只要有一项失败,.then 中内容就不会执行。


3.3 Promise.race

另外还有一个 Promise.race 方法。它将返回第一个完成的 Promise 的结果,无论成功还是失败。一旦有一个 Promise 执行完成,其他的 Promise 结果将会被忽略

const fetchData = (id, delay) => new Promise((resolve, reject) => {
 setTimeout(() => {
   if (id === 2) reject(`Error from ${id}`);
   else resolve(`Data from ${id}`);
 }, delay);
});

Promise.race([
 fetchData(1, 2000),
 fetchData(2, 1000),
 fetchData(3, 3000)
])
.then(result => console.log("获胜的结果:", result))
.catch(error => console.error("错误:", error));

// 输出 (约1秒后):
// 错误: Error from 2

3.4 async/await

示例:

// 模拟异步操作:获取用户信息
async function fetchUserInfo(userId) {
  // 模拟网络请求延迟
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log("获取用户信息...");
  return { id: userId, name: "张三", email: "zhangsan@example.com" };
}
 
// 模拟异步操作:获取用户订单
async function fetchUserOrders(userId) {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log("获取用户订单...");
  return [
    { id: 1, product: "手机", price: 5000 },
    { id: 2, product: "电脑", price: 8000 }
  ];
}
 
// 主函数:使用 async/await 组合异步操作
async function getUserData(userId) {
  try {
    console.log("开始获取用户数据...");
 
    // 等待获取用户信息
    const userInfo = await fetchUserInfo(userId);
    console.log("用户信息:", userInfo);
 
    // 等待获取用户订单
    const userOrders = await fetchUserOrders(userId);
    console.log("用户订单:", userOrders);
 
    // 计算订单总额
    const totalAmount = userOrders.reduce((sum, order) => sum + order.price, 0);
    console.log("订单总额:", totalAmount);
 
    return {
      userInfo,
      userOrders,
      totalAmount
    };
  } catch (error) {
    console.error("获取数据时出错:", error);
  }
}
 
// 调用主函数
(async () => {
  const result = await getUserData(1);
  console.log("最终结果:", result);
})();

// 输出结果:
// 开始获取用户数据...
// 获取用户信息...
// 用户信息: { id: 1, name: '张三', email: 'zhangsan@example.com' }
// 获取用户订单...
// 用户订单: [
//   { id: 1, product: '手机', price: 5000 },
//   { id: 2, product: '电脑', price: 8000 }
// ]
// 订单总额: 13000
// 最终结果: {
//   userInfo: { id: 1, name: '张三', email: 'zhangsan@example.com' },
//   userOrders: [
//     { id: 1, product: '手机', price: 5000 },
//     { id: 2, product: '电脑', price: 8000 }
//   ],
//   totalAmount: 13000
// }

async/await 本质上是 Promise 的语法糖,使异步代码看起来更像同步代码,提高了可读性和维护性。需要注意:

async 函数总是返回一个Promise,即使你 await 的方法里没有任何需要异步的操作。
await 关键字只能在 async 函数内部使用
如果 await 后的内容抛出异常,可以用try/catch捕获


3.5 手写 Promise

在面试中,还可能会让你手写一个 Promise。仿写一个基础的 Promise 实际上没想象中难。通过 3.1 节,我们先分析一下手写一个 Promise 需要实现的基础事项:

(1) new Promise((resolve, reject) => {}),需要是这样的调用结构,内部用 resolve 代表成功reject 代表失败

(2) 调用后,可以使用 .then、.catch、.finally(多次使用 .then) => 注意链式调用的状态传递(这将在后续流程中完成);

(3) 内部有状态管理机制(Pending、Fulfilled、Rejected),一旦状态变化就不能再改变

通过 1.4 节的构造函数,我们可以发现它们结构很像,所以我们就用构造函数来实现它的基础定义

function MyPromise(executor) {
  var self = this
  self.state = 'pending'
  self.value = undefined
  self.reason = undefined

  function resolve(value) {
    if (self.state === 'pending') {
      self.state = 'fulfilled'
      self.value = value
    }
  }

  function reject(reason) {
    if (self.state === 'pending') {
      self.state = 'rejected'
      self.reason = reason
    }
  }

  try {
    executor(resolve, reject)
  } catch (error) {
    reject(error)
  }
}

分析一下:

(1) 首先,我们定义构造函数时,一定会接收一个 Function 类型入参,在这里我们叫做 executor。它需要在最后传递 resolve、reject 并立即执行。如果非函数,用 try、catch 捕获,直接返回 reject 即可;

(2) 内部存储一个状态 state(Pending、Fulfilled、Rejected),一个成功标识 value,一个错误标识 reason

(3) 为实现状态不可逆,resolve 和 reject 只能在当前状态为 pending 的情况下触发,随后改变当前实例状态和结果。

(4)之所以 resolve 方法和 reject 方法要放在构造函数内部,而不是原型链上,主要是因为:这样能防止外部直接调用它们,且它们本身只会用于 executor 函数调用,确保状态变更只影响当前实例。

现在我们完成了预期事项(1)和(3)。


通过 1.5 节的原型链,我们可以把 then、catch、finally 放在原型链上,从而让每个 Promise 实例都可以使用,避免每个实例都要创建一份方法副本,节省内存空间。
 
定下这个基调后,我们需要重新设定一下关于(2)的预期。因为接下来比较复杂,我们将逐步分析并完成预期。


第一步:我们先写一个基础的 then 方法。需要注意:

在第 2 节我们知道了事件循环。再看 3.1 节的示例,如果在初始化 new MyPromise 时,里面用到了宏任务、微任务,此时需要确保 then 的回调函数不会立即执行(catch、finally 同理)。

因为此时初始化的 Promise 实例的状态还是 pending,所以我们就可以根据实例状态来判断,是否可以立即执行回调函数。如果为 pending 就不能;如果为 fulfilled 就执行。(catch 后续判断 rejected 同理)

此时我们要在构造函数内存储一个 List里面存放暂未执行的回调函数。当触发了 resolve(rejected)的时候,遍历 List,将本次 resolve(rejected)的 value 结果传递进去即可。

除此以外,如果初始化 new MyPromise 时,里面没有延后执行的任务,假如执行成功,那么在进入 then 时,Promise 实例的状态将是 fulfilled。此时按照上述说法,我们要执行回调函数,但是 Promise 执行回调我们知道是微任务,所以我们要用第 2 节中其他原生的微任务封装一下(假如环境不支持其他微任务,则还用宏任务 setTimeout 来替代)。

MutationObserver:https://blog.youkuaiyun.com/qq_35385241/article/details/121989261

最后还有一点,为了能够链式调用,then 在执行回调函数后,需要返回一个新的 MyPromise 对象(因为我们不能强制用户在回调函数里 return 一个 MyPromise 对象 => 但如果用户真这么写,我们将在下方其他环节里改进)


该逻辑比较抽象,接下来通过示例理解下:

function queueMicrotask(callback) {
  if (typeof process !== 'undefined' && typeof process.nextTick === 'function') {
    process.nextTick(callback)
  } else if (typeof MutationObserver !== 'undefined') {
    var observer = new MutationObserver(callback)
    var node = document.createTextNode('')
    observer.observe(node, { characterData: true })
    node.data = ''
  } else {
    setTimeout(callback, 0)
  }
}

function MyPromise(executor) {
  var self = this
  self.state = 'pending'
  self.value = undefined
  self.reason = undefined
  self.onFulfilledCallbacks = []
  self.onRejectedCallbacks = []

  function resolve(value) {
    if (self.state === 'pending') {
      self.state = 'fulfilled'
      self.value = value
      self.onFulfilledCallbacks.forEach(callback => callback(value))
    }
  }

  function reject(reason) {
    if (self.state === 'pending') {
      self.state = 'rejected'
      self.reason = reason
      self.onRejectedCallbacks.forEach(callback => callback(reason))
    }
  }

  try {
    executor(resolve, reject)
  } catch (error) {
    reject(error)
  }
}
MyPromise.prototype.then = function(onFulfilled) {
  var self = this
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) { return value }

  var promise2 = new MyPromise(function(resolve, reject) {
    function handleFulfilled(value) {
      queueMicrotask(function() {
        try {
          var x = onFulfilled(value)
          resolve(x)
        } catch (error) {
          reject(error)
        }
      })
    }

    if (self.state === 'fulfilled') {
      handleFulfilled(self.value)
    } else if (self.state === 'pending') {
      self.onFulfilledCallbacks.push(handleFulfilled)
    }
  })

  return promise2
}

此时我们用 3.1 示例,试用 MyPromise 并分析一下:

function fetchUserData(userId) {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => {
      if (userId === 1) {
        resolve({
          id: 1,
          name: '张三',
          email: 'zhangsan@example.com'
        })
      } else {
        reject(new Error('用户不存在'))
      }
    }, 1000)
  })
}

fetchUserData(1)
  .then(userData => {
    console.log('用户数据:', userData)
    return 'name:',
  })
  .then(name => {
    console.log(name)
  })

(1) fetchUserData 时,因为有 setTimeout,所以内部状态为 pending。此时触发第一个 then,因为状态是 pending,所以将本次封装好的执行函数 handleFulfilled 推入到了 fetchUserData 返回的 Promise 对象的 onFulfilledCallbacks 中。再触发第二个 then,因为第一个 then 返回的 Promise 对象状态为 pending,所以将本次执行函数 handleFulfilled 推入到 第一个then 返回的 Promise 对象的 onFulfilledCallbacks 中

=> 假如 then 方法接收了一个非函数的参数,我们在这里帮用户写了一个最简单的 Function,以确保链式调用能够正确向下传递

onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) { return value }

(2) 随后 setTimeout 触发(先只关注执行成功的情况),执行成功内部触发 resolve 方法,并通过 onFulfilledCallbacks.forEach(callback => callback(value)),将本次得到的值 value 传递给存储的函数(第一个 then 封装的 handleFulfilled)。随后第一个 then 的 Promise 执行回调函数成功后,触发 resolve,并通过 onFulfilledCallbacks.forEach(callback => callback(value)) 再将本次得到的值 value 传递给下一个存储的函数(第二个 then 封装的 handleFulfilled)。

以此类推,基础功能完成。

(3) 同理的,假如 fetchUserData 没有 setTimeout,而是直接 resolve 结果,那么此时状态为 fulfilled。触发第一个 then 时,因为状态是 fulfilled,所以直接执行 handleFulfilled(self.value)。但是因为内部封装了微任务,所以第一个 then 的 Promise 对象状态将会是 pending。最后还是和上述流程一模一样。


第二步:完成 catch 方法。需要注意:

因为之前只关注执行成功的情况,目前在链式传递状态的过程中,当前 then 方法没有判定状态为 Rejected 的情况。所以我们先把这个功能补上

// ....

MyPromise.prototype.then = function(onFulfilled) {
  var self = this
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) { return value }
  onRejected = function(reason) { throw reason }

  var promise2 = new MyPromise(function(resolve, reject) {
    function handleFulfilled(value) {
      queueMicrotask(function() {
        try {
          var x = onFulfilled(value)
          resolve(x)
        } catch (error) {
          reject(error)
        }
      })
    }

    function handleRejected(reason) {
      queueMicrotask(function() {
        try {
          var x = onRejected(reason)
          reject(x)
        } catch (error) {
          reject(error)
        }
      })
    }

    if (self.state === 'fulfilled') {
      handleFulfilled(self.value)
    } else if (self.state === 'rejected') {
      handleRejected(self.reason)
    } else if (self.state === 'pending') {
      self.onFulfilledCallbacks.push(handleFulfilled)
      self.onRejectedCallbacks.push(handleRejected)
    }
  })

  return promise2
}

分析一下:补充的功能和第一步相同。如果第一个 Promise 状态为 pending,则这次额外将 handleRejected 推入到 onRejectedCallbacks。因为我们不知道第一个 Promise 会是成功还是失败。此时,当执行失败后,触发 self.onRejectedCallbacks.forEach(callback => callback(reason)),将原因 reason 给到存储的函数。后续流程同理。如果第一个 Promise 对象执行结果为 rejected,流程依然同理。

此时我们也可以关注到,如果其中任意一个 Promise 出现了 rejected,后续流程就都会是 rejected。


接下来关于 catch 方法,我们现在应该能知道,它和 then 原理是相同的:都需要返回 Promise 对象。唯一的不同点就是,catch 的回调,在上一个 Promise 的结果为 Fulfilled 的情况下不触发(那我们就要默认把上一步 value 结果向下传递)。所以我们可以快速写出答案:

// ....

MyPromise.prototype.catch = function(onRejected) {
  var self = this
  onFulfilled = function(value) { return value }
  onRejected = typeof onRejected === 'function' ? onRejected : function(reason) { throw reason }

  var promise2 = new MyPromise(function(resolve, reject) {
    function handleFulfilled(value) {
      queueMicrotask(function() {
        try {
          var x = onFulfilled(value)
          resolve(x)
        } catch (error) {
          reject(error)
        }
      })
    }

    function handleRejected(reason) {
      queueMicrotask(function() {
        try {
          var x = onRejected(reason)
          reject(x)
        } catch (error) {
          reject(error)
        }
      })
    }

    if (self.state === 'fulfilled') {
      handleFulfilled(self.value)
    } else if (self.state === 'rejected') {
      handleRejected(self.reason)
    } else if (self.state === 'pending') {
      self.onFulfilledCallbacks.push(handleFulfilled)
      self.onRejectedCallbacks.push(handleRejected)
    }
  })

  return promise2
}

此时,你就会发现,和 then 方法高度吻合,所以我们来合并一下最终结果

// ...

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  var self = this
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) { return value }
  onRejected = typeof onRejected === 'function' ? onRejected : function(reason) { throw reason }

  var promise2 = new MyPromise(function(resolve, reject) {
    function handleFulfilled(value) {
      queueMicrotask(function() {
        try {
          var x = onFulfilled(value)
          resolve(x)
        } catch (error) {
          reject(error)
        }
      })
    }

    function handleRejected(reason) {
      queueMicrotask(function() {
        try {
          var x = onRejected(reason)
          reject(x)
        } catch (error) {
          reject(error)
        }
      })
    }

    if (self.state === 'fulfilled') {
      handleFulfilled(self.value)
    } else if (self.state === 'rejected') {
      handleRejected(self.reason)
    } else if (self.state === 'pending') {
      self.onFulfilledCallbacks.push(handleFulfilled)
      self.onRejectedCallbacks.push(handleRejected)
    }
  })

  return promise2
}

MyPromise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

第三步:完成 finally 方法。需要注意:

finally 的核心还是和 then 一样,所以我们还是直接调用 then 方法来执行回调函数。只是这里 finally 比 then、catch 复杂一点。

首先我们要保证,无论上一步是成功还是失败,当前回调函数都要执行。所以调用 then 方法时的两个入参都要能触发回调函数。比如:

MyPromise.prototype.finally = function(callback) {
  return this.then(
    callback,
    callback
  )
}

但是 finally 里的内容不应该随链向下传递(还是要将上一步的成功/失败结果传递下去),比如(这个是错误用法):

Promise.resolve(42)
  .finally(() => 'xxx')
  .then((value) => console.log(value)); // 应该输出 42,而不是 xxx

所以目前的 then 方法肯定不支持,它会直接调用回调函数,并将结果传下去。为了解决这一点,我们就要在传递给 then 的入参上想办法。那就是:在执行回调函数后,返回上一步执行的结果

说到这里,答案就呼之欲出了:

MyPromise.prototype.finally = function(callback) {
  return this.then(
    value => MyPromise.resolve(callback()).then(() => value),
    reason => MyPromise.resolve(callback()).then(() => { throw reason })
  )
}

MyPromise.resolve = function(value) {
  if (value instanceof MyPromise) {
    return value
  }

  return new MyPromise(function(resolve) {
    resolve(value)
  })
}

我们在 MyPromise 上创建静态方法 resolve。为什么不写在原型链上,是因为这样能让我们在不用创建 MyPromise 实例的情况下,去直接调用。比如:

// 将普通值转换为 Promise
const p1 = MyPromise.resolve(123);
// 等价于
const p1 = new MyPromise(resolve => resolve(123));

这样一来,我们在 finally 里,就可以直接使用 MyPromise.resolve 返回一个 Promise 实例,而不用写的更复杂了。

所以 MyPromise.resolve(callback()).then(() => value) 的结果就是:调用 callback 回调函数后,返回一个 Promise 对象,随后触发 then(catch 同理),返回 value(reason)。

value => MyPromise.resolve(callback()).then(() => value),
// 最终结果等价于(只不过是在 callback 执行结束后触发)
function(value) { return value }

第四步:以上 我们完成了所有基本事项。接下来还要考虑一种情况:如果在 then、catch 中返回一个 Promise 对象,此时一定不会按照预期工作(因为当前不会深层次遍历执行 内部嵌套的 Promise)。我们来解决这一点,完整代码:

function queueMicrotask(callback) {
  if (typeof process !== 'undefined' && typeof process.nextTick === 'function') {
    process.nextTick(callback)
  } else if (typeof MutationObserver !== 'undefined') {
    var observer = new MutationObserver(callback)
    var node = document.createTextNode('')
    observer.observe(node, { characterData: true })
    node.data = ''
  } else {
    setTimeout(callback, 0)
  }
}

function MyPromise(executor) {
  var self = this
  self.state = 'pending'
  self.value = undefined
  self.reason = undefined
  self.onFulfilledCallbacks = []
  self.onRejectedCallbacks = []

  function resolve(value) {
    if (self.state === 'pending') {
      self.state = 'fulfilled'
      self.value = value
      self.onFulfilledCallbacks.forEach(callback => callback(value))
    }
  }

  function reject(reason) {
    if (self.state === 'pending') {
      self.state = 'rejected'
      self.reason = reason
      self.onRejectedCallbacks.forEach(callback => callback(reason))
    }
  }

  try {
    executor(resolve, reject)
  } catch (error) {
    reject(error)
  }
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
  var self = this
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) { return value }
  onRejected = typeof onRejected === 'function' ? onRejected : function(reason) { throw reason }

  var promise2 = new MyPromise(function(resolve, reject) {
    function handleFulfilled(value) {
      queueMicrotask(function() {
        try {
          var x = onFulfilled(value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
    }

    function handleRejected(reason) {
      queueMicrotask(function() {
        try {
          var x = onRejected(reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
    }

    if (self.state === 'fulfilled') {
      handleFulfilled(self.value)
    } else if (self.state === 'rejected') {
      handleRejected(self.reason)
    } else if (self.state === 'pending') {
      self.onFulfilledCallbacks.push(handleFulfilled)
      self.onRejectedCallbacks.push(handleRejected)
    }
  })

  return promise2
}

MyPromise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

MyPromise.prototype.finally = function(callback) {
  var P = this.constructor
  return this.then(
    value => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  )
}

function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) {
    reject(new TypeError('Chaining cycle detected for promise'))
    return
  }

  var called = false

  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      var then = x.then
      if (typeof then === 'function') {
        then.call(x, function(y) {
          if (called) return
          called = true
          resolvePromise(promise, y, resolve, reject)
        }, function(r) {
          if (called) return
          called = true
          reject(r)
        })
      } else {
        resolve(x)
      }
    } catch (error) {
      if (called) return
      called = true
      reject(error)
    }
  } else {
    resolve(x)
  }
}

MyPromise.resolve = function(value) {
  if (value instanceof MyPromise) {
    return value
  }

  return new MyPromise(function(resolve) {
    resolve(value)
  })
}

共有两个调整点:

(1) finally 增加 var P = this.constructor 的处理。增加原因很简单:我们在 1.5 节中有看到 constructor 会得到对应的构造函数(在这里就是 MyPromise 构造函数)以及继承功能。如果没继承,此时通过它可以使用到 MyPromise.resolve。但如果被继承(重写了 resolve 方法),此时将使用被重写后的 resolve 方法,避免了写死 MyPromise.resolve 带来的不可扩展性。

(2) 在 resolvePromise 内,当上一步的结果是有 then 方法的(为什么不判断 MyPromise 实例,和(1)原因相同,可能存在继承关系,此时不一定是 MyPromise 实例),那我们就要立即执行 then 方法。为确保 this 指向正确,使用 call。为避免内部再次嵌套 Promise,所以递归调用 resolvePromise。

=> 其他情况在报错情况下 reject,正常情况下 resolve 即可。

至此,我们完成了所有预期事项。


3.6 手写 Promise.all

有了手写 MyPromise 的经验,我们先分析一下 MyPromise.all 的预期:

(1) 入参为一个 List,内部是多个需要执行的 Function。这些 Function 本质预期是返回 Promise 对象,所以我们需要兼容返回只是个普通值的情况(也就是上文的 MyPromise.resolve 的用法);

(2) 在进到 all 方法后,循环执行 List 内的 Function,并将执行结果逐个存储。过程中,只有全部成功,才成功;如果其中任一失败,最后结果都是失败但不影响其他 Function 执行)。

(3) all 方法依然返回 Promise 对象,用于外层 .then / .catch。

示例:

MyPromise.all = function(promises) {
  return new MyPromise(function(resolve, reject) {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('promises must be an array'))
    }

    var results = []
    var completed = 0

    if (promises.length === 0) {
      resolve(results)
    }

    function resolvePromise(index, value) {
      results[index] = value
      completed++
      if (completed === promises.length) {
        resolve(results)
      }
    }

    for (var i = 0; i < promises.length; i++) {
      (function(i) {
        MyPromise.resolve(promises[i]).then(
          function(value) {
            resolvePromise(i, value)
          },
          function(error) {
            reject(error)
          }
        )
      })(i)
    }
  })
}


MyPromise.all([func1, func2, func3])
  .then(results => {
    console.log('All promises succeeded:', results);
  })
  .catch(error => {
    console.log('At least one promise failed:', error);
  });

3.7 手写 Promise.race

race 就更简单了,它只需要返回第一个完成的 Promise 的结果,无论成功还是失败。一旦有一个 Promise 执行完成,其他的 Promise 结果将会被忽略。我们只需要改造下 MyPromise.all 方法:

MyPromise.race = function(promises) {
  return new MyPromise(function(resolve, reject) {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('promises must be an array'))
    }

    if (promises.length === 0) {
      return
    }

    for (var i = 0; i < promises.length; i++) {
      MyPromise.resolve(promises[i]).then(resolve, reject)
    }
  })
}

4、解决递归栈溢出

4.1 什么是递归,递归栈溢出的原因

递归是一种编程技术,函数直接或间接调用自身来解决问题。递归通常用于解决可以被分解成相似的子问题的问题。以下先给一个会出现递归栈溢出的示例:

function recursiveFunc(n) {
 if (n === 0) return;
 recursiveFunc(n - 1);
}

关于出现递归栈溢出的原因,我们需要分析一下这段代码:
(上一篇文章:https://blog.youkuaiyun.com/qq_45613931/article/details/144483787

(1) 在上一篇文章 1.3 节里,我们首先知道引用类型的值是存储在堆内存中的,存储在栈内存上的是指向堆内存中该对象的地址。在上一篇文章 3.3 节里还写到,函数在创建时,要记录函数能够访问的所有局部变量、函数的参数信息。所以该例在存储时,可用下面简图来表述。

在这里插入图片描述

(2)当每次进行函数调用时,都会在调用栈上创建一个新的栈帧。因此当递归层级较深或根本没有设置跳出递归的终止条件时,函数调用会不断创建新的栈帧,直到栈空间被耗尽,从而超过了调用栈的容量,发生栈溢出。如下所示:

function countDown(n) {
 console.log(n);
 countDown(n - 1); // 没有合适的终止条件
}
countDown(100000);

function recursiveCount(n) {
 if (n === 0) return;
 recursiveCount(n - 1);
}
try {
 recursiveCount(1000000); // 尝试递归100万次
 console.log("成功完成");
} catch (e) {
 console.error("发生错误:", e.message);
}

其中,正是因为每次 recursiveCount 调用都需要等待其内部的递归调用完成才能计算结果并返回,所以它们都需要保持活跃状态,就不会被垃圾回收机制所回收。

从这里你也知道了:栈有固定的大小限制。这个限制是由操作系统和编程语言的运行时环境决定的。在大多数系统中,默认的栈大小通常在几MB左右。


4.2 解决方案

对于需要大量递归的问题,可以考虑以下方法来解决或避免递归栈溢出:

(1)将递归转换为循环

该方法最简单,就是将比较简单的递归用法,改成使用 for 循环的形式(前提是,能这么做的话)。

如下所示:

function factorialIterative(n) {
 let result = 1;
 for (let i = 2; i <= n; i++) {
   result *= i;
 }
 return result;
}

console.log(factorialIterative(5)); // 输出: 120

=> 需要额外注意,如果循环次数过多,又会导致线程阻塞,导致页面卡顿。此时可以参考我的文章(JS代码优化):
https://blog.youkuaiyun.com/qq_45613931/article/details/143828634


(2)尾递归优化

尾递归是指递归调用是函数的最后一个操作(某些语言和环境支持尾递归优化,可以防止栈溢出)。如下所示:

function factorialTailRecursive(n, accumulator = 1) {
 if (n <= 1) return accumulator;
 return factorialTailRecursive(n - 1, n * accumulator);
}

console.log(factorialTailRecursive(5)); // 输出: 120

原理:在支持尾调用优化的环境中,编译器可以将尾递归转化为循环此时当前函数帧的信息不再需要保留,编译器可以重用当前的栈帧,而不是创建新的。这样,无论递归多少次,栈的深度都保持不变


(3)蹦床函数(Trampoline)

蹦床函数是一种在不支持尾调用的环境中,可以在不增加调用栈深度的情况下,模拟尾递归优化的技术。如下所示:

function trampoline(fn) {
 return function(...args) {
   let result = fn(...args);
   while (typeof result === 'function') {
     result = result();
   }
   return result;
 };
}

function sumTail(x, y) {
 if (y > 0) {
   return () => sumTail(x + 1, y - 1);
 } else {
   return x;
 }
}

const sum = trampoline(sumTail);
console.log(sum(1, 100000)); // 可以处理大数而不会栈溢出

原理:每次递归都不再是真正的函数调用,而是返回一个新的函数。此时 rampoline 函数使用循环来执行这些返回的函数,由于使用了循环而不是实际的递归调用,栈深度保持恒定。只是该方法更复杂些,但可以保证在任何环境中使用。


(4) 记忆化(Memoization)

该方法侧面解决栈溢出,即:通过缓存已计算的结果来优化递归,显著减少递归深度。如下所示:

function memoize(fn) {
 const cache = new Map();
 return function(...args) {
   const key = JSON.stringify(args);
   if (cache.has(key)) {
     return cache.get(key);
   }
   const result = fn.apply(this, args);
   cache.set(key, result);
   return result;
 };
}

const fibonacci = memoize(function(n) {
 if (n <= 1) return n;
 return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(100)); // 可以快速计算大的斐波那契数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端Jerry_Zheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值