2025面试大全(17)

110 阅读39分钟

1. 怎么把函数中的 arguments 转成数组?

在JavaScript中,函数的参数可以通过arguments对象来访问,它是一个类数组对象,但并不是真正的数组。如果你想要将arguments转换为数组,可以使用以下几种方法:

1. 使用 Array.from() 方法

function func() {
  var args = Array.from(arguments);
  // 现在args是一个真正的数组
}

2. 使用展开运算符(...)

function func() {
  var args = [...arguments];
  // 现在args是一个真正的数组
}

3. 使用 Array.prototype.slice.call() 方法

function func() {
  var args = Array.prototype.slice.call(arguments);
  // 现在args是一个真正的数组
}

4. 使用 Array.apply() 方法

function func() {
  var args = Array.apply(null, arguments);
  // 现在args是一个真正的数组
}

示例

function sum() {
  // 将arguments转换为数组
  var numbers = [...arguments];
  
  // 使用数组的方法,例如reduce来计算总和
  return numbers.reduce(function(acc, curr) {
    return acc + curr;
  }, 0);
}
console.log(sum(1, 2, 3, 4));  // 输出 10

以上方法都可以将arguments对象转换为真正的数组,从而可以使用数组的方法和属性。 注意:在现代JavaScript中(ES6及以后),通常推荐使用剩余参数(rest parameters)来代替arguments对象,剩余参数会自动将传入的参数收集到一个真正的数组中:

function sum(...numbers) {
  // numbers已经是一个数组
  return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4));  // 输出 10

使用剩余参数是更现代、更简洁的方式。

2. 如何获取页面的滚动距离值?

在Web开发中,获取页面的滚动距离通常指的是获取窗口(viewport)相对于整个文档的垂直和水平滚动位置。你可以使用以下JavaScript属性来获取这些值:

垂直滚动距离

  • window.pageYOffset:返回当前窗口的垂直滚动位置。
  • document.documentElement.scrollTop:对于标准模式下的文档,返回<html>元素的垂直滚动位置。
  • document.body.scrollTop:对于怪异模式(quirks mode)下的文档,返回<body>元素的垂直滚动位置。

水平滚动距离

  • window.pageXOffset:返回当前窗口的水平滚动位置。
  • document.documentElement.scrollLeft:对于标准模式下的文档,返回<html>元素的水平滚动位置。
  • document.body.scrollLeft:对于怪异模式下的文档,返回<body>元素的水平滚动位置。

获取滚动距离的兼容性代码

为了确保兼容性,你可以同时检查这些属性:

// 获取垂直滚动距离
var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
// 获取水平滚动距离
var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft;

示例

以下是一个简单的示例,展示了如何在滚动事件中获取滚动距离:

window.addEventListener('scroll', function() {
  var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
  var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft;
  
  console.log('Vertical scroll position: ' + scrollTop);
  console.log('Horizontal scroll position: ' + scrollLeft);
});

在这个示例中,当用户滚动页面时,会触发scroll事件,并且打印出当前的垂直和水平滚动位置。

注意事项

  • 在现代浏览器中,window.pageYOffsetwindow.pageXOffset是最常用的属性,因为它们在所有标准模式下都有效。
  • 在IE8及更早的版本中,你需要使用document.documentElement.scrollTopdocument.documentElement.scrollLeft
  • 在某些情况下,例如在移动设备上,滚动事件可能会被节流(throttled)或防抖(debounced),以优化性能。 使用这些属性和技巧,你可以在JavaScript中轻松获取页面的滚动距离。

3. 如何实现SEO优化

SEO是一种通过优化网站内容和结构,提高网站在搜索引擎中的排名,从而增加网站流量的技术。以下是一些实现SEO优化的关键步骤和策略:

1. 关键词研究

  • 识别目标关键词:使用工具如Google Keyword Planner、Ahrefs、SEMrush等来研究潜在用户可能搜索的关键词。
  • 长尾关键词:关注长尾关键词,它们竞争较小,但能带来更精准的流量。

2. 网站结构优化

  • 清晰的导航:确保网站导航简单直观,有助于搜索引擎抓取。
  • URL结构:使用简洁、描述性的URL,包含关键词。
  • sitemap.xml:创建并提交sitemap.xml文件,帮助搜索引擎更好地索引网站。

3. 内容优化

  • 高质量内容:发布原创、有价值、与用户搜索意图相关的内容。
  • 关键词布局:在标题、正文、标题标签(H1、H2等)和元描述中合理使用关键词。
  • 内部链接:通过内部链接帮助搜索引擎理解网站结构,提升页面权重。

4. 技术SEO

  • 网站速度:优化网站加载速度,提高用户体验和搜索引擎排名。
  • 移动优化:确保网站在移动设备上表现良好,采用响应式设计。
  • HTTPS:使用HTTPS协议,增加网站安全性,也是搜索引擎排名因素之一。

5. 外部链接建设

  • 反向链接:获取来自其他高质量网站的反向链接,提高网站权威性。
  • 社交媒体:在社交媒体上分享内容,增加曝光和链接机会。

6. 用户体验(UX)

  • 页面布局:设计清晰的页面布局,提高用户停留时间和互动率。
  • 内容可读性:使用短段落、列表和图文并茂的方式提高内容可读性。

7. 分析与监测

  • 使用工具:如Google Analytics、Google Search Console等监测网站流量和搜索引擎表现。
  • 持续优化:根据数据分析结果,不断调整SEO策略。

8. 遵循搜索引擎指南

  • 避免黑帽SEO:不使用作弊手段,如隐藏文本、关键词堆砌等。
  • 遵循谷歌网页质量指南:关注内容质量,提供有价值的信息。

9. 本地SEO

  • Google我的商家:对于本地业务,注册并优化Google我的商家列表。
  • 本地关键词:在内容中包含地区名称,优化本地搜索。

10. 国际SEO

  • 语言和地区定位:如果面向多个语言或地区,使用hreflang标签进行语言和地区定位。
  • 本地化内容:为不同地区提供本地化内容。

结语

SEO是一个持续的过程,需要不断地学习和适应搜索引擎算法的变化。通过综合运用上述策略,可以有效地提高网站在搜索引擎中的排名,从而吸引更多的有机流量。

4. 如何让Promise.all在抛出异常后依然有效

Promise.all 在遇到任何一个 promise 失败时,会立即抛出异常,并停止处理其他的 promises。如果你想让 Promise.all 在抛出异常后依然有效,即继续等待所有 promises 完成,无论它们是成功还是失败,你可以使用以下几种方法:

1. 使用 Promise.allSettled

Promise.allSettled 是一个类似 Promise.all 的方法,但它会等待所有的 promises 都被解决(无论是成功还是失败)后才返回结果。每个结果都是一个对象,包含 statusvaluereason 属性。

