Skip to content

30K 前端面试题,现场回答

一、你可以介绍一下手写 Promise.all 的实现思路吗

Details

当然可以。Promise.all 是一个静态方法,它接受一个 Promise 对象的数组作为输入,并返回一个新的 Promise。这个返回的 Promise 只有在输入的所有 Promise 都成功解决(resolved)时才解决,解决的值是一个数组,包含所有输入 Promise 解决的值。如果任何一个输入 Promise 被拒绝(rejected),返回的 Promise 就会立即被拒绝,拒绝的原因是第一个拒绝的 Promise 的原因。

实现Promise.all的思路主要包括以下几个步骤:

  1. 返回一个新的 Promise: Promise.all的实现首先需要返回一个新的 Promise 对象,这个 Promise 将在所有输入的 Promise 都成功解决时解决,或者在任何一个输入的 Promise 被拒绝时被拒绝。

  2. 遍历所有输入的 Promise: 对输入的 Promise 数组进行遍历,为每一个 Promise 分别设置thencatch处理程序。

  3. 收集解决的值: 创建一个数组来收集所有解决的值。每当一个输入的 Promise 被解决时,就将其值添加到这个数组中。数组中值的顺序应该与输入的 Promise 数组中 Promise 的顺序相同。

  4. 处理解决和拒绝:

    • 如果一个 Promise 被解决,检查是否所有的 Promise 都已解决。如果是,那么使用收集到的解决值数组解决Promise.all返回的 Promise。
    • 如果任何一个 Promise 被拒绝,立即拒绝Promise.all返回的 Promise,拒绝原因是第一个拒绝的 Promise 的原因。

下面是一个简化版的Promise.all的实现:

javascript
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = []; // 用于收集所有解决的值
    let completed = 0; // 已完成的Promise数量

    promises.forEach((promise, index) => {
      // 处理每个Promise
      Promise.resolve(promise)
        .then((value) => {
          results[index] = value; // 保存解决的值
          completed += 1; // 更新计数器

          // 如果所有Promise都完成了,则解决返回的Promise
          if (completed === promises.length) {
            resolve(results);
          }
        })
        .catch(reject); // 任何Promise拒绝则立即拒绝
    });

    // 处理空数组的特殊情况
    if (promises.length === 0) {
      resolve(results);
    }
  });
}

这个实现包括了上述所有步骤,确保了Promise.all的核心功能:等待所有 Promise 解决或在第一个 Promise 被拒绝时立即拒绝。

二、你可以介绍一下实现模拟 new 操作符行为的一个方法吗?

Details

在 JavaScript 中,new关键字用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。如果想要模拟new操作符的行为,我们可以创建一个函数,这个函数模拟了new操作符的所有关键步骤。这些步骤大致如下:

  1. 创建一个空对象:新创建的对象将会继承构造函数的原型。
  2. 将构造函数的作用域赋给新对象(即将this绑定到新对象上)。
  3. 执行构造函数中的代码(为这个新对象添加属性)。
  4. 返回新对象:如果构造函数返回一个对象,则返回该对象;否则,返回步骤 1 创建的对象。

下面是一个模拟new操作符的函数实现示例:

javascript
function simulateNew(constructor, ...args) {
  // 步骤1:创建一个空对象,并继承构造函数的原型。
  const obj = Object.create(constructor.prototype);

  // 步骤2 & 3:将构造函数的作用域赋给新对象(也就是将this绑定到新对象上),并执行构造函数中的代码。
  const result = constructor.apply(obj, args);

  // 步骤4:根据构造函数的返回值决定返回哪个对象。
  // 如果构造函数返回一个对象,则返回该对象;否则,返回步骤1创建的对象。
  return result !== null &&
    (typeof result === "object" || typeof result === "function")
    ? result
    : obj;
}

// 使用示例
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function () {
  return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
};

const person1 = simulateNew(Person, "John", 30);
console.log(person1.sayHello()); // "Hello, my name is John and I am 30 years old."

这段代码成功地模拟了new关键字的工作原理,创建了一个Person类型的新实例,并且这个实例可以访问Person原型上的sayHello方法。通过这种方式,你可以在不使用new关键字的情况下实现类似的对象创建和继承功能。

三、你可以介绍一下你对原型链的理解吗

Details

原型链是 JavaScript 中实现对象继承和共享属性或方法的一种机制。它是基于原型的继承核心概念,使得 JavaScript 中的对象可以继承另一个对象的属性和方法。这里的“原型”可以理解为一个模板对象,其他对象可以通过它来继承属性和方法。

基本概念

在 JavaScript 中,几乎所有的对象都是Object的实例,它们都从Object.prototype继承属性和方法。同时,它们也可以拥有自己的属性和方法。

原型对象(Prototype)

每一个 JavaScript 对象(除了null)在创建时都会与另一个对象关联,这个对象就是我们所说的原型,每一个对象都会从原型“继承”属性。

构造函数(Constructor)

JavaScript 使用构造函数作为对象的模板。通过new操作符来创建一个新对象,这个新对象会继承构造函数原型上的属性和方法。

__proto__属性

__proto__是每个对象都有的一个属性,它指向该对象的原型。这是原型链的核心,通过__proto__属性将对象与原型连接起来。

原型链工作原理

当你试图访问一个对象的属性或方法时,如果这个对象自身没有这个属性或方法,JavaScript 引擎会沿着这个对象的原型链向上查找,直到找到这个属性或方法,或者到达原型链的末端(Object.prototype.__proto__,其值为null)。

为什么重要?

原型链是 JavaScript 中实现继承的主要方法。它允许对象共享方法和属性,减少了每个对象需要定义相同方法的需要,节省了内存。同时,它也是理解 JavaScript 对象、构造函数、this关键字等高级概念的基础。

示例

javascript
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function () {
  return `Hello, my name is ${this.name}`;
};

const person1 = new Person("Alice");
console.log(person1.sayHello()); // "Hello, my name is Alice"
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.__proto__ === Object.prototype); // true
console.log(person1.__proto__.__proto__.__proto__); // null

在这个例子中,person1对象继承自Person.prototype,而Person.prototype对象又继承自Object.prototype。这形成了一个原型链:person1 -> Person.prototype -> Object.prototype -> null

通过原型链,JavaScript 支持基于原型的继承和属性共享,是该语言中最强大的特性之一。

四、你可以说一下浏览器 JavaScript 为什么是异步的理解吗

Details

JavaScript 在浏览器中是单线程执行的,这意味着在任何给定时刻,只能执行一段代码。这个设计选择主要是为了简化对 DOM 的操作,避免复杂的同步问题。如果 JavaScript 是多线程的,开发者就需要处理多个脚本同时尝试修改页面的同步问题,这会大大增加编程的复杂性。

然而,单线程执行也带来了一个显著的缺点:如果执行长时间运行的任务,它会阻塞页面,导致不良的用户体验。为了解决这个问题,浏览器提供了异步编程的能力,使得 JavaScript 可以在不阻塞主线程的情况下执行时间较长的任务。

异步编程的原因:

  1. 避免阻塞 UI:用户界面渲染和脚本执行在同一线程上发生。长时间运行的脚本会阻塞 UI 的更新,导致应用程序看起来卡顿或不响应。通过异步操作,可以将耗时任务放在后台执行,从而保持界面的响应性。

  2. 处理 I/O 密集型操作:Web 应用程序经常需要执行 I/O 操作,如从服务器请求数据或加载文件。这些操作不需要 CPU 持续计算,但会因为等待数据而导致线程阻塞。异步编程允许这些任务在等待数据时释放主线程,以便可以继续处理其他任务。

  3. 利用回调和事件循环:JavaScript 的异步编程模型依赖于回调函数和事件循环。当异步操作完成时,回调函数会被加入到任务队列中,等待当前执行栈清空后执行。这个模型使得开发者可以编写非阻塞的代码,而不需要担心底层的多线程同步问题。

  4. 支持 Promise 和 Async/Await:为了解决回调地狱(callback hell)的问题,并使异步代码更容易编写和理解,ES6 引入了 Promise,后来又引入了 async/await 语法糖。这些都是基于 JavaScript 的异步特性,提供了更强大、更易于管理的方式来处理异步操作。

实现机制:

  • 事件循环:浏览器通过事件循环来管理事件的执行。当 JavaScript 引擎执行完当前执行栈中的所有任务后,它会查看任务队列。如果队列中有待处理的任务,引擎会依次取出并执行这些任务。

  • Web API:浏览器提供了一套 Web API,如setTimeoutXMLHttpRequestfetch,它们可以启动异步操作。当这些操作完成时,相应的回调函数会被添加到任务队列中,等待执行。

这种设计使得 JavaScript 可以执行长时间运行的任务,而不会阻塞主线程,保持了 Web 应用程序的响应性和性能。

五、webpack 是单线程的吗?

Details

是的,Webpack 本质上是单线程的。它运行在 Node.js 环境中,依赖于 Node.js 的事件循环机制。尽管 Node.js 支持异步 I/O 操作,可以高效地处理文件读写、网络请求等操作,但它的 JavaScript 执行是单线程的。这意味着在任何给定时刻,Webpack 的处理过程中只有一个主线程在工作,进行模块解析、依赖收集、插件执行等任务。

并行处理

尽管 Webpack 本身是单线程的,但它可以利用一些策略和工具来加速构建过程,实现类似多线程的并行处理效果。例如:

  • 使用thread-loader:在某些耗时的加载器(loader)之前加入thread-loader,可以将这些任务分发到一个工作池的多个工作线程中。这样,耗时的加载器可以在多个线程中并行执行,减少总的构建时间。

  • 使用parallel-webpack:这是一个可以让 Webpack 构建在多个进程中并行运行的工具,从而加快构建速度。它通过启动多个 Webpack 实例来实现并行处理。

  • 利用插件和工具进行代码压缩和优化:某些插件能够利用多核 CPU 的优势来并行处理任务(例如,TerserWebpackPlugin在压缩 JavaScript 代码时可以开启多线程模式)。

总结

虽然 Webpack 的核心处理逻辑是单线程的,但它可以通过上述方法和工具来实现任务的并行处理,从而优化构建性能和效率。这表明,尽管在单线程环境中运行,Webpack 和其生态系统的设计仍然能够有效利用现代多核 CPU 的计算资源,提高构建速度。

六、webpack 主要有哪几大模块?

Details

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器,它主要由以下几大核心组成部分构成,每个部分扮演着不同的角色:

1. 入口(Entry)

  • 作用:入口指定了 Webpack 应该用哪个文件来开始构建其内部依赖图。
  • 详解:可以将入口理解为应用程序的起始点。Webpack 会从这里开始,收集并构建所有需要的模块。入口可以是单个文件(单入口)或多个文件(多入口)。

2. 输出(Output)

  • 作用:输出告诉 Webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。
  • 详解:默认情况下,Webpack 将结果输出到./dist/main.js。输出配置允许你将打包文件放在你指定的任何位置,并且你可以指定输出文件的名称。

3. 加载器(Loaders)

  • 作用:加载器让 Webpack 能够去处理那些非 JavaScript 文件(Webpack 自身只理解 JavaScript)。
  • 详解:加载器可以将所有类型的文件转换为 Webpack 能够处理的有效模块,从而可以添加到依赖图中。比如,style-loadercss-loader可以让你导入 CSS 文件,babel-loader可以让你使用最新的 JavaScript 代码,而不用担心兼容性问题。

4. 插件(Plugins)

  • 作用:插件用于执行范围更广的任务。从打包优化和压缩,一直到重新定义环境中的变量。
  • 详解:插件可以影响构建过程中的每个阶段,是 Webpack 极其强大的功能所在。它们是 Webpack 生态系统中的关键元素,可以用来进行各种任务,比如打包优化、资产管理和环境变量注入等。

5. 模式(Mode)

  • 作用:通过设置mode参数为developmentproductionnone,可以启用 Webpack 内置在相应环境下的优化。
  • 详解mode参数可以启用 Webpack 内部的优化插件,根据选择的模式不同,Webpack 会默认启用不同的配置项,以最适应开发环境或生产环境。

6. 模块(Module)

  • 作用:在 Webpack 里,每个文件都被当作一个模块。
  • 详解:Webpack 会从入口开始递归找出所有依赖的模块。每一种文件可以通过相应的加载器来处理,最终转换成模块加入到依赖图中。

总结

Webpack 的这些组成部分共同工作,使其能够高效地管理、打包和转换项目资源。通过灵活配置这些部分,开发者可以优化应用的大小和性能,提高开发效率和生产效率。

七、vite 主要有哪几大模块?其中 rollup 的原理是什么?

Details

Vite 是一个现代化的前端构建工具,它利用了浏览器原生 ES 模块导入的能力来提供快速的冷启动和即时模块热替换(HMR)。Vite 主要由以下几个核心部分组成:

1. 开发服务器(Development Server)

  • 作用:提供了一个快速的开发服务器,支持丰富的功能,如即时模块热更新(HMR)。
  • 详解:Vite 在开发模式下利用浏览器支持的 ES 模块导入来服务代码,无需打包操作,这使得启动速度极快。

2. 插件系统(Plugins)

  • 作用:通过插件扩展 Vite 的功能,Vite 插件可以利用 Rollup 插件接口,因为 Vite 在底层使用 Rollup 进行代码打包。
  • 详解:插件系统允许开发者自定义和扩展构建步骤,比如使用 Vue 或 React,处理 CSS 预处理器,或者支持图片和字体文件等。

3. 构建(Build)

  • 作用:用于生产环境的代码构建。
  • 详解:当准备将应用部署到生产环境时,Vite 提供了打包和优化工具。在这个阶段,Vite 使用 Rollup 来打包应用,因为 Rollup 非常擅长生成小而高效的静态资源。

4. 模块热更新(HMR)

  • 作用:支持模块热更新,改进开发体验。
  • 详解:Vite 开发服务器能够实时响应文件的更改,并且仅更新更改的模块,而不是重新加载整个页面,这样可以大大提高开发效率。

Rollup 的原理

Rollup 是一个 JavaScript 模块打包器,它专注于生成尽可能小的代码块,以便于在浏览器中使用。它与其他打包器(如 Webpack)的主要区别在于输出格式:Rollup 默认生成的是 ES 模块,而不是 Webpack 的自定义模块系统。

  • 模块合并:Rollup 会分析入口点和所有依赖模块,然后将这些模块合并到一个文件中。这种方法减少了引导时间和请求次数,因为浏览器只需要加载一个文件。

  • 树摇(Tree-shaking):Rollup 使用静态分析来识别“死代码”(未使用的代码),并在最终的包中排除它们。这进一步减小了包的大小,因为不会包含无用代码。

  • 代码分割和懒加载:尽管 Rollup 最初设计用来生成单一的 bundle 文件,但它也支持代码分割,使得可以创建多个 chunk,这些 chunk 可以按需加载,从而提高应用的性能。

Rollup 的这些特性使它非常适合用于构建现代 JavaScript 库和工具,以及任何需要高效加载的前端项目。在 Vite 中,Rollup 被用作生产构建的工具,因为它的优化能力可以提供更小的代码包,从而加快页面加载速度。

八、那么 esbuild 呢?

Details

esbuild是一个极其快速的 JavaScript 打包器和压缩器,它采用 Go 语言编写,旨在通过多核并行计算来实现编译速度的极大提升。相较于其他打包工具如 Webpack 和 Rollup,esbuild的主要优势在于其速度,它可以在几毫秒内完成大型项目的打包任务,这在以往的 JavaScript 工具中是难以想象的。

esbuild 的核心特性包括:

  • 极速打包:通过利用 Go 语言的并行性和高效,esbuild能够实现令人难以置信的快速构建时间。
  • 简洁的 APIesbuild提供了一个简洁直观的 API,使得配置和使用变得非常简单。
  • 内置 Minification 和 Tree-shakingesbuild内置了代码压缩和未使用代码的移除(tree-shaking)功能,进一步减少输出文件的大小。
  • 加载器支持esbuild支持多种文件类型的加载器,例如 JavaScript、TypeScript、JSX、CSS 等,允许直接引入这些资源而无需额外配置。
  • 插件系统:尽管esbuild的功能非常强大,但它也提供了一个插件系统,以便开发者可以扩展其功能以适应特定的构建需求。

esbuild 的工作原理

esbuild通过以下方式实现其快速的构建性能:

  • 并行处理esbuild利用 Go 的高效并行能力来同时处理多个文件和任务,大大减少了处理时间。
  • 简化的 AST 转换:与其他打包工具相比,esbuild在转换代码时使用了更简化的抽象语法树(AST)解析和转换步骤,减少了处理过程中的计算负担。
  • 避免不必要的抽象esbuild避免使用过多的抽象层,这意味着在解析和打包过程中有更少的间接性,从而减少了时间和内存的消耗。

使用场景

esbuild适用于多种开发场景,包括但不限于:

  • 开发环境下的快速重建和热模块替换(HMR)。
  • 生产环境下的高效打包和资源优化。
  • 作为其他工具和框架的底层编译器或打包器(例如,Vite 在某些场景下使用esbuild来提高其启动和构建速度)。

总的来说,esbuild通过其设计和实现,为现代 Web 开发提供了一个快速且高效的构建工具,特别是在需要快速迭代和构建大型项目时,它提供了巨大的速度优势。

九、vite 为什么既用 esbuild 又用 rollup?他们在 vite 中分别扮演什么样的角色和作用

Details

Vite 结合了 esbuildRollup 的优势,以提供一个既快速又灵活的现代前端开发环境。这两个工具在 Vite 中扮演着互补的角色,分别针对开发和生产构建过程优化。

esbuild 在 Vite 中的作用

开发环境优化

  • 快速启动:Vite 使用 esbuild 进行依赖预打包(pre-bundling)。这意味着对于那些大型的第三方库,Vite 利用 esbuild 的快速打包能力,预先将它们打包成更易于快速加载的形式。这减少了在开发过程中服务器启动和热更新的时间。
  • 快速转译esbuild 也用于将 TypeScript、JSX 等语言快速转译成标准的 JavaScript,这一步骤非常快,有助于实现近乎即时的热模块替换(HMR)。

esbuild 的这些特性使得 Vite 能够在开发过程中提供极快的反馈和更新速度。

Rollup 在 Vite 中的作用

生产环境优化

  • 高质量的代码输出:对于生产构建,Vite 则使用 Rollup 来进行代码的打包和优化。Rollup 在处理代码分割(code splitting)、静态资源导入、打包库(library bundling)和生成高效的最终代码方面表现出色。
  • 丰富的插件生态:由于 Rollup 拥有广泛的插件生态,它可以为生产环境构建提供高级的优化和特性,如自动压缩、代码拆分和懒加载支持、自定义处理逻辑等。

通过这种方式,Vite 结合了 esbuild 的速度和 Rollup 的灵活性与高级特性,为开发者提供了最佳的开发体验和生产环境输出。

总结

Vite 创造性地结合了 esbuildRollup,以实现开发和生产的最优化:

  • 在开发阶段,Vite 利用 esbuild 的速度优势,提供快速的重建和热更新,使得开发过程中的等待时间最小化。
  • 对于生产构建,Vite 则转向 Rollup 的强大功能和丰富的插件生态,以确保最终构建的质量和性能。

这样的设计选择使 Vite 成为了一个非常受欢迎的现代前端开发工具,它在保持开发效率的同时,也能够生成高效且优化的生产代码。