前端面试题四

212 阅读19分钟

flex 居中怎么实现?

在 CSS 中,可以通过以下方式将元素居中显示:

  1. 水平居中:
    • 对于块级元素(如 <div>):设置 margin-left: auto;margin-right: auto;,同时指定一个固定的宽度(例如 width: 300px;)。
    • 对于行内元素(如 <span>):将元素的父级容器设置为 text-align: center;
  1. 垂直居中:
    • 对于块级元素(如 <div>):使用 Flexbox 布局,将元素的父级容器设置为 display: flex;,然后使用 align-items: center; 属性。
    • 对于行内元素(如 <span>):使用 Flexbox 布局,将元素的父级容器设置为 display: inline-flex;,然后使用 align-items: center; 属性。

以下是一个示例代码,展示如何同时实现水平和垂直居中:

<!DOCTYPE html>
<html>
<head>
  <style>
    .container {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 300px;
      height: 200px;
      border: 1px solid black;
    }
  </style>
</head>
<body>
  <div class="container">
    <p>居中内容</p>
  </div>
</body>
</html>

在上述示例中,.container 类指定了一个宽度和高度,并设置了 display: flex;justify-content: center;align-items: center; 来实现元素的水平和垂直居中显示。

javascript语言中,为什么在2^53范围内是安全的?

在 JavaScript 中,数字类型(Number)采用的是双精度浮点数表示法,遵循 IEEE 754 标准。双精度浮点数使用 64 位二进制来表示一个数值,其中包括符号位、指数位和尾数位。

JavaScript 中的 Number 类型可以表示的数值范围是有限的。在这个范围内,大约是从 -9,007,199,254,740,992 (-2^53) 到 9,007,199,254,740,992 (2^53)。这个范围是由双精度浮点数的指数位和尾数位的位数决定的。

那么为什么在 2^53 范围内是安全的呢?这是因为在这个范围内,JavaScript 的 Number 类型可以精确表示整数。任何介于 -2^53 到 2^53 之间的整数都可以被准确表示,而不会出现舍入错误或精度损失。

然而,当超出这个范围时,JavaScript 的 Number 类型开始出现精度问题。由于尾数位的限制,超出范围的整数无法精确表示,可能会出现舍入错误。例如,超过 2^53 的整数在进行计算时可能会失去精度,或者被近似为最接近的可表示的数。

对于大于 2^53 的整数或需要进行高精度计算的场景,JavaScript 提供了其他数据类型和库来处理,如 BigInt 类型或第三方的大数运算库。这些数据类型和库可以处理更大范围和更高精度的数值计算,但相应地会增加计算的复杂性和性能开销。

因此,对于一般的数值计算和应用,保持在 JavaScript 的 Number 类型的安全范围内是可靠的,而超出这个范围的情况需要使用其他方式来处理。

用js写一个数字每三位用逗号分割的算法。

你可以使用 JavaScript 来编写一个函数,将数字的每三位添加逗号分隔。以下是一个示例算法:

function addCommas(number) {
  // 将数字转换为字符串
  var str = number.toString();

  // 从末尾开始每三位添加逗号
  var result = '';
  var count = 0;
  for (var i = str.length - 1; i >= 0; i--) {
    result = str.charAt(i) + result;
    count++;
    if (count === 3 && i !== 0) {
      result = ',' + result;
      count = 0;
    }
  }

  return result;
}

// 示例使用
var number = 1234567890;
var formattedNumber = addCommas(number);
console.log(formattedNumber);  // 输出: "1,234,567,890"

在上述示例中,addCommas 函数接受一个数字作为参数,将其转换为字符串,并从末尾开始遍历。在遍历过程中,每三位数字后面添加一个逗号,直到到达数字的开头。

注意,在实际应用中,你可能需要添加一些边界条件和错误处理,例如处理负数、小数或非数字的情况。此示例仅展示了将数字每三位添加逗号的基本思路。

用js写一个二分查找?

以下是使用 JavaScript 编写的一个二分查找算法的示例:

function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    let mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) {
      return mid; // 找到目标值,返回索引
    } else if (arr[mid] < target) {
      left = mid + 1; // 目标值在右半部分,更新左边界
    } else {
      right = mid - 1; // 目标值在左半部分,更新右边界
    }
  }

  return -1; // 目标值不存在,返回 -1
}

// 示例使用
var array = [1, 3, 5, 7, 9, 11, 13];
var target = 9;
var index = binarySearch(array, target);
console.log(index);  // 输出: 4

在上述示例中,binarySearch 函数接受一个已排序的数组 arr 和一个目标值 target 作为参数。它使用循环来不断缩小搜索范围,直到找到目标值或确定它不存在。

初始时,定义左边界 left 为数组起始索引,右边界 right 为数组末尾索引。在每一次循环中,计算中间索引 mid,并将中间元素与目标值进行比较。如果中间元素等于目标值,则返回该索引。如果中间元素小于目标值,则目标值位于右半部分,更新左边界 leftmid + 1。如果中间元素大于目标值,则目标值位于左半部分,更新右边界 rightmid - 1。重复这个过程直到找到目标值或左边界超过右边界。

如果循环结束时仍然没有找到目标值,函数返回 -1 表示目标值不存在于数组中。

需要注意的是,二分查找要求目标数组必须是已排序的。如果数组无序,需要事先进行排序操作。另外,该示例假设目标数组中没有重复元素,如果数组中存在重复元素,该算法可能无法返回特定的索引,可能会返回其中一个匹配的索引。

有了解过 TypeScript 吗,讲一下泛型?

是的,我了解 TypeScript,并且可以为你讲解一下泛型。

在 TypeScript 中,泛型(Generics)是一种用于创建可重用的、类型安全的代码的工具。它允许在定义函数、类或接口时使用类型参数,以便在使用这些函数、类或接口时指定具体的类型。使用泛型可以增加代码的灵活性和重用性,并提供更好的类型检查。

使用泛型,你可以定义一个函数或类,其参数或成员的类型不是具体的类型,而是一种占位符或参数化的类型。这个占位符可以在函数调用或类实例化时被实际的类型替代。

以下是一个 TypeScript 中使用泛型的示例:

// 泛型函数示例
function reverse<T>(array: T[]): T[] {
  return array.reverse();
}

const numbers = [1, 2, 3, 4, 5];
const reversedNumbers = reverse(numbers);
console.log(reversedNumbers);  // 输出: [5, 4, 3, 2, 1]

const strings = ['apple', 'banana', 'cherry'];
const reversedStrings = reverse(strings);
console.log(reversedStrings);  // 输出: ['cherry', 'banana', 'apple']

// 泛型类示例
class Box<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

const numberBox = new Box<number>(42);
console.log(numberBox.getValue());  // 输出: 42

const stringBox = new Box<string>('Hello');
console.log(stringBox.getValue());  // 输出: 'Hello'

在上述示例中,我们定义了一个泛型函数 reverse,它接受一个数组参数,并返回一个反转后的数组。函数的类型参数 T 用作数组元素的类型占位符。在调用函数时,我们可以为类型参数 T 提供具体的类型,例如 numberstring

另外,我们还定义了一个泛型类 Box<T>,它包含一个私有成员变量 value,类型由类型参数 T 决定。通过使用类型参数 T,我们可以创建具有不同类型值的 Box 实例,并通过 getValue 方法返回对应类型的值。

泛型在 TypeScript 中的应用非常广泛,它可以用于函数、类、接口等各种场景,使代码更加灵活和可复用,同时提供类型安全性。

js中的infer 关键字是什么呢?

在 TypeScript 中,infer 是一个用于类型推断的关键字。它通常与条件类型(Conditional Types)一起使用,用于从已知类型中提取或推断其他类型。

条件类型是一种特殊的类型,它根据条件在编译时选择不同的类型。而 infer 关键字允许在条件类型中引入一个待推断的类型参数,并将它绑定到一个新的类型变量上。

以下是一个示例,演示了 infer 关键字的用法:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function sum(a: number, b: number): number {
  return a + b;
}

type Result = ReturnType<typeof sum>;
// Result 的类型被推断为 number,因为 typeof sum 的类型是 (a: number, b: number) => number

console.log(Result);  // 输出: number

怎么理解 JS 的异步?

理解 JavaScript 中的异步概念是很重要的,因为 JavaScript 是一种单线程的语言,而异步操作允许在执行耗时任务时不会阻塞主线程,从而提供更好的用户体验。

在 JavaScript 中,异步指的是代码执行的方式,其中某些操作被安排在后台运行,而不会立即执行并阻塞后续代码的执行。异步操作通常涉及一些需要等待的任务,例如网络请求、文件读写、定时器等。在执行异步操作时,JavaScript 会继续执行后续的代码,而不会等待异步操作完成。

异步操作在 JavaScript 中通常通过回调函数、Promise 对象、Async/Await 等方式来处理。

  1. 回调函数:回调函数是一种常见的处理异步操作的方式。在进行异步操作时,可以将一个函数作为参数传递给异步函数,当操作完成时,异步函数会调用回调函数并传递相应的结果。
function fetchData(callback) {
  setTimeout(() => {
    const data = '异步数据';
    callback(data);
  }, 1000);
}

function handleData(data) {
  console.log('处理数据:', data);
}

fetchData(handleData);  // 调用异步函数,并传递回调函数
  1. Promise 对象:Promise 是一种更现代的处理异步操作的方式,它提供了一种更结构化和可链式的处理异步操作的机制。
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = '异步数据';
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then(data => {
    console.log('处理数据:', data);
  })
  .catch(error => {
    console.error('发生错误:', error);
  });
  1. Async/Await:Async/Await 是 ES2017 引入的一种异步编程的语法糖,它基于 Promise,并提供了一种更简洁和直观的方式来处理异步操作。
async function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = '异步数据';
      resolve(data);
    }, 1000);
  });
}

async function processData() {
  try {
    const data = await fetchData();
    console.log('处理数据:', data);
  } catch (error) {
    console.error('发生错误:', error);
  }
}

processData();

在上述示例中,使用回调函数、Promise 对象或 Async/Await 来处理异步操作时,JavaScript 可以在异步操作执行期间继续执行后续代码,而不会被阻塞。这种方式使得 JavaScript 可以处理复杂的异步任务,同时保持代码的可读性和可维护性。

JS 里面如何实现拖拽的功能。

在 JavaScript 中实现拖拽功能通常涉及以下几个步骤:

  1. 选取拖拽源(Drag Source):确定要拖动的元素或区域,可以通过 DOM 操作或事件监听选择拖拽源。
  2. 监听拖拽事件:为拖拽源元素绑定事件监听器,以便在拖拽过程中触发相应的事件。
  3. 处理拖拽事件:根据拖拽事件的不同阶段(开始、移动、结束等),执行相应的操作来实现拖拽功能。

以下是一个简单的示例代码,展示如何使用 JavaScript 实现基本的拖拽功能:

<!DOCTYPE html>
<html>
<head>
  <style>
    #draggable {
      width: 100px;
      height: 100px;
      background-color: red;
      position: absolute;
      top: 0;
      left: 0;
      cursor: move;
    }
  </style>
</head>
<body>
  <div id="draggable"></div>

  <script>
    // 获取拖拽源元素
    var draggable = document.getElementById('draggable');

    // 定义拖拽开始时的初始位置
    var startX = 0;
    var startY = 0;

    // 监听拖拽开始事件
    draggable.addEventListener('mousedown', dragStart);

    // 拖拽开始处理函数
    function dragStart(event) {
      event.preventDefault();
      startX = event.clientX - draggable.offsetLeft;
      startY = event.clientY - draggable.offsetTop;

      // 监听拖拽过程中的移动事件
      document.addEventListener('mousemove', dragMove);
      // 监听拖拽结束事件
      document.addEventListener('mouseup', dragEnd);
    }

    // 拖拽过程中的移动处理函数
    function dragMove(event) {
      event.preventDefault();

      var newLeft = event.clientX - startX;
      var newTop = event.clientY - startY;

      draggable.style.left = newLeft + 'px';
      draggable.style.top = newTop + 'px';
    }

    // 拖拽结束处理函数
    function dragEnd(event) {
      event.preventDefault();

      // 移除拖拽过程中的事件监听
      document.removeEventListener('mousemove', dragMove);
      document.removeEventListener('mouseup', dragEnd);
    }
  </script>
</body>
</html>

在上述示例中,我们创建了一个红色的可拖拽方块。在拖拽开始时,记录初始位置(startXstartY)和鼠标位置与方块的偏移量。在拖拽过程中,根据鼠标位置和初始偏移量计算方块的新位置,并通过设置 style.leftstyle.top 来实现移动效果。在拖拽结束时,移除事件监听器,停止拖拽操作。

需要注意的是,上述示例只是一个基本的拖拽实现,实际应用中可能需要考虑更多的情况,例如边界限制、拖拽对象。

说一下 CommonJS 和 ES Module 的差异。

CommonJS 和 ES Module(ESM)是两种不同的模块化系统,用于在 JavaScript 中组织和管理代码的方式。它们具有一些差异,包括语法、加载机制和用途。

  1. 语法差异:
  • CommonJS 使用 require()module.exports(或 exports)来导入和导出模块。例如:
// 导入模块
const moduleA = require('./moduleA');

// 导出模块
module.exports = { ... };
  • ESM 使用 importexport 关键字来导入和导出模块。例如:
// 导入模块
import moduleA from './moduleA';

// 导出模块
export default { ... };
  1. 加载机制差异:
    • CommonJS 是同步加载模块的,即在运行时按需加载所需模块,并阻塞后续代码的执行,直到模块加载完成。
    • ESM 是异步加载模块的,即在解析代码时通过静态分析识别模块依赖,并在需要时异步加载模块,不会阻塞后续代码的执行。
  1. 支持环境差异:
    • CommonJS 主要用于服务器端开发,例如 Node.js,它对 CommonJS 模块有很好的支持。
    • ESM 是 ECMAScript 标准的一部分,被广泛支持,并且可以在现代浏览器和 Node.js 中使用。然而,ESM 在浏览器环境中需要使用模块打包工具(如 Webpack、Rollup 等)来转换和加载模块。
  1. 静态分析差异:
    • ESM 的模块依赖关系可以在代码静态分析阶段就确定,这使得工具能够进行更好的优化和分析。
    • CommonJS 的模块加载是在运行时进行的,模块的依赖关系无法在静态分析阶段完全确定。

需要注意的是,虽然 CommonJS 和 ESM 是不同的模块化系统,但它们之间有一些互操作性。在某些环境中(如 Node.js),可以使用适当的配置和工具来使它们能够相互使用和转换。

同一个请求发送多次,如何保证获取的是最后一次的结果呢?

要保证在多次发送同一个请求时,获取的是最后一次请求的结果,可以使用以下方法:

  1. 取消之前的请求:在发送新请求之前,可以检查是否存在之前的请求,并在发送新请求之前取消之前的请求。这样可以确保只有最后一次请求会继续进行,而之前的请求会被中止。
  2. 添加请求标识符:在每次发送请求时,为请求添加一个唯一的标识符(例如一个计数器或时间戳),并将该标识符保存在发送的请求中。当收到响应后,只处理具有最新标识符的响应,忽略之前的响应。
  3. 使用 Promise 或回调函数:将每次发送请求封装为一个返回 Promise 对象或带有回调函数的函数。在每次请求之后,使用 Promise 的 then 方法或回调函数来处理最后一次请求的结果,并忽略之前的结果。

下面是一个使用 Promise 的示例代码:

let currentRequest = null;

function sendRequest() {
  if (currentRequest) {
    // 如果存在之前的请求,则取消它
    currentRequest.cancel();
  }

  // 发送新请求
  const request = new Request();

  currentRequest = request;

  return request.send().then(response => {
    // 处理最后一次请求的结果
    if (request === currentRequest) {
      // 处理最后一次请求的响应
      console.log(response);
    }
  });
}

在上述示例中,sendRequest 函数会检查当前是否存在之前的请求,并取消它。然后,它创建一个新的请求对象,并将其赋值给 currentRequest 变量。在请求的 send 方法返回 Promise 后,使用 Promise 的 then 方法来处理最后一次请求的响应。通过检查 request 对象是否与 currentRequest 相等,可以确保只处理最后一次请求的结果。

请注意,上述示例中的 Request 类和请求取消的实现是假设的,你需要根据实际情况自行实现适当的请求和取消机制。

js算法题,写一个股票买卖求最大收益,只买卖一次。

下面是一个 JavaScript 实现的股票买卖求最大收益的算法,限定只能买卖一次:

function maxProfit(prices) {
  let minPrice = Infinity; // 记录最低价格
  let maxProfit = 0; // 记录最大收益

  for (let i = 0; i < prices.length; i++) {
    if (prices[i] < minPrice) {
      minPrice = prices[i]; // 更新最低价格
    } else if (prices[i] - minPrice > maxProfit) {
      maxProfit = prices[i] - minPrice; // 更新最大收益
    }
  }

  return maxProfit;
}

// 示例使用
const prices = [7, 1, 5, 3, 6, 4];
const profit = maxProfit(prices);
console.log(profit); // 输出: 5

在上述示例中,maxProfit 函数接受一个数组 prices,数组中的每个元素表示某一天的股票价格。算法使用两个变量 minPricemaxProfit 来记录最低价格和最大收益。

遍历股票价格数组,对于每一天的价格,如果它低于当前的最低价格 minPrice,则更新 minPrice。如果当前价格减去最低价格大于当前的最大收益 maxProfit,则更新 maxProfit。最终返回 maxProfit 即为最大收益。

在示例中,股票价格为 [7, 1, 5, 3, 6, 4],最佳的买入时机是第二天的价格 1,卖出时机是第五天的价格 6,最大收益为 6 - 1 = 5。

Webpack 和 Vite,Vite 为什么快?

Webpack 和 Vite 都是现代前端开发中常用的构建工具,用于打包和构建应用程序。它们有一些区别,其中 Vite 被认为更快的原因主要有以下几点:

  1. 开发服务器:Vite 使用原生 ES 模块(ESM)作为开发服务器的构建模块系统,而不是像 Webpack 那样使用传统的模块打包方式。这意味着在开发过程中,Vite 可以直接使用浏览器原生支持的模块加载功能,无需在每次修改代码时重新构建和热更新整个应用程序。这种基于 ESM 的开发服务器可以实现更快的冷启动和热更新速度。
  2. 虚拟模块(Virtual Modules):Vite 引入了虚拟模块的概念。它会拦截非 JavaScript 文件(如 CSS、图片等),并在开发服务器中通过虚拟模块的方式将其作为响应返回给浏览器。这样,当浏览器请求这些非 JavaScript 文件时,无需进行实际的磁盘读取或构建过程,从而提供了更快的开发体验。
  3. 按需编译:Vite 采用按需编译的策略,只在需要时编译被引入的模块,而不是像 Webpack 那样一次性编译整个应用。这意味着 Vite 只会编译当前正在编辑的模块,而不会浪费时间编译其他未被修改的模块,从而提高了开发效率。
  4. 缓存:Vite 使用了高度缓存的设计,它会缓存构建过程中的中间结果,并在后续的构建过程中重复使用这些结果。这种缓存机制减少了重复构建的次数,从而提升了构建速度。

需要注意的是,尽管 Vite 在开发阶段表现更快,但在生产环境中,Webpack 仍然是一个成熟且强大的工具,它提供了更多的功能和灵活性,可以用于处理复杂的应用程序构建需求。因此,在选择构建工具时,需要根据具体的项目需求和规模来做出合适的选择。

前端项目中请求的错误处理是怎么做的?

在前端项目中,请求的错误处理通常涉及以下几个方面:

  1. 错误状态码处理:当接收到响应时,首先检查响应的状态码。常见的状态码如 200(成功)、400(客户端错误)、500(服务器错误)等。根据不同的状态码,可以执行相应的错误处理逻辑,例如显示错误信息、重试请求、跳转到错误页面等。
  2. 异常处理:在请求过程中可能发生网络错误、超时等异常情况。可以通过使用 try-catch 语句块或 Promise 的 catch 方法来捕获异常,并执行适当的错误处理操作,例如显示错误提示、重试请求等。
  3. 统一错误处理:为了避免在每个请求中都编写重复的错误处理代码,可以在项目中实现统一的错误处理机制。可以创建一个错误处理器(Error Handler),用于捕获请求的错误,并根据错误类型进行适当的处理。这可以通过拦截器(Interceptor)或中间件(Middleware)来实现,具体取决于使用的请求库或框架。
  4. 显示错误信息:当发生请求错误时,合适地向用户展示错误信息非常重要。可以通过弹窗、通知、控制台输出、页面提示等方式来向用户显示错误信息,以便用户了解发生的问题并采取适当的行动。
  5. 错误日志记录:在生产环境中,记录请求错误信息是一种良好的实践,可以帮助开发人员跟踪和排查问题。可以将错误信息发送到后端日志系统或使用前端错误监控工具进行收集和分析。

需要根据具体项目的需求和情况来决定错误处理的策略和实现方式。在实际开发中,可以结合使用请求库、框架提供的错误处理机制以及自定义的处理逻辑来处理请求的错误。

前端项目上线后怎么提示用户刷新当前页面?

在前端项目上线后,如果有必要提示用户刷新当前页面,可以使用以下方法之一来实现:

  1. 显示提示消息:在项目的某个页面或布局中,添加一个提示消息,告知用户有新版本可用,建议刷新页面以获取最新内容。可以使用弹窗、通知条、顶部或底部的固定提示栏等方式来显示提示消息。
  2. 监听服务端事件:前端可以通过与后端建立 WebSocket 连接或长轮询等机制,监听服务器端的事件或推送。当有新版本可用时,后端可以向前端发送一个事件或推送,前端收到后,显示相应的提示消息。
  3. 定时检查更新:在前端应用程序中,可以定期向后端发送请求,检查是否有新版本可用。可以使用定时器或定时任务来触发这个检查操作,并在检查到新版本时显示提示消息。

需要注意的是,在提示用户刷新页面时,应当注意用户的体验和操作流程。确保提示信息明确清晰,并提供相应的操作按钮或链接,以便用户可以方便地执行刷新操作。同时,尽量避免过度打扰用户,合理选择时机和方式来提示用户刷新页面。

Nodejs 如何读取一个 20G 大小的文件?

在 Node.js 中读取一个 20GB 大小的文件时,由于文件的大小较大,需要使用适当的技术和方法来处理。以下是一些可以考虑的方法:

  1. 使用流(Stream):使用流可以有效地处理大文件,避免一次性将整个文件加载到内存中。可以使用 Node.js 内置的可读流(Readable Stream)来逐块地读取文件内容,然后对每个块进行处理。这样可以减少内存占用,并且可以在读取文件时逐步处理数据。

以下是一个使用流来读取大文件的示例代码:

const fs = require('fs');

const filePath = 'path/to/large/file.txt';

const readStream = fs.createReadStream(filePath, { encoding: 'utf8' });

readStream.on('data', chunk => {
  // 处理每个数据块
  console.log(chunk);
});

readStream.on('end', () => {
  // 读取完成
  console.log('文件读取完成');
});

readStream.on('error', error => {
  // 处理错误
  console.error('文件读取错误:', error);
});
  1. 使用逐行读取:如果文件是文本文件且按行分隔,可以考虑使用逐行读取的方法,一次读取一行。可以使用第三方模块如 readline 或自定义代码来实现逐行读取的功能。

以下是使用 readline 模块逐行读取大文件的示例代码:

const fs = require('fs');
const readline = require('readline');

const filePath = 'path/to/large/file.txt';

const readStream = fs.createReadStream(filePath, { encoding: 'utf8' });
const lineReader = readline.createInterface({
  input: readStream,
  crlfDelay: Infinity
});

lineReader.on('line', line => {
  // 处理每一行数据
  console.log(line);
});

lineReader.on('close', () => {
  // 读取完成
  console.log('文件读取完成');
});

lineReader.on('error', error => {
  // 处理错误
  console.error('文件读取错误:', error);
});

使用上述方法,可以逐块或逐行地读取大文件,避免一次性加载整个文件到内存中,从而在处理大文件时降低内存占用并提高性能。