Promise.allSettled([promise1, promise2, promise3]).then(results => {
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Promise ${index + 1} succeeded with value:`, result.value);
    } else {
      console.log(`Promise ${index + 1} failed with reason:`, result.reason);
    }
  });
});

2. 对每个 promise 包装一层 catch

你可以在每个 promise 外面包装一层 catch,这样即使某个 promise 失败,它也会被捕获,并返回一个成功的结果。

Promise.all([
  promise1.catch(error => error),
  promise2.catch(error => error),
  promise3.catch(error => error)
]).then(results => {
  results.forEach((result, index) => {
    if (result instanceof Error) {
      console.log(`Promise ${index + 1} failed with error:`, result);
    } else {
      console.log(`Promise ${index + 1} succeeded with value:`, result);
    }
  });
});

3. 自定义一个类似 Promise.all 的函数

你可以创建一个自定义函数,该函数会处理每个 promise 的结果,无论成功还是失败。

function promiseAllWithErrors(promises) {
  return Promise.all(promises.map(p => p.catch(error => ({ error }))));
}
promiseAllWithErrors([promise1, promise2, promise3]).then(results => {
  results.forEach((result, index) => {
    if (result && result.error) {
      console.log(`Promise ${index + 1} failed with error:`, result.error);
    } else {
      console.log(`Promise ${index + 1} succeeded with value:`, result);
    }
  });
});

结论

使用 Promise.allSettled 是最直接和简洁的方法,因为它内置支持等待所有 promises 完成。如果你需要兼容不支持 Promise.allSettled 的环境,可以使用第二种或第三种方法来模拟类似的行为。这样,即使某些 promises 失败,你也能获取到所有 promises 的结果,并进行相应的处理。

5. 实现一个数字转中文的方法

实现一个将数字转换为中文的方法涉及到将数字的每一位转换成对应的中文数字,并且处理单位(如十、百、千、万等)。以下是一个简单的JavaScript函数,用于将数字转换为中文表示:

function numberToChinese(num) {
  const chineseNumbers = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
  const units = ['', '十', '百', '千'];
  const bigUnits = ['', '万', '亿', '兆'];
  if (num === 0) {
    return chineseNumbers[0];
  }
  let str = '';
  let i = 0;
  while (num > 0) {
    let part = num % 10000;
    let partStr = '';
    if (part === 0) {
      if (!str.startsWith(chineseNumbers[0])) {
        str = chineseNumbers[0] + str;
      }
    } else {
      let j = 0;
      let zeroFlag = false;
      while (part > 0) {
        let digit = part % 10;
        if (digit === 0) {
          if (!zeroFlag && j !== 0) {
            partStr = chineseNumbers[digit] + partStr;
            zeroFlag = true;
          }
        } else {
          partStr = chineseNumbers[digit] + units[j] + partStr;
          zeroFlag = false;
        }
        part = Math.floor(part / 10);
        j++;
      }
      str = partStr + bigUnits[i] + str;
    }
    num = Math.floor(num / 10000);
    i++;
  }
  // 处理“一十”开头的特殊情况
  if (str.startsWith(chineseNumbers[1] + units[1])) {
    str = str.substring(1);
  }
  return str;
}
// 示例
console.log(numberToChinese(0));    // 零
console.log(numberToChinese(10));   // 十
console.log(numberToChinese(123));  // 一百二十三
console.log(numberToChinese(1001)); // 一千零一
console.log(numberToChinese(10010));// 一万零一十

这个函数可以处理0到99999999之间的数字。对于更大的数字,可以继续扩展bigUnits数组。函数的基本逻辑是将数字分成每四位一组,然后将每组的数字转换成中文,最后将大单位(万、亿、兆)加到每组转换后的中文数字后面。 请注意,这个实现并没有处理所有可能的中文数字表达方式,比如“两”在中文中有时用来代替“二”,以及一些口语化的表达。这个函数提供了一个基本的转换功能,可以根据需要进一步优化和扩展。

6. react是否支持给标签设置自定义的属性,比如给video标签设置webkit-playsinline?

是的,React支持给标签设置自定义属性,包括给video标签设置webkit-playsinline属性。 在React中,你可以通过在组件的属性中直接设置自定义属性来为HTML标签添加非标准的或自定义的属性。对于webkit-playsinline这样的属性,你可以像这样使用:

<video webkit-playsinline="true" /* 其他属性 */>
  <source src="movie.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

或者,如果你更喜欢使用驼峰式命名法来设置属性(这是React推荐的属性命名方式),你可以这样写:

<video webkitPlaysInline={true} /* 其他属性 */>
  <source src="movie.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

在这两种情况下,React都会将属性正确地渲染到DOM中。 需要注意的是,webkit-playsinline是一个非标准的属性,主要用于在iOS设备上实现视频的内联播放。如果你希望视频在所有支持该属性的浏览器上都能内联播放,你可能还需要添加其他类似的属性,如playsinline(标准属性)和x5-playsinline(用于腾讯X5内核浏览器)。 例如:

<video
  playsinline="true"
  webkit-playsinline="true"
  x5-playsinline="true"
  /* 其他属性 */
>
  <source src="movie.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

这样,你就可以确保视频在尽可能多的环境下都能内联播放。

7. 使用css实现一个无限循环动画

要使用CSS实现一个无限循环动画,你可以使用@keyframes规则来创建动画序列,并使用animation-nameanimation-iteration-count属性来应用动画并使其无限循环。以下是一个简单的例子,展示了如何实现一个无限循环的旋转动画:

/* 定义关键帧 */
@keyframes rotateAnimation {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
/* 应用动画的元素 */
.animated-element {
  width: 100px;
  height: 100px;
  background-color: red;
  /* 动画名称 */
  animation-name: rotateAnimation;
  /* 动画持续时间 */
  animation-duration: 2s;
  /* 无限循环 */
  animation-iteration-count: infinite;
  /* 动画节奏,这里设置为匀速 */
  animation-timing-function: linear;
}
<div class="animated-element"></div>

在这个例子中,.animated-element类将应用于一个div元素。这个div将会无限循环地旋转,每次旋转需要2秒钟,并且是以匀速进行。 你可以根据需要调整@keyframes中的动画序列,以及animation-durationanimation-timing-function等属性来改变动画的效果。 如果你想要更复杂的动画,可以在@keyframes中添加更多的关键帧。例如,一个来回移动的动画:

@keyframes moveAnimation {
  0% {
    transform: translateX(0);
  }
  50% {
    transform: translateX(100px);
  }
  100% {
    transform: translateX(0);
  }
}
.moving-element {
  width: 50px;
  height: 50px;
  background-color: blue;
  animation-name: moveAnimation;
  animation-duration: 4s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}
<div class="moving-element"></div>

在这个例子中,.moving-element类的div将会在水平方向上无限循环地来回移动。 通过组合不同的CSS属性和关键帧,你可以创造出各种复杂的无限循环动画效果。

8. 用js实现二叉树的定义和基本操作

在JavaScript中,我们可以使用对象来表示二叉树的节点,并通过函数来实现二叉树的基本操作。以下是一个简单的二叉树实现,包括节点的定义、插入节点、中序遍历、前序遍历和后序遍历。

二叉树节点的定义

function TreeNode(value) {
  this.value = value;
  this.left = null;
  this.right = null;
}

二叉树的插入操作

我们可以定义一个函数来插入节点,这里以插入值为基准,小于父节点的值插入到左子树,大于或等于父节点的值插入到右子树。

function insertNode(root, value) {
  if (root === null) {
    return new TreeNode(value);
  }
  if (value < root.value) {
    root.left = insertNode(root.left, value);
  } else {
    root.right = insertNode(root.right, value);
  }
  return root;
}

二叉树的遍历操作

中序遍历

中序遍历会先访问左子树,然后访问根节点,最后访问右子树。

function inorderTraversal(root, callback) {
  if (root !== null) {
    inorderTraversal(root.left, callback);
    callback(root.value);
    inorderTraversal(root.right, callback);
  }
}
前序遍历

前序遍历会先访问根节点,然后访问左子树,最后访问右子树。

function preorderTraversal(root, callback) {
  if (root !== null) {
    callback(root.value);
    preorderTraversal(root.left, callback);
    preorderTraversal(root.right, callback);
  }
}
后序遍历

后序遍历会先访问左子树,然后访问右子树,最后访问根节点。

function postorderTraversal(root, callback) {
  if (root !== null) {
    postorderTraversal(root.left, callback);
    postorderTraversal(root.right, callback);
    callback(root.value);
  }
}

使用示例

// 创建一个空的二叉树根节点
let root = null;
// 插入节点
root = insertNode(root, 5);
root = insertNode(root, 3);
root = insertNode(root, 7);
root = insertNode(root, 2);
root = insertNode(root, 4);
root = insertNode(root, 6);
root = insertNode(root, 8);
// 中序遍历
console.log('Inorder traversal:');
inorderTraversal(root, value => console.log(value));
// 前序遍历
console.log('Preorder traversal:');
preorderTraversal(root, value => console.log(value));
// 后序遍历
console.log('Postorder traversal:');
postorderTraversal(root, value => console.log(value));

这个简单的实现展示了如何定义二叉树节点、插入新节点以及进行三种基本的遍历操作。你可以根据需要扩展这个实现,例如添加查找、删除节点等功能。

9. 非递归遍历二叉树

在TypeScript中实现非递归遍历二叉树,我们可以使用栈(对于前序和后序遍历)和队列(对于层序遍历)来辅助。以下是一个简单的TypeScript实现,包括二叉树节点的定义以及前序、中序、后序和层序遍历的非递归实现。

二叉树节点的定义

class TreeNode {
  value: any;
  left: TreeNode | null;
  right: TreeNode | null;
  constructor(value: any) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

非递归前序遍历

function preorderTraversal(root: TreeNode | null): any[] {
  if (!root) return [];
  const stack = [root];
  const result = [];
  while (stack.length) {
    const node = stack.pop()!;
    result.push(node.value);
    if (node.right) stack.push(node.right);
    if (node.left) stack.push(node.left);
  }
  return result;
}

非递归中序遍历

function inorderTraversal(root: TreeNode | null): any[] {
  const stack = [];
  const result = [];
  let current = root;
  while (stack.length || current) {
    while (current) {
      stack.push(current);
      current = current.left;
    }
    current = stack.pop()!;
    result.push(current.value);
    current = current.right;
  }
  return result;
}

非递归后序遍历

function postorderTraversal(root: TreeNode | null): any[] {
  if (!root) return [];
  const stack = [root];
  const result = [];
  while (stack.length) {
    const node = stack.pop()!;
    result.unshift(node.value); // 将值插入结果数组的前面
    if (node.left) stack.push(node.left);
    if (node.right) stack.push(node.right);
  }
  return result;
}

非递归层序遍历(广度优先遍历)

function levelOrderTraversal(root: TreeNode | null): any[] {
  if (!root) return [];
  const queue = [root];
  const result = [];
  while (queue.length) {
    const node = queue.shift()!;
    result.push(node.value);
    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }
  return result;
}

使用示例

// 创建一个空的二叉树根节点
let root: TreeNode | null = new TreeNode(5);
root.left = new TreeNode(3);
root.right = new TreeNode(7);
root.left.left = new TreeNode(2);
root.left.right = new TreeNode(4);
root.right.left = new TreeNode(6);
root.right.right = new TreeNode(8);
// 非递归前序遍历
console.log('Preorder traversal:', preorderTraversal(root));
// 非递归中序遍历
console.log('Inorder traversal:', inorderTraversal(root));
// 非递归后序遍历
console.log('Postorder traversal:', postorderTraversal(root));
// 非递归层序遍历
console.log('Level order traversal:', levelOrderTraversal(root));

这个实现展示了如何在TypeScript中定义二叉树节点以及如何进行非递归的前序、中序、后序和层序遍历。你可以根据需要进一步扩展这个实现,例如添加查找、删除节点等功能。

10. 什么是同步和异步?

同步和异步是编程中描述任务执行方式的两个概念,它们主要涉及到任务执行的顺序和程序等待任务完成的方式。

同步(Synchronous)

同步指的是程序按照代码的顺序一步一步执行,每一步都必须等待上一步完成后才能开始。在同步操作中,如果某个任务需要较长时间来完成,程序会一直等待这个任务完成,期间不能执行其他任务。 示例:

console.log("开始执行");
// 同步任务
for (let i = 0; i < 1000000000; i++) {
  // some heavy computation
}
console.log("执行结束");

在这个例子中,console.log("执行结束")会在for循环完成后才执行,程序会阻塞在for循环上,直到它完成。

异步(Asynchronous)

异步指的是程序可以同时执行多个任务,不必等待一个任务完成后再开始下一个任务。在异步操作中,程序可以发起一个任务,然后继续执行其他代码,而不需要等待这个任务完成。当任务完成时,程序会通过回调函数、事件、Promise等方式来处理结果。 示例:

console.log("开始执行");
// 异步任务
setTimeout(() => {
  console.log("异步任务完成");
}, 1000);
console.log("执行结束");

在这个例子中,setTimeout是一个异步操作,它会在1秒后执行回调函数,但程序不会等待这1秒,而是会立即继续执行console.log("执行结束")

同步与异步的区别

  1. 执行顺序:同步操作按顺序执行,异步操作可以并行执行。
  2. 阻塞与非阻塞:同步操作是阻塞的,异步操作是非阻塞的。
  3. 效率:异步操作通常更高效,因为它不会让程序等待。
  4. 复杂性:异步代码通常比同步代码更复杂,因为需要处理回调、事件、Promise等。

异步编程的常见模式

  • 回调函数:将函数作为参数传递,在任务完成时调用。
  • 事件:使用事件监听器来响应异步操作完成的事件。
  • Promise:表示一个异步操作的最终完成(或失败),以及其结果值。
  • Async/Await:基于Promise的语法糖,使异步代码看起来更像是同步代码。 理解同步和异步对于编写高效、响应快的程序非常重要,尤其是在处理I/O操作、网络请求等可能需要较长时间完成的任务时。

11. 说说你的ES6-ES12的了解

ES6-ES12是指ECMAScript从第6版到第12版的更新,这些版本为JavaScript带来了许多新的特性和改进。以下是我对这些版本的一些主要特性的了解:

ES6(ECMAScript 2015)

  • let和const:提供了块级作用域的变量声明方式。
  • 箭头函数:简化了函数的声明和this的处理。
  • 模板字符串:允许更方便地创建多行字符串和插值表达式。
  • 解构赋值:允许从数组或对象中提取多个值。
  • 默认参数值:函数参数可以设置默认值。
  • 剩余参数和展开运算符:允许处理不确定数量的参数或展开数组。
  • Promise:用于异步编程,表示一个未来可能会完成的操作。
  • 类(Class):提供了更接近传统面向对象语言的语法。
  • 模块(Module):允许将代码分割成不同的文件,并通过import和export进行管理。

ES7(ECMAScript 2016)

  • Array.prototype.includes:检查数组是否包含某个值。
  • 指数运算符()**:简化了指数运算的语法。

ES8(ECMAScript 2017)

  • 异步函数(Async/Await):基于Promise的语法糖,使异步代码更易于编写和理解。
  • 共享内存和原子操作:提供了更底层的多线程支持。
  • Object.values/Object.entries:方便地获取对象的值和键值对。

ES9(ECMAScript 2018)

  • 异步迭代:允许使用for-await-of循环遍历异步迭代对象。
  • Promise.finally:无论Promise是解决还是拒绝,都会执行的回调。
  • Rest/Spread属性:允许在对象字面量中使用剩余参数和展开运算符。
  • 正则表达式改进:包括命名捕获组、反向引用等。

ES10(ECMAScript 2019)

  • Array.prototype.flat/flatMap:用于平铺和映射平铺数组。
  • 可选的catch绑定:允许在catch子句中省略异常变量的绑定。
  • String.prototype.trimStart/trimEnd:去除字符串两端的空白字符。
  • Object.fromEntries:将键值对列表转换为对象。

ES11(ECMAScript 2020)

  • 可选的链式调用(Optional Chaining):允许读取嵌套对象属性时避免报错。
  • 空值合并运算符(Nullish Coalescing):用于处理null或undefined的情况。
  • 国际化的改进:包括国际化的支持和对Intl.DateTimeFormat的扩展。
  • Promise.allSettled:等待所有Promise都完成,无论它们是解决还是拒绝。

ES12(ECMAScript 2021)

  • 逻辑赋值运算符:结合了逻辑运算符和赋值运算符。
  • String.prototype.replaceAll:允许替换所有匹配的字符串,而不仅仅是第一个。
  • Promise.any:只要有一个Promise解决,就返回那个已经解决的Promise。
  • 数字分隔符:允许在数字中添加下划线以提高可读性。 这些新特性和改进使得JavaScript更加现代化、强大和易于使用。了解这些特性有助于编写更高效、更简洁的代码。

12. 怎么进行站点内的图片性能优化?

站点内的图片性能优化是提高网页加载速度和用户体验的重要环节。以下是一些有效的图片性能优化策略:

1. 选择合适的图片格式

  • JPEG:适合照片和色彩丰富的图像,压缩比高。
  • PNG:适合需要透明背景的图像,压缩比不如JPEG。
  • WebP:现代格式,提供比JPEG和PNG更好的压缩率,支持透明度。
  • AVIF:新的图像格式,提供更高的压缩率,但兼容性有待提高。

2. 压缩图片

  • 使用工具如TinyPNG、ImageOptim等在线或离线压缩图片。
  • 利用CSS或JavaScript库进行实时压缩(如libjpeg)。

3. 使用适当的分辨率

  • 不要使用比显示尺寸大的图片。
  • 为不同设备提供不同分辨率的图片(响应式图片)。

4. 利用现代图片标签

  • 使用<picture>元素为不同屏幕尺寸和分辨率提供最合适的图片。
  • 使用srcsetsizes属性让浏览器选择最合适的图片源。

5. 延迟加载(Lazy Loading)

  • only loaded when they are about to enter the viewport.
  • 可以使用HTML的loading="lazy"属性或JavaScript库实现。

6. 使用CDN

  • 内容分发网络(CDN)可以加快图片的加载速度,特别是对于地理位置分散的用户。

7. 缓存策略

  • 设置合理的HTTP缓存头,如Cache-Control,以减少重复加载。

8. 图片优化工具和库

  • 使用自动化工具如Imagemin、Gulp等进行构建时自动优化。
  • 利用Webpack的loader进行图片压缩。

9. 移除不必要的元数据

  • 从图片中移除EXIF数据等不必要的元信息,以减少文件大小。

10. 使用SVG

  • 对于矢量图形,使用SVG格式,它可以无损缩放且通常文件较小。

11. 监控和分析

  • 使用工具如Google PageSpeed Insights、Lighthouse等分析图片性能。
  • 监控网站性能,找出加载慢的图片并进行优化。

12. 图片精灵(Sprite Sheets)

  • 将多个小图标合并成一张大图,减少HTTP请求次数。

13. 避免使用Base64编码的图片

  • Base64编码的图片会增加文件大小,除非图片非常小,否则不建议使用。 通过实施这些策略,可以显著提高网站的性能,减少页面加载时间,提升用户体验。记得在优化过程中保持图片的质量和视觉效果。

13. ['1','2','3'].map(parseInt) 的返回值是什么?

['1','2','3'].map(parseInt) 的返回值是 [1, NaN, NaN]。 这是因为 map 方法会遍历数组中的每个元素,并对其执行提供的函数。在这个例子中,提供的函数是 parseIntparseInt 函数有两个参数:第一个是要解析的字符串,第二个是基数(radix)。当第二个参数省略时,parseInt 会根据字符串的格式来决定基数。如果字符串以 "0x" 开头,则基数为16(十六进制);否则,基数为10(十进制)。 在 map 方法的回调函数中,除了当前元素外,还传递了索引和整个数组作为参数。因此,parseInt 接收到的第二个参数实际上是数组的索引,而不是我们期望的基数。 具体执行过程如下:

  1. parseInt('1', 0):因为索引为0,parseInt 认为是十进制,所以返回 1
  2. parseInt('2', 1):基数不能为1,parseInt 无法解析,所以返回 NaN
  3. parseInt('3', 2):因为索引为2,parseInt 尝试以二进制解析字符串 "3",但 "3" 不是有效的二进制数字,所以返回 NaN。 因此,最终的结果是 [1, NaN, NaN]。 为了避免这种情况,可以提供一个总是返回10的函数作为 parseInt 的第二个参数,例如:
['1','2','3'].map(function(item) {
  return parseInt(item, 10);
});

这样就能得到预期的结果 [1, 2, 3]

14. POST请求的 Content-Type 常见的有哪几种?

在HTTP POST请求中,Content-Type头用于指示发送给服务器的数据类型。常见的Content-Type值包括:

  1. application/x-www-form-urlencoded
    • 这是默认的表单提交方式,数据被编码成键值对形式,例如key1=value1&key2=value2
  2. multipart/form-data
    • 用于提交包含文件上传的表单数据。数据被分割成多个部分,每个部分都有自己的头信息,例如Content-Disposition
  3. application/json
    • 表示发送的数据是JSON格式,这是一种轻量级的数据交换格式,常用于API通信。
  4. text/plain
    • 表示发送的数据是纯文本格式,不包含任何特殊格式或编码。
  5. application/xmltext/xml
    • 表示发送的数据是XML格式,适用于XML数据交换。
  6. application/graphql
    • 用于GraphQL查询,表示发送的数据是GraphQL格式。
  7. application/octet-stream
    • 表示发送的数据是二进制流,通常用于文件下载或上传。
  8. application/x-www-form-urlencoded
    • 类似于application/x-www-form-urlencoded,但用于编码非ASCII字符。
  9. application/vnd.api+json
    • 用于JSON API规范,表示发送的数据是符合JSON API格式的JSON。
  10. application/pdfimage/jpegimage/png等:
    • 这些是特定类型的MIME类型,用于指示发送的是特定格式的文件,如PDF文档或图像。 选择哪种Content-Type取决于你发送的数据类型以及服务器期望接收的数据格式。例如,如果你正在提交一个包含文件上传的表单,你应该使用multipart/form-data。如果你正在与一个期望JSON数据的API通信,你应该使用application/json。 在实际应用中,application/x-www-form-urlencodedmultipart/form-data是表单提交中最常用的两种Content-Type,而application/json则在现代Web API中非常常见。

15. 怎么触发BFC,BFC有什么应用场景?

触发BFC的条件: 在CSS中,BFC(Block Formatting Context,块级格式化上下文)可以通过以下方式触发:

  1. float的值不为none
  2. overflow的值不为visible(即hiddenscrollauto)。
  3. display的值为inline-blocktable-celltable-captionflexinline-flexgridinline-grid
  4. position的值为absolutefixedBFC的应用场景: BFC在CSS布局中有很多有用的应用场景,主要包括:
  5. 防止垂直margin合并
    • 当两个块级元素上下排列时,它们的垂直margin会合并。通过触发BFC,可以防止这种合并行为,使布局更符合预期。
  6. 清除浮动
    • 当父元素内部有浮动元素时,父元素可能无法正确包含这些浮动元素,导致高度塌陷。通过在父元素上触发BFC,可以包含浮动元素,使父元素高度正常。
  7. 自适应两栏布局
    • 利用BFC和浮动的特性,可以创建一个自适应宽度的两栏布局。一栏浮动,另一栏触发BFC,这样浮动栏的宽度由其内容决定,而BFC栏则自适应剩余宽度。
  8. 防止文字环绕
    • 当文本和浮动元素相邻时,文本可能会环绕浮动元素。通过在文本容器上触发BFC,可以防止这种环绕行为,使文本正常显示在浮动元素下方。
  9. 复杂的布局需求
    • 在一些复杂的布局中,可能需要利用BFC来控制元素的布局行为,以达到特定的设计效果。 示例代码:
/* 触发BFC */
.bfc-container {
  overflow: hidden; /* 或其他触发BFC的方式 */
}
/* 清除浮动 */
.float-container::after {
  content: '';
  display: block;
  clear: both;
}
/* 自适应两栏布局 */
.left-column {
  float: left;
  width: 300px; /* 固定宽度 */
}
.right-column {
  overflow: hidden; /* 触发BFC,自适应剩余宽度 */
}

在实际开发中,根据具体需求选择合适的方式触发BFC,并利用BFC的特性来解决布局问题。

16. 怎么预防按钮的重复点击?

预防按钮的重复点击可以通过多种方式实现,以下是一些常见的方法:

1. 使用disabled属性

在按钮被点击时,立即将按钮的disabled属性设置为true,直到操作完成后再将其设置为false。

<button id="myButton">点击我</button>
<script>
document.getElementById('myButton').addEventListener('click', function() {
  this.disabled = true;
  // 执行某些操作...
  setTimeout(() => {
    this.disabled = false; // 假设操作完成,重新启用按钮
  }, 3000); // 假设操作需要3秒完成
});
</script>

2. 使用变量控制

通过一个变量来控制按钮的点击状态。

let isClicked = false;
document.getElementById('myButton').addEventListener('click', function() {
  if (isClicked) return; // 如果已经点击过,则直接返回
  isClicked = true; // 设置点击状态为true
  // 执行某些操作...
  setTimeout(() => {
    isClicked = false; // 操作完成,重置点击状态
  }, 3000);
});

3. 使用CSS类

通过添加一个CSS类来改变按钮的样式,并阻止进一步点击。

<button id="myButton">点击我</button>
<style>
.disabled {
  pointer-events: none; /* 防止点击 */
  opacity: 0.5; /* 改变样式,表示不可用 */
}
</style>
<script>
document.getElementById('myButton').addEventListener('click', function() {
  this.classList.add('disabled');
  // 执行某些操作...
  setTimeout(() => {
    this.classList.remove('disabled'); // 操作完成,移除禁用样式
  }, 3000);
});
</script>

4. 使用防抖(debounce)或节流(throttle)

如果按钮的点击会导致一些高频操作,可以使用防抖或节流函数来限制函数的执行频率。

function debounce(func, wait) {
  let timeout;
  return function() {
    const context = this, args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, wait);
  };
}
document.getElementById('myButton').addEventListener('click', debounce(function() {
  // 执行某些操作...
}, 3000));

5. 使用锁机制

在一些复杂的应用中,可以使用锁机制来控制按钮的点击。

let lock = false;
document.getElementById('myButton').addEventListener('click', function() {
  if (lock) return;
  lock = true;
  // 执行某些操作...
  setTimeout(() => {
    lock = false; // 操作完成,释放锁
  }, 3000);
});

选择哪种方法取决于你的具体需求和应用场景。在大多数情况下,使用disabled属性或变量控制是最简单直接的方法。

17. JavaScript 中如何取消请求

在JavaScript中,取消请求通常涉及到使用XMLHttpRequestfetch API。以下是两种情况下如何取消请求的方法:

使用 XMLHttpRequest

XMLHttpRequest 对象有一个 abort 方法,可以用来取消正在进行的请求。

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/api/data', true);
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    console.log('Request completed');
  }
};
xhr.send();
// 取消请求
xhr.abort();

调用 abort 方法后,XMLHttpRequestreadyState 会变为 0UNSENT),并且会触发 abort 事件。

使用 fetch API

fetch API 返回的是一个 Promise,它本身并不直接提供取消请求的方法。但是,你可以使用 AbortController 接口来取消 fetch 请求。

const controller = new AbortController();
const signal = controller.signal;
fetch('https://example.com/api/data', { signal })
  .then(response => {
    if (response.ok) {
      return response.json();
    }
    throw new Error('Network response was not ok.');
  })
  .then(data => console.log(data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('The user aborted a request.');
    } else {
      console.log('Fetch error:', error.message);
    }
  });
// 取消请求
controller.abort();

当调用 controller.abort() 时,如果请求尚未完成,它会立即被取消,并且 fetch 的 Promise 会拒绝(reject)一个名为 AbortError 的错误。

注意事项

  • 在使用 AbortController 时,确保每个请求都使用一个新的实例,因为一旦调用 abort 方法,同一个 AbortController 的所有关联请求都会被取消。
  • 取消请求后,应该处理可能出现的错误,例如在 catch 块中检查错误类型。
  • 不是所有的浏览器都支持 AbortController。对于不支持的环境,你可能需要使用 Polyfill 或者回退到其他方法。 通过这些方法,你可以在需要时取消正在进行的HTTP请求,以避免不必要的网络活动或处理用户操作导致的请求中断。

18. 说说你对前端鉴权的理解

前端鉴权是指在前端应用中实现用户身份验证和授权的一系列机制,以确保只有合法用户才能访问受保护的资源。前端鉴权通常与后端鉴权配合使用,共同构建一个安全的应用系统。以下是前端鉴权的一些关键概念和常见实现方式:

关键概念

  1. 身份验证(Authentication)
    • 确认用户的身份,证明“你是你声称的那个人”。
    • 常见方式包括用户名密码登录、多因素认证(MFA)、生物识别等。
  2. 授权(Authorization)
    • 确定用户是否有权限执行某个操作或访问某个资源。
    • 常见模型包括基于角色的访问控制(RBAC)、基于属性的访问控制(ABAC)等。
  3. 凭证(Credentials)
    • 证明用户身份的信息,如用户名、密码、令牌(Token)等。
  4. 会话(Session)
    • 用户登录后建立的一个持久连接,用于在多次请求间保持用户的认证状态。
  5. 令牌(Token)
    • 一种轻量级的认证方式,通常用于无状态的API认证,如JWT(JSON Web Tokens)。

常见实现方式

  1. Cookie-Session机制
    • 用户登录后,服务器生成一个SessionID,并将其通过Cookie发送给客户端。
    • 客户端后续请求时会携带该Cookie,服务器通过SessionID识别用户身份。
  2. Token机制
    • 用户登录后,服务器生成一个Token(如JWT)发送给客户端。
    • 客户端存储Token,并在后续请求的HTTP头部(如Authorization字段)中携带Token。
    • 服务器验证Token的有效性,从而识别用户身份。
  3. OAuth
    • 一种授权框架,允许第三方应用在不需要用户密码的情况下访问其在其他服务提供商上的信息。
    • 常用于社交登录、第三方API访问等场景。
  4. API密钥
    • 为每个用户或应用生成一个密钥,用于调用API时证明身份。
    • 通常用于服务器间的认证。
  5. 前端加密
    • 在客户端对敏感信息(如密码)进行加密,然后再发送到服务器,以增加传输过程的安全性。

前端鉴权的挑战

  • 安全性:前端代码容易被篡改,需要确保鉴权机制的安全性。
  • 跨域问题:前端应用可能需要与多个后端服务进行交互,需要处理跨域请求的鉴权问题。
  • 用户体验:鉴权过程应尽量简洁、快速,避免对用户造成过多干扰。

最佳实践

  • 使用HTTPS:确保数据在传输过程中的安全性。
  • 避免在前端存储敏感信息:如密码、密钥等,应仅存储非敏感的凭证(如Token)。
  • 定期更新凭证:如Token的有效期,以减少安全风险。
  • 实现登出机制:允许用户主动结束会话,注销身份。
  • 监控和日志:记录鉴权相关的操作和异常,以便于审计和故障排查。 前端鉴权是保障应用安全的重要环节,需要结合具体的应用场景和需求,选择合适的鉴权机制,并不断优化和更新以应对新的安全挑战。

19. 开发的过程中你用到过哪些设计模式?

在开发过程中,设计模式是非常有用的工具,它们可以帮助开发者解决常见的软件设计问题,提高代码的可维护性、可读性和可扩展性。以下是一些常见的设计模式,以及它们在开发中可能的应用场景:

创建型模式

  1. 单例模式(Singleton)
    • 确保一个类只有一个实例,并提供一个全局访问点。
    • 应用场景:数据库连接池、日志记录器等。
  2. 工厂模式(Factory Method)
    • 定义一个接口用于创建对象,但让子类决定实例化哪个类。
    • 应用场景:对象创建逻辑复杂、需要动态创建对象等。
  3. 抽象工厂模式(Abstract Factory)
    • 提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
    • 应用场景:需要创建多个产品族、产品等级结构等。
  4. 建造者模式(Builder)
    • 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
    • 应用场景:复杂对象的构造、如文档生成器等。
  5. 原型模式(Prototype)
    • 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
    • 应用场景:对象创建成本较大、需要复制对象等。

结构型模式

  1. 适配器模式(Adapter)
    • 将一个类的接口转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。
    • 应用场景:集成第三方库、兼容旧系统等。
  2. 桥接模式(Bridge)
    • 将抽象部分与实现部分分离,使它们可以独立地变化。
    • 应用场景:跨平台应用、多种实现方式等。
  3. 组合模式(Composite)
    • 将对象组合成树形结构以表示“部分-整体”的层次结构,使得客户可以统一使用单个对象和组合对象。
    • 应用场景:文件系统、GUI组件等。
  4. 装饰器模式(Decorator)
    • 动态地给一个对象添加一些额外的职责,而不改变其接口。
    • 应用场景:扩展对象功能、如日志装饰器、缓存装饰器等。
  5. 外观模式(Facade)
    • 为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
    • 应用场景:简化复杂系统接口、如API封装等。
  6. 享元模式(Flyweight)
    • 为运用共享技术有效地支持大量细粒度的对象。
    • 应用场景:文本编辑器中的字符表示、大量相似对象等。
  7. 代理模式(Proxy)
    • 为其他对象提供一种代理以控制对这个对象的访问。
    • 应用场景:远程代理、虚拟代理、保护代理等。

行为型模式

  1. 模板方法模式(Template Method)
    • 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
    • 应用场景:算法框架、代码复用等。
  2. 策略模式(Strategy)
    • 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
    • 应用场景:算法选择、如排序策略、支付方式等。
  3. 状态模式(State)
    • 允许一个对象在其内部状态改变时改变它的行为。
    • 应用场景:状态机、如订单状态管理等。
  4. 观察者模式(Observer)
    • 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
    • 应用场景:事件处理、订阅发布模式等。
  5. 迭代器模式(Iterator)
    • 提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
    • 应用场景:遍历集合、如列表、树等。
  6. 责任链模式(Chain of Responsibility)
    • 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。
    • 应用场景:请求处理、如日志级别过滤等。
  7. 命令模式(Command)
    • 将一个请求封装为一个对象,从而让你使用不同的请求、队列或日志请求来参数化其他对象。
    • 应用场景:请求排队、操作记录等。
  8. 备忘录模式(Memento)
    • 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
    • 应用场景:撤销操作、状态保存等。
  9. 访问者模式(Visitor)
    • 表示一个作用于某对象结构

20. 路径总和

"路径总和"是一个常见的算法问题,通常指的是在二叉树中找出从根节点到叶节点的路径,使得路径上节点的值之和等于给定的目标和。 以下是一个使用TypeScript实现的示例,该示例定义了一个二叉树节点类 TreeNode 和一个函数 hasPathSum,该函数检查是否存在从根节点到叶节点的路径,其路径和等于给定的目标和。

// 定义二叉树节点类
class TreeNode {
    val: number;
    left: TreeNode | null;
    right: TreeNode | null;
    constructor(val: number = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}
// 检查是否存在路径总和等于目标和的函数
function hasPathSum(root: TreeNode | null, targetSum: number): boolean {
    // 如果根节点为空,直接返回false
    if (root === null) {
        return false;
    }
    // 如果是叶节点,检查路径和是否等于目标和
    if (root.left === null && root.right === null) {
        return targetSum === root.val;
    }
    // 递归检查左子树和右子树
    return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}
// 示例使用
const root = new TreeNode(5);
root.left = new TreeNode(4);
root.right = new TreeNode(8);
root.left.left = new TreeNode(11);
root.left.left.left = new TreeNode(7);
root.left.left.right = new TreeNode(2);
root.right.left = new TreeNode(13);
root.right.right = new TreeNode(4);
root.right.right.right = new TreeNode(1);
const targetSum = 22;
console.log(hasPathSum(root, targetSum)); // 应该输出true,因为存在路径 5->4->11->2 的和为22

在这个实现中,hasPathSum 函数通过递归的方式遍历二叉树。对于每个节点,它检查是否是叶节点(即没有子节点),如果是,则检查从根节点到该叶节点的路径和是否等于目标和。如果不是叶节点,它将继续递归地检查其子节点,同时从目标和中减去当前节点的值。 请注意,这个实现假设二叉树中的节点值都是非负数。如果节点值可以是负数,那么问题会变得更加复杂,因为负数可能会使路径和减少,从而需要更多的检查来确保找到有效的路径。

21. 怎么实现大型文件上传?

大型文件上传通常需要考虑网络稳定性、上传速度、服务器存储等问题。以下是一种常见的实现大型文件上传的方案,包括前端和后端的处理:

前端实现

  1. 文件分片
    • 将大文件分割成多个小文件块(例如,每块1MB或5MB)。
    • 使用Blob对象的slice方法进行分片。
  2. 并发上传
    • 同时上传多个文件块,以提高上传速度。
    • 使用XMLHttpRequest或Fetch API进行异步上传。
  3. 断点续传
    • 记录已上传的文件块信息(例如,使用localStorage或IndexedDB)。
    • 在中断后重新开始上传时,只上传未完成的文件块。
  4. 进度显示
    • 监听每个文件块的上传进度事件,更新整体上传进度。
  5. 请求封装
    • 对每个文件块的上传请求进行封装,包括设置请求头、处理响应等。 以下是一个简单的前端实现示例(使用JavaScript):
function uploadLargeFile(file) {
  const CHUNK_SIZE = 1 * 1024 * 1024; // 分片大小1MB
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
  for (let i = 0; i < totalChunks; i++) {
    const start = i * CHUNK_SIZE;
    const end = Math.min(file.size, start + CHUNK_SIZE);
    const chunk = file.slice(start, end);
    const formData = new FormData();
    formData.append('file', chunk);
    formData.append('filename', file.name);
    formData.append('chunkIndex', i);
    formData.append('totalChunks', totalChunks);
    fetch('http://your-server.com/upload', {
      method: 'POST',
      body: formData,
    })
    .then(response => response.json())
    .then(data => {
      console.log(`Chunk ${i} uploaded successfully`);
      // 处理进度和断点续传逻辑
    })
    .catch(error => {
      console.error('Upload error:', error);
    });
  }
}
// 使用示例
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', (event) => {
  const file = event.target.files[0];
  uploadLargeFile(file);
});

后端实现

  1. 接收文件块
    • 接收前端发送的文件块,包括文件名、块索引、总块数等信息。
  2. 存储文件块
    • 将接收到的文件块存储到临时目录。
  3. 合并文件块
    • 当所有文件块上传完成后,按照索引顺序合并文件块。
  4. 清理临时文件
    • 合并完成后,删除临时存储的文件块。 以下是一个简单的后端实现示例(使用Node.js):
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.post('/upload', (req, res) => {
  const tempDir = path.join(__dirname, 'temp');
  if (!fs.existsSync(tempDir)) {
    fs.mkdirSync(tempDir);
  }
  const fileRouter = req.files.file;
  const filename = req.body.filename;
  const chunkIndex = parseInt(req.body.chunkIndex);
  const totalChunks = parseInt(req.body.totalChunks);
  const chunkPath = path.join(tempDir, `${filename}_chunk${chunkIndex}`);
  fs.writeFileSync(chunkPath, fileRouter.data);
  // 检查是否所有块都已上传
  const allChunksUploaded = Array.from({ length: totalChunks }, (_, i) =>
    fs.existsSync(path.join(tempDir, `${filename}_chunk${i}`))
  ).every(exists => exists);
  if (allChunksUploaded) {
    // 合并文件块
    const finalFilePath = path.join(__dirname, 'uploads', filename);
    const writeStream = fs.createWriteStream(finalFilePath);
    for (let i = 0; i < totalChunks; i++) {
      const chunkPath = path.join(tempDir, `${filename}_chunk${i}`);
      const readStream = fs.createReadStream(chunkPath);
      readStream.pipe(writeStream, { end: false });
      readStream.on('end', () => {
        fs.unlinkSync(chunkPath); // 删除临时文件块
      });
    }
    writeStream.on('finish', () => {
      res.json({ message: 'File uploaded and merged successfully' });
    });
    writeStream.end();
  } else {
    res.json({ message: `Chunk ${chunkIndex} uploaded successfully` });
  }
});
app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

22. 编辑距离

编辑距离,通常指Levenshtein距离,是衡量两个字符串之间差异的一种标准,表示将一个字符串转换为另一个字符串所需的最少单字符编辑操作的数量。这些编辑操作可以是插入、删除或替换一个字符。 以下是用TypeScript实现编辑距离计算的示例代码:

function levenshteinDistance(a: string, b: string): number {
  const dp: number[][] = [];
  // 初始化dp数组
  for (let i = 0; i <= a.length; i++) {
    dp[i] = [i];
  }
  for (let j = 0; j <= b.length; j++) {
    dp[0][j] = j;
  }
  // 计算编辑距离
  for (let i = 1; i <= a.length; i++) {
    for (let j = 1; j <= b.length; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      dp[i][j] = Math.min(
        dp[i - 1][j] + 1, // 删除
        dp[i][j - 1] + 1, // 插入
        dp[i - 1][j - 1] + cost // 替换
      );
    }
  }
  return dp[a.length][b.length];
}
// 使用示例
const str1 = "kitten";
const str2 = "sitting";
const distance = levenshteinDistance(str1, str2);
console.log(`The Levenshtein distance between "${str1}" and "${str2}" is ${distance}`);

在这个实现中,我们使用了一个二维数组dp来存储中间结果,其中dp[i][j]表示将字符串a的前i个字符转换为字符串b的前j个字符所需的最小编辑距离。我们通过动态规划的方式填充这个数组,最终dp[a.length][b.length]就是两个字符串的编辑距离。 这个实现的时间复杂度是O(nm),其中n和m分别是两个字符串的长度。空间复杂度也是O(nm),可以通过只存储当前行和上一行来优化空间复杂度到O(min(n, m))。

23. 跨域时怎么处理 cookie?

跨域时处理cookie需要特别注意,因为出于安全考虑,浏览器默认禁止跨域请求携带cookie。但是,如果你确实需要跨域请求时携带cookie,可以采取以下几种方法:

1. CORS(跨源资源共享)

通过CORS(Cross-Origin Resource Sharing)设置,服务器可以允许特定的跨域请求携带cookie。需要在服务器端设置相应的响应头:

Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Credentials: true

同时,在客户端发起请求时,也需要设置withCredentialstrue

fetch('http://another-domain.com/api', {
  credentials: 'include' // 设置为 'include' 以携带cookie
});

或者使用XMLHttpRequest:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://another-domain.com/api', true);
xhr.withCredentials = true; // 设置为true以携带cookie
xhr.send();

2. JSONP(仅适用于GET请求)

JSONP(JSON with Padding)是一种利用<script>标签不受同源策略限制的特性来绕过跨域问题的技术。但是,JSONP不支持自定义HTTP头部,因此不能明确地携带cookie。不过,如果客户端和服务器之间有某种约定,可以通过查询参数传递类似cookie的信息。

3. 代理服务器

使用代理服务器可以将请求发送到同一域的代理,然后由代理转发请求到目标服务器。这样,因为请求是发送到同一域的,所以可以携带cookie。代理服务器再将从目标服务器接收的响应返回给客户端。

4. 后端服务器直接设置

如果后端服务器支持,可以直接在服务器之间进行通信,而不是通过浏览器。这样,服务器之间的请求可以自由地携带cookie,不受同源策略的限制。

注意事项:

  • 当使用CORS时,Access-Control-Allow-Origin不能设置为*,因为Access-Control-Allow-Credentialstrue时,不允许使用通配符。
  • 确保cookie的域(domain)和路径(path)与请求的URL匹配,否则cookie可能不会被发送。
  • 考虑安全性,确保只有可信的域可以接收cookie。 在选择方法时,需要根据具体的场景和需求来决定。CORS是目前最常用的解决方案,但需要服务器端的支持。如果服务器端无法修改,可以考虑使用代理服务器或JSONP(仅适用于GET请求)。

24. 说说 webpack-dev-server 的原理

webpack-dev-server 是一个基于 webpack 的开发服务器,它提供了一些用于开发时的便利功能,如实时重新加载(live reloading)、热模块替换(Hot Module Replacement,HMR)、代理服务等。其原理主要基于以下几部分:

1. Webpack编译器

webpack-dev-server 使用 webpack 编译器来编译文件。当文件发生变化时,webpack 会重新编译代码,并且生成新的编译结果。

2. WebSocket连接

webpack-dev-server 通过 WebSocket 与浏览器建立实时通信。当编译器检测到文件变化并完成重新编译后,会通过 WebSocket 向浏览器发送消息,通知浏览器进行更新。

3. 实时重新加载(Live Reloading)

当浏览器收到来自 webpack-dev-server 的更新通知后,会自动刷新页面,从而实现实时预览最新的编译结果。

4. 热模块替换(HMR)

HMR 是 webpack-dev-server 的一个高级功能,它允许在运行时替换、添加或删除模块,而无需重新加载整个页面。这是通过 webpack 的 HMR 插件和模块系统的配合实现的。当某个模块发生变化时,只有该模块会被重新编译并替换,从而提高开发效率。

5. 内存中文件系统

webpack-dev-server 将编译后的文件存储在内存中,而不是写入磁盘。这样可以让文件读写更快,提高开发效率。

6. 文件监听

webpack-dev-server 会监听文件系统的变化。当检测到文件修改时,会触发重新编译。这是通过 webpack 的文件监听功能实现的。

7. 代理服务

webpack-dev-server 可以配置代理服务器,解决开发过程中的跨域问题。代理服务器会拦截特定的请求,并将它们转发到后端服务器。

工作流程:

  1. 启动 webpack-dev-server
  2. webpack 开始编译文件,并将编译结果存储在内存中。
  3. webpack-dev-server 通过 WebSocket 与浏览器建立连接。
  4. 当文件发生变化时,webpack 重新编译文件。
  5. 编译完成后,webpack-dev-server 通过 WebSocket 向浏览器发送更新通知。
  6. 浏览器根据通知进行实时重新加载或热模块替换。

总结:

webpack-dev-server 的核心原理是利用 webpack 的编译能力和文件监听功能,结合 WebSocket 实现实时通信,从而在开发过程中提供快速反馈。通过内存中文件系统和代理服务,进一步优化开发体验。

25. 数组中的第 k 大的数字

在 TypeScript 中实现查找数组中第 k 大的数字可以通过多种方法来完成,例如排序后访问、使用堆或者快速选择算法。这里我将提供一个简单且常用的方法,即通过排序后访问第 k 大的元素。 以下是一个 TypeScript 函数的实现:

function findKthLargest(nums: number[], k: number): number {
    // 对数组进行排序(默认为升序)
    nums.sort((a, b) => a - b);
    // 返回第 k 大的元素,即倒数第 k 个元素
    return nums[nums.length - k];
}
// 示例使用
const nums = [3, 2, 1, 5, 6, 4];
const k = 2;
console.log(findKthLargest(nums, k)); // 输出:5

这个方法首先对数组进行排序,然后直接访问倒数第 k 个元素来获取第 k 大的数字。请注意,这个方法会修改原数组,如果需要保持原数组不变,可以先复制一个数组副本进行排序。 如果你需要更高效的算法,可以考虑使用快速选择算法,它可以在平均 O(n) 的时间复杂度内找到第 k 大的元素,但是实现起来相对复杂一些。

26. 怎么实现洗牌算法?

洗牌算法通常用于打乱数组中的元素顺序,确保每个元素出现在每个位置的概率相等。一个常见且有效的洗牌算法是Fisher-Yates洗牌算法(也称为Knuth洗牌算法)。以下是该算法在TypeScript中的实现:

function shuffle<T>(array: T[]): T[] {
  let currentIndex = array.length, temporaryValue, randomIndex;
  // 当还剩有元素待洗牌时继续
  while (currentIndex !== 0) {
    // 取一个待洗牌的元素
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;
    // 与当前元素交换
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }
  return array;
}
// 示例使用
const myArray = [1, 2, 3, 4, 5];
const shuffledArray = shuffle(myArray);
console.log(shuffledArray);

这个函数接受一个数组作为参数,并返回一个洗牌后的新数组。算法的基本思想是从数组的最后一个元素开始,随机选择一个元素与它交换,然后减少考虑的数组长度,继续这个过程,直到遍历完整个数组。 这种方法确保了每个元素都有相等的概率被交换到任何位置,从而实现了公平的洗牌。 请注意,由于JavaScript的Math.random()函数生成的随机数是伪随机数,因此在加密或安全性要求高的场合,可能需要使用更安全的随机数生成方法。

27. HTTP1.0,HTTP1.1,HTTP2.0之间有什么区别?

HTTP/1.0、HTTP/1.1和HTTP/2.0是HTTP协议的三个主要版本,它们之间存在一些显著的区别:

HTTP/1.0

  • 无状态:每个请求都是独立的,服务器不保存关于请求状态的信息。
  • 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。
  • 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。

HTTP/1.1

  • 持久连接:HTTP/1.1引入了持久连接(persistent connection),允许在一个TCP连接上发送和接收多个HTTP请求/响应,而不是每次请求/响应都要打开一个新的连接。
  • 管道化:在持久连接的基础上,HTTP/1.1还支持管道化(pipelining),允许客户端在等待服务器响应之前发送多个请求。
  • 缓存控制:HTTP/1.1提供了更多的缓存控制机制,如If-Modified-Since、If-None-Match等。
  • Host头:HTTP/1.1增加了Host请求头,允许在一个服务器上配置多个网站(虚拟主机)。
  • 范围请求:允许客户端请求资源的某个部分,而不是整个资源,这有助于断点续传。
  • 改进的带宽管理:通过增加更多的头部字段,如Content-Encoding(内容编码)、Accept-Encoding(接受编码)等,来优化带宽使用。

HTTP/2.0

  • 二进制分帧层:HTTP/2.0将数据分割为更小的帧,并在帧的基础上定义了流(stream),允许在单个连接上同时发送多个请求或响应,而不会互相干扰。
  • 头部压缩:HTTP/2.0使用了HPACK算法来压缩请求和响应的头部,减少了数据传输量。
  • 多路复用:允许在单个TCP连接上同时发送多个请求或响应,解决了HTTP/1.1中的队头阻塞问题。
  • 服务器推送:服务器可以在客户端请求之前主动推送资源,减少了额外的请求开销。
  • 优先级和流量控制:允许客户端设置请求的优先级,服务器可以根据优先级进行响应。同时,流量控制机制可以防止快速发送方淹没接收方。
  • 安全性:虽然HTTP/2.0本身并不要求使用加密,但大多数浏览器只支持加密的HTTP/2.0连接(即使用HTTPS)。

总结

  • 性能:HTTP/2.0在性能上相比HTTP/1.1有显著提升,特别是在高延迟网络环境下。
  • 复杂性:HTTP/2.0的协议复杂性增加,但为开发者提供了更高效的数据传输方式。
  • 兼容性:HTTP/2.0在设计时考虑了与HTTP/1.1的兼容性,可以通过升级机制从HTTP/1.1过渡到HTTP/2.0。 这些版本的演进主要是为了提高网络通信的效率、安全性和灵活性,以适应不断增长的网络应用需求。

28. DNS 预解析是什么?怎么实现?

DNS预解析DNS Prefetching是一种优化技术,用于提高网站性能。它允许浏览器在用户访问特定链接之前提前解析域名,这样当用户真正点击链接时,DNS解析过程已经完成,从而减少了等待时间。

工作原理

  1. 提前解析:当浏览器遇到一个链接时,它会提前解析该链接的域名,即使用户还没有点击该链接。
  2. 缓存结果:解析结果会被缓存起来,以便在用户真正访问该链接时快速使用。

实现方式

1. 使用<link>标签

在HTML的<head>部分,可以使用<link>标签来指定需要预解析的域名:

<link rel="dns-prefetch" href="//example.com">

这行代码告诉浏览器提前解析example.com的DNS。

2. 使用HTTP头

也可以通过HTTP头来实现DNS预解析。在服务器的响应头中添加:

X-DNS-Prefetch-Control: on

这会启用DNS预解析。如果想要禁用,可以设置为off

3. 使用meta标签

另一种方法是在HTML的<head>部分使用meta标签:

<meta http-equiv="x-dns-prefetch-control" content="on">

这同样会启用DNS预解析。

注意事项

  • 适量使用:虽然DNS预解析可以提高性能,但过多使用可能会导致不必要的DNS查询,从而适得其反。
  • 安全性考虑:预解析的域名应该是可信的,以避免潜在的安全风险。
  • 浏览器支持:大多数现代浏览器都支持DNS预解析,但仍有少数不支持。因此,建议作为性能优化的补充手段,而不是唯一手段。 通过合理使用DNS预解析,可以有效地提高网站的性能和用户体验。

29. 说说你对JS的模块化方案的了解

JavaScript的模块化方案经历了从无到有,从简单到复杂的发展过程。以下是一些主要的JS模块化方案:

1. 全局函数模式

最初,JavaScript没有模块化概念,开发者通常通过全局函数来组织代码。这种方法简单,但容易造成全局变量污染。

2. 命名空间模式

为了减少全局变量的数量,开发者开始使用命名空间来组织代码。例如:

var myApp = {
  utils: {
    // 工具函数
  },
  config: {
    // 配置信息
  }
};

3. 立即执行函数表达式(IIFE)

IIFE可以创建一个私有作用域,避免全局变量污染。例如:

(function() {
  // 私有变量和函数
})();

4. CommonJS

CommonJS是Node.js采用的模块化方案,通过requiremodule.exports实现模块的导入和导出:

// 导出
module.exports = function() {};
// 导入
const myModule = require('./myModule');

5. AMD(Asynchronous Module Definition)

AMD是一种异步模块定义方案,适用于浏览器环境。它允许异步加载模块,并通过definerequire实现模块的导入和导出:

// 定义模块
define(['dependency'], function(dependency) {
  return function() {};
});
// 引用模块
require(['myModule'], function(myModule) {
  // 使用myModule
});

6. UMD(Universal Module Definition)

UMD是一种兼容CommonJS和AMD的模块化方案,适用于多种环境:

(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['dependency'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS
    module.exports = factory(require('dependency'));
  } else {
    // 全局变量
    root.returnExports = factory(root.dependency);
  }
}(this, function(dependency) {
  // 模块代码
}));

7. ES6模块

ES6模块是ECMAScript 2015(ES6)引入的官方模块化方案,使用importexport关键字:

// 导出
export function myFunction() {}
// 导入
import { myFunction } from './myModule';

ES6模块是未来发展的趋势,得到了现代浏览器的广泛支持。

8. SystemJS

SystemJS是一个通用的模块加载器,支持AMD、CommonJS、ES6等多种模块化方案。它可以在浏览器中动态加载模块。

选择合适的模块化方案

选择模块化方案时,需要考虑以下因素:

  • 环境:Node.js环境通常使用CommonJS,浏览器环境可以使用AMD或ES6模块。
  • 兼容性:需要考虑旧版浏览器的兼容性。
  • 工具支持:考虑构建工具(如Webpack、Browserify)对模块化方案的支持。 随着ES6模块的普及,越来越多的项目开始采用ES6模块作为主要的模块化方案。

30. 你常用的 React Hooks 有哪些?

作为一个人工智能,我没有个人使用习惯,但我可以告诉你一些在React开发中常用的 Hooks,以及它们的一般用途:

  1. useState:
    • 用途:允许你在函数组件中添加React状态。
    • 示例:const [count, setCount] = useState(0);
  2. useEffect:
    • 用途:处理副作用,如API调用、订阅或手动更改DOM。
    • 示例:useEffect(() => { document.title = You clicked ${count} times; }, [count]);
  3. useContext:
    • 用途:访问React的context对象,无需通过组件树逐层传递props。
    • 示例:const value = useContext(MyContext);
  4. useReducer:
    • 用途:管理复杂的状态逻辑,类似于Redux。
    • 示例:const [state, dispatch] = useReducer(reducer, initialArg, init);
  5. useCallback:
    • 用途:返回一个记忆化的回调函数,避免在渲染过程中重新创建函数。
    • 示例:const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
  6. useMemo:
    • 用途:返回一个记忆化的值,避免在每次渲染时重新计算。
    • 示例:const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  7. useRef:
    • 用途:返回一个可变的ref对象,其.current属性被初始化为传递的参数(初始值)。最常用于访问DOM元素。
    • 示例:const myRef = useRef(initialValue);
  8. useImperativeHandle:
    • 用途:在使用ref时自定义暴露给父组件的实例值。
    • 示例:useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } }), [inputRef]);
  9. useLayoutEffect:
    • 用途:与useEffect类似,但它会在所有的DOM变更之后同步调用,适合处理DOM操作。
    • 示例:useLayoutEffect(() => { /* perform DOM updates */ }, [dependencies]);
  10. useDebugValue:
    • 用途:在开发者工具中显示自定义标签,用于调试。
    • 示例:useDebugValue(isOnline ? 'Online' : 'Offline'); 这些Hooks是React官方提供的一部分,它们极大地简化了函数组件中的状态管理和副作用处理。开发者根据具体的需求和场景选择合适的Hooks来使用。随着React的发展,未来可能会出现更多的Hooks来满足更复杂的需求。