2025面试题大全(2)

182 阅读1小时+

1. 怎么进行 Node 服务的内存优化?

Node.js 服务的内存优化是一个重要的环节,尤其是在处理高并发和大数据量的场景下。以下是一些常见的内存优化策略:

1. 监控和分析内存使用

  • 使用内置模块:使用 Node.js 的 process.memoryUsage() 方法来监控内存使用情况。
  • 工具:使用如 Chrome DevTools、Node.js Profiler、Memwatch-next 等工具进行内存分析。

2. 优化数据结构

  • 选择合适的数据结构:例如,使用数组而不是对象来存储大量数据,因为数组在 V8 引擎中通常有更优的内存布局。
  • 避免内存泄漏:确保对象不再需要时能够被垃圾回收,避免无限制的增长。

3. 使用流(Streams)

  • 处理大文件:使用流来处理大文件或数据流,避免一次性将所有数据加载到内存中。

4. 限制内存使用

  • 设置内存限制:使用 --max-old-space-size--max-new-space-size 标志来限制 Node.js 进程的内存使用。

5. 优化缓存策略

  • 合理使用缓存:缓存可以提高性能,但过大的缓存会导致内存消耗过多。使用适当的缓存失效策略,如 LRU(最近最少使用)缓存。

6. 避免全局变量

  • 减少全局变量:全局变量会在整个应用生命周期内保留在内存中,尽量减少使用全局变量。

7. 优化代码

  • 减少闭包:闭包可以捕获外部变量,导致内存占用增加,尽量减少不必要的闭包。
  • 使用原生方法:尽可能使用原生方法,因为它们通常比自定义实现更高效。

8. 垃圾回收优化

  • 手动触发垃圾回收:在适当的时候手动触发垃圾回收,例如在处理完大量数据后。
  • 分代回收:了解 V8 的分代回收机制,优化对象的生命周期,让垃圾回收更高效。

9. 使用集群(Clustering)

  • 多进程:利用 Node.js 的集群模块来创建多个进程,每个进程有自己的内存空间,可以分散内存压力。

10. 外部存储

  • 数据库和缓存服务:将数据存储在数据库或外部缓存服务中,如 Redis,避免在 Node.js 进程中存储大量数据。

11. 代码分割和懒加载

  • 按需加载:将代码分割成多个模块,按需加载,减少初始加载时的内存占用。

12. 更新 Node.js 版本

  • 使用最新版本:新版本的 Node.js 通常包含内存优化和性能提升的改进。

13. 使用高效库

  • 选择高效的库:使用内存效率高的库,避免使用已知有内存泄漏问题的库。

14. 定期重启

  • 重启服务:对于长时间运行的服务,定期重启可以帮助释放内存。

总结

内存优化是一个持续的过程,需要结合具体的应用场景和性能监控数据进行调整。通过上述策略,可以有效地管理和优化 Node.js 服务的内存使用,提高应用的性能和稳定性。

2. redux-thunk 和 redux 是什么关系?

redux-thunk 是一个在 Redux 中使用的中间件,用于处理异步操作。它与 Redux 的关系可以从以下几个方面来理解:

1. Redux 的核心概念

  • Redux 是一个 JavaScript 应用的状态管理库,它通过一个单一的 Store 来管理应用的状态,并且状态是只读的,只能通过 dispatching actions 来改变。

2. 同步与异步

  • 同步操作:在 Redux 中,默认情况下,action 创建函数是同步的。当你调用 store.dispatch(action) 时,action 会立即被 reducer 处理,并且状态会立即更新。
  • 异步操作:然而,在实际应用中,我们经常需要处理异步操作,比如从服务器获取数据。Redux 本身并不支持异步操作,因此需要额外的库来处理。

3. Middleware(中间件)

  • 中间件:Redux 的中间件是在 action 被 dispatch 到 reducer 之间执行的一些代码,可以用来处理日志、错误、异步操作等。
  • redux-thunk 正是一个用于处理异步操作的中间件。

4. redux-thunk 的工作原理

  • Thunk:一个 thunk 是一个返回函数的函数。在 redux-thunk 中,你可以 dispatch 一个函数而不是一个 action 对象。这个函数接收 dispatchgetState 作为参数,可以执行异步操作,并在操作完成后 dispatch 一个普通的 action。
  • 异步流程:使用 redux-thunk,你可以在 thunk 函数中执行异步操作(如 API 请求),然后在操作完成后 dispatch 一个 action 来更新状态。

5. 安装和使用

  • 安装redux-thunk 需要作为依赖安装到项目中。
  • 使用:在创建 Redux store 时,通过 applyMiddleware 方法将 redux-thunk 应用到 store 中。

6. 其他异步中间件

  • redux-thunk 是处理异步操作的一种选择,还有其他中间件如 redux-sagaredux-observable 等,它们提供了不同的处理异步操作的方式。

总结

redux-thunk 是 Redux 的一个中间件,用于扩展 Redux 的功能,使其能够处理异步操作。它允许你 dispatch 函数而不是 action 对象,这些函数可以执行异步代码并最终 dispatch 标准的 action 来更新应用的状态。redux-thunk 使得 Redux 在处理复杂异步逻辑时更加灵活和强大。

3. 如何给自己团队的大型前端项目设计单元测试?

设计大型前端项目的单元测试是一个系统性工程,需要考虑到测试策略、工具选择、测试覆盖率、持续集成等多个方面。以下是一些步骤和最佳实践,可以帮助你为团队的大型前端项目设计有效的单元测试:

1. 确定测试策略

  • 目标明确:明确测试的目标,例如提高代码质量、减少bug、确保代码可维护性等。
  • 测试范围:确定哪些部分需要测试,哪些不需要。通常,复杂的逻辑、易变的部分和核心功能是测试的重点。
  • 测试类型:除了单元测试,还可能需要集成测试、端到端测试等。明确每种测试的职责和范围。

2. 选择合适的测试工具

  • 测试框架:选择适合项目的测试框架,如 Jest、Mocha、Jasmine 等。
  • 断言库:选择断言库,如 Chai、Should.js 等,用于编写测试断言。
  • 模拟库:选择模拟库,如 Sinon、Mockito 等,用于模拟复杂的依赖和外部交互。
  • 测试辅助工具:如 Enzyme、React Testing Library 等,用于简化特定技术栈的测试。

3. 编写测试代码

  • 测试独立性:确保每个测试用例都是独立的,不依赖于其他测试。
  • 测试可读性:编写清晰、可读的测试代码,便于团队成员理解和维护。
  • 测试覆盖率:使用工具(如 Istanbul)监控测试覆盖率,确保关键代码都被测试覆盖。
  • 模拟外部依赖:使用模拟库来模拟外部依赖,如API调用、数据库操作等,以隔离测试。

4. 组织测试代码

  • 测试文件结构:保持测试文件与源代码文件的对应关系,例如同一目录下或特定的测试目录。
  • 测试命名规范:制定统一的测试命名规范,如 describeit 的命名,以便于识别测试的内容。

5. 集成持续集成(CI)

  • 自动化测试:在 CI 流程中集成测试,确保每次提交或合并请求都运行测试。
  • 测试报告:生成测试报告,包括覆盖率报告,以便于团队了解测试结果。

6. 维护和优化

  • 定期审查:定期审查测试代码,确保测试的有效性和准确性。
  • 性能优化:如果测试运行缓慢,分析并优化测试性能。
  • 更新测试:随着项目的发展,更新测试以覆盖新的功能和代码变更。

7. 培训和文档

  • 团队培训:确保团队成员了解如何编写和维护测试。
  • 文档记录:编写测试相关的文档,包括测试策略、工具使用指南等。

8. 监控和反馈

  • 监控测试结果:持续监控测试结果,及时发现并解决问题。
  • 反馈机制:建立反馈机制,让团队成员可以轻松报告测试相关的问题。

示例:使用 Jest 编写一个简单的单元测试

// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

在这个示例中,我们有一个简单的 sum 函数,并且为其编写了一个单元测试,使用 Jest 作为测试框架。 通过遵循上述步骤和最佳实践,你可以为团队的大型前端项目设计出一个有效、可维护的单元测试体系。

4. 如何做一个前端项目工程的自动化部署,有哪些规范和流程设计?

前端项目工程的自动化部署是提高开发效率、确保项目稳定性和一致性的关键环节。以下是一些规范和流程设计,可以帮助你实现前端项目的自动化部署:

1. 版本控制

  • 使用Git:确保所有代码都通过Git进行版本控制。
  • 分支策略:采用合理的分支策略,如Git Flow或GitHub Flow,明确开发、测试、预发布和主分支的用途。

2. 持续集成/持续部署(CI/CD)

  • 选择CI/CD工具:如Jenkins、Travis CI、GitLab CI、GitHub Actions等。
  • 配置CI/CD管道:定义管道中的各个阶段,如编译、测试、打包、部署等。

3. 自动化测试

  • 单元测试:在CI过程中运行单元测试,确保代码质量。
  • 集成测试:运行集成测试,检查组件间的交互。
  • 端到端测试:在必要时运行端到端测试,模拟真实用户场景。

4. 代码质量检查

  • 代码风格检查:使用工具如ESLint、Prettier等确保代码风格一致。
  • 静态代码分析:使用工具如SonarQube进行静态代码分析,发现潜在问题。

5. 构建和打包

  • 构建工具:使用Webpack、Rollup等构建工具打包前端资源。
  • 环境变量:管理不同环境下的配置,如开发、测试、生产环境。

6. 部署策略

  • 蓝绿部署:部署新版本的同时保留旧版本,快速回滚。
  • 滚动部署:逐步替换旧版本,减少服务中断。
  • 金丝雀部署:先部署到一小部分服务器,观察效果后再全面部署。

7. 自动化部署流程

  • 触发部署:可以通过代码推送、手动触发或定时任务触发部署。
  • 部署脚本:编写脚本自动化部署过程,如 scp、rsync 或使用容器化技术如Docker。
  • 环境隔离:确保开发、测试、生产环境相互隔离。

8. 监控和日志

  • 应用监控:使用工具如Prometheus、Grafana监控应用状态。
  • 日志收集:使用ELK堆栈(Elasticsearch、Logstash、Kibana)或类似工具收集和分析日志。

9. 回滚机制

  • 快速回滚:确保能够快速回滚到之前的稳定版本。
  • 版本管理:保留历史版本的构建产物,便于回滚。

10. 安全性考虑

  • 权限控制:限制对生产环境的访问权限。
  • 安全扫描:在部署前进行安全扫描,发现潜在的安全问题。

11. 文档和培训

  • 流程文档:编写详细的部署流程文档。
  • 团队培训:确保团队成员了解自动化部署的流程和工具使用。

12. 持续优化

  • 反馈循环:建立反馈循环,根据部署过程中遇到的问题不断优化流程。
  • 性能优化:优化构建和部署过程中的性能,减少部署时间。

示例:简单的GitHub Actions自动化部署流程

# .github/workflows/deploy.yml
name: Deploy to Production
on:
  push:
    branches:
      - main
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Install Dependencies
      run: npm install
    - name: Build
      run: npm run build
    - name: Deploy
      run: scp -r ./build user@server:/path/to/production
      env:
        SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}

在这个示例中,我们使用GitHub Actions定义了一个简单的自动化部署流程,当代码推送到main分支时,会自动安装依赖、构建项目并将构建产物部署到生产服务器。 通过遵循上述规范和流程设计,你可以实现一个高效、可靠的前端项目自动化部署体系。

5. 纯 TS 项目工程中, 如何使用 alias path?

在纯TypeScript项目工程中使用alias路径(路径别名),可以通过以下步骤实现:

1. 配置TypeScript编译器

首先,你需要在tsconfig.json文件中配置路径别名。这可以通过compilerOptions中的paths属性来实现。

{
  "compilerOptions": {
    "baseUrl": ".", // 设置基础目录
    "paths": {
      "@components/*": ["src/components/*"], // 设置路径别名
      "@utils/*": ["src/utils/*"]
    }
  }
}

在这个例子中,@components@utils被设置为指向src/componentssrc/utils目录的别名。

2. 配置模块解析器

为了在开发过程中(如在使用IDE时)能够正确解析这些别名,你可能需要配置模块解析器。如果你使用的是Webpack,可以在webpack.config.js中配置resolve.alias

const path = require('path');
module.exports = {
  // ...
  resolve: {
    alias: {
      '@components': path.resolve(__dirname, 'src/components/'),
      '@utils': path.resolve(__dirname, 'src/utils/')
    }
  }
  // ...
};

3. 配置IDE

大多数现代IDE(如Visual Studio Code)都能够根据tsconfig.json中的配置自动解析路径别名。但是,如果你遇到了问题,可能需要进一步配置IDE。 例如,在Visual Studio Code中,你可以通过安装typescript-plugin-path-alias插件来增强路径别名的支持。

4. 使用别名

在项目中,你可以直接使用配置的别名来导入模块。

import { MyComponent } from '@components/MyComponent';
import { utilFunction } from '@utils/utilFunction';

5. 确保构建工具支持

确保你使用的构建工具(如Webpack、Rollup等)支持路径别名。如果使用Webpack,上面已经展示了如何配置。其他构建工具也有类似的配置选项。

6. 注意事项

  • 确保路径别名的配置与实际文件结构相匹配。
  • 在团队协作时,确保所有成员都使用相同的tsconfig.json配置。
  • 如果项目中有多个tsconfig.json文件(如用于不同环境的配置),确保路径别名在所有配置中保持一致。 通过以上步骤,你可以在纯TypeScript项目工程中有效地使用路径别名,提高开发效率和代码可维护性。

6. 实现:setObjectValue(obj: object, keys: string[], value: any) 方法, 支持安全设置对象的值

为了实现一个setObjectValue方法,该方法能够安全地设置对象的值,我们需要考虑以下几点:

  1. 参数验证:确保传入的obj是一个对象,keys是一个字符串数组,value是任意值。
  2. 遍历键数组:通过遍历keys数组来深入对象的层级。
  3. 安全性检查:在设置值之前,检查当前层级是否是一个对象,如果不是,则初始化为一个空对象。
  4. 设置值:在到达最后一层时,设置对应的值。 下面是一个实现这个方法的TypeScript代码示例:
function setObjectValue(obj: object, keys: string[], value: any): void {
  if (typeof obj !== 'object' || obj === null) {
    throw new Error('First argument must be an object.');
  }
  if (!Array.isArray(keys) || keys.some(key => typeof key !== 'string')) {
    throw new Error('Second argument must be an array of strings.');
  }
  let current = obj;
  for (let i = 0; i < keys.length - 1; i++) {
    const key = keys[i];
    if (!(key in current)) {
      current[key] = {}; // 初始化为空对象
    }
    if (typeof current[key] !== 'object' || current[key] === null) {
      throw new Error(`Expected an object at key '${key}' but found ${typeof current[key]}.`);
    }
    current = current[key];
  }
  const lastKey = keys[keys.length - 1];
  current[lastKey] = value;
}
// 示例使用
const myObject = {};
setObjectValue(myObject, ['a', 'b', 'c'], 123);
console.log(myObject); // { a: { b: { c: 123 } } }

说明:

  • 参数验证:方法开始时,我们检查obj是否是一个对象,keys是否是一个字符串数组。
  • 遍历键数组:使用一个for循环遍历keys数组,直到倒数第二个键,这样我们就可以在最后一层设置值。
  • 安全性检查:在遍历过程中,如果发现当前键不存在,我们将其初始化为一个空对象。同时,我们检查当前键对应的值是否是一个对象,如果不是,则抛出错误。
  • 设置值:在遍历完成后,我们使用最后一个键来设置传入的value。 这个方法可以安全地设置对象的嵌套值,同时提供了必要的错误处理来确保过程的健壮性。

7. 一个 tcp 连接能发几个 http 请求?

一个TCP连接可以发送多个HTTP请求,但具体数量取决于使用的HTTP版本和服务器配置。

  1. HTTP/1.0:在HTTP/1.0中,默认情况下每个TCP连接只能发送一个HTTP请求。每次请求完成后,连接会被关闭。但是,可以通过使用Connection: keep-alive头部来保持连接开放,从而在同一个TCP连接上发送多个请求。
  2. HTTP/1.1:HTTP/1.1默认支持持久连接(Connection: keep-alive),这意味着可以在同一个TCP连接上发送多个HTTP请求。然而,这些请求是顺序执行的,即必须等待上一个请求的响应完成才能发送下一个请求。这种限制导致了所谓的“队头阻塞”(Head-of-Line Blocking)问题。
  3. HTTP/2:HTTP/2解决了HTTP/1.1的队头阻塞问题,它支持多路复用(Multiplexing)。在HTTP/2中,多个请求和响应可以在同一个TCP连接上同时传输,而不需要等待之前的请求完成。这样可以大大提高连接的利用率 и效率。理论上,HTTP/2可以在一个TCP连接上发送无限多个请求,但实际上,这个数量可能会受到服务器配置、资源限制或其他因素的限制。
  4. HTTP/3:HTTP/3使用QUIC协议代替TCP,它也支持多路复用,并且在设计上旨在提供比HTTP/2更好的性能。HTTP/3的请求数量同样理论上是无限的,但实际数量会受到类似HTTP/2的那些限制。 总的来说,现代的HTTP协议(如HTTP/1.1、HTTP/2和HTTP/3)都支持在单个TCP连接上发送多个请求,但实际可发送的请求数量会受到协议特性、服务器配置和资源限制的影响。在HTTP/2和HTTP/3中,由于多路复用的支持,这个数量通常远高于HTTP/1.0和HTTP/1.1。

8. promise.race、promise.all、promise.allSettled 有哪些区别?

Promise.racePromise.allPromise.allSettled 都是 JavaScript 中用于处理多个 Promise 实例的方法,但它们在处理 Promise 的方式和返回结果上有所不同。以下是它们之间的主要区别:

  1. Promise.race(iterable)
    • 行为Promise.race 接受一个可迭代对象(如数组)作为输入,只要其中的一个 Promise 实例率先改变状态(无论是 fulfilled 还是 rejected),就会调用返回的 Promise 的回调函数。
    • 结果:返回一个新的 Promise 实例,这个实例的状态和第一个改变状态的 Promise 实例相同。
    • 用途:通常用于超时处理或者只需要第一个响应的场景。
  2. Promise.all(iterable)
    • 行为Promise.all 也接受一个可迭代对象作为输入,只有当所有输入的 Promise 实例都成功(fulfilled)时,返回的 Promise 实例才会成功。如果任何一个 Promise 实例失败(rejected),返回的 Promise 实例也会立即失败。
    • 结果:返回一个新的 Promise 实例,如果所有输入的 Promise 都成功,则结果是一个数组,包含所有 Promise 成功的结果。如果任何一个 Promise 失败,则结果就是那个失败的原因。
    • 用途:适用于需要所有任务都完成才能继续执行下一步的场景。
  3. Promise.allSettled(iterable)(ES2020 引入):
    • 行为Promise.allSettled 接受一个可迭代对象作为输入,无论每个 Promise 实例是成功还是失败,都会等待所有的 Promise 实例都 settle(即每个 Promise 都已经 fulfilled 或 rejected)后才返回。
    • 结果:返回一个新的 Promise 实例,结果是一个数组,每个元素都是一个对象,表示对应的 Promise 的结果。每个对象都有 status 属性(值为 "fulfilled""rejected")和 valuereason 属性(分别表示成功的结果或失败的原因)。
    • 用途:适用于需要知道每个 Promise 的最终结果,但不希望任何一个 Promise 的失败导致整体失败的场景。 总结
  • Promise.race 是“赛跑”机制,只要有一个 Promise 完成就结束。
  • Promise.all 是“全或无”机制,所有 Promise 都成功才成功,一旦有一个失败就失败。
  • Promise.allSettled 是“全部完成”机制,不管成功还是失败,都要等所有 Promise 都有结果了才结束。 根据不同的使用场景和需求,可以选择合适的方法来处理 Promise。

9. 手写实现 Promise.allSettled

要手写实现 Promise.allSettled,我们需要创建一个函数,该函数接受一个可迭代对象(通常是数组)的 Promise,并返回一个新的 Promise。这个新的 Promise 将在所有输入的 Promise 都已经 settled(即每个 Promise 都已经 fulfilled 或 rejected)后解决,其结果是一个数组,每个元素都是一个对象,表示对应的 Promise 的结果。 以下是 Promise.allSettled 的一个简单实现:

function allSettled(promises) {
  return new Promise((resolve) => {
    const results = [];
    let completedPromises = 0;
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then(
        (value) => {
          results[i] = { status: 'fulfilled', value };
          completedPromises++;
          if (completedPromises === promises.length) {
            resolve(results);
          }
        },
        (reason) => {
          results[i] = { status: 'rejected', reason };
          completedPromises++;
          if (completedPromises === promises.length) {
            resolve(results);
          }
        }
      );
    }
    if (promises.length === 0) {
      resolve(results);
    }
  });
}

这个实现中,我们做了以下几件事:

  1. 创建了一个新的 Promise,并定义了 resolve 函数来最终解决这个 Promise。
  2. 初始化了一个 results 数组来存储每个 Promise 的结果。
  3. 使用一个循环遍历输入的 Promise 数组,对每个 Promise 使用 Promise.resolve 来确保它是一个 Promise 实例。
  4. 为每个 Promise 添加 then 方法,分别处理 fulfilled 和 rejected 状态。在每种情况下,我们都会在 results 数组中相应位置存储一个对象,表示该 Promise 的状态和结果。
  5. 每当有一个 Promise settle,我们就增加 completedPromises 计数器。当这个计数器等于输入数组的长度时,意味着所有 Promise 都已经 settle,我们可以解决外层的 Promise 并返回 results 数组。
  6. 如果输入的 Promise 数组为空,我们直接解决外层的 Promise 并返回一个空数组。 这个实现覆盖了 Promise.allSettled 的基本功能。需要注意的是,这个实现没有处理可能的异常,例如输入不是一个可迭代对象或者包含非 Promise 值的情况。在真实环境中,你可能需要添加更多的错误处理来使这个函数更健壮。

10. CSS 尺寸单位有哪些?

CSS 中有多种尺寸单位可用于指定元素的宽度、高度、边距、填充等属性。以下是一些常见的 CSS 尺寸单位:

  1. 绝对单位
    • px:像素,是相对于显示器屏幕分辨率而言的。
    • in:英寸,1英寸等于2.54厘米。
    • cm:厘米。
    • mm:毫米。
    • pt:点,1点等于1/72英寸。
    • pc:派卡,1派卡等于12点。
  2. 相对单位
    • %:百分比,相对于包含块的宽度或高度。
    • em:相对于当前元素的字体大小。
    • rem:相对于根元素(<html>)的字体大小。
    • ex:相对于当前字体中小写字母x的高度。
    • ch:相对于当前字体中数字0的宽度。
    • vw:视口宽度的百分比。
    • vh:视口高度的百分比。
    • vmin:视口最小尺寸的百分比(vwvh中的较小者)。
    • vmax:视口最大尺寸的百分比(vwvh中的较大者)。
    • fr:在网格布局中用于表示可用空间的一部分。
  3. 其他单位
    • deg:度,用于角度值,如旋转 transform。
    • rad:弧度,用于角度值。
    • grad:梯度,用于角度值。
    • turn:圈数,用于角度值,1圈等于360度。
  4. 字体相关单位
    • cap:相对于当前字体中大写字母的高度。
    • ic:相对于当前字体中汉字字符的宽度。
    • lh:相对于当前元素的行高。
    • rlh:相对于根元素的行高。
  5. 无单位值
    • 有些属性可以接受无单位值,例如 line-height 可以直接设置为 11.5 等,表示无单位的比例值。 使用这些单位时,需要根据具体情况选择合适的单位。例如,对于响应式设计,通常使用 %emremvwvh 等相对单位,以便于在不同屏幕尺寸下保持布局的一致性。而对于需要精确控制的场景,如边框宽度、字体大小等,可能会使用 px 这样的绝对单位。

11. React Router 中,HashRouter 和 BrowserRouter 的区别和原理?

在 React Router 中,HashRouterBrowserRouter 是两种常见的路由器,它们用于管理单页应用(SPA)中的路由。它们之间的主要区别在于它们如何处理 URL 和页面渲染。

HashRouter

原理

  • HashRouter 使用 URL 的哈希部分(即 # 后面的部分)来保持 UI 和 URL 同步。
  • 当 URL 的哈希部分发生变化时,浏览器不会向服务器发送请求,而是通过监听 hashchange 事件来更新页面内容。 特点
  • 不需要服务器配置:因为路由变化不会导致页面刷新,所以不需要服务器来处理路由。
  • 兼容性好:即使在旧版浏览器中也能正常工作。 示例
http://example.com/#/home
http://example.com/#/about

BrowserRouter

原理

  • BrowserRouter 使用 HTML5 的 history API 来保持 UI 和 URL 同步。
  • 当使用 pushStatereplaceState 或浏览器的前进、后退按钮导致 URL 变化时,BrowserRouter 会监听这些变化并更新页面内容。 特点
  • 需要服务器配置:服务器需要被配置为总是返回同一个 HTML 页面(通常是应用的入口点),无论请求的 URL 是什么。
  • 更自然的 URL:没有 # 符号,URL 看起来更干净、更符合常规的页面路由方式。 示例
http://example.com/home
http://example.com/about

区别

  1. URL 格式
    • HashRouter:带有 # 的哈希路由。
    • BrowserRouter:没有 # 的普通路由。
  2. 服务器配置
    • HashRouter:不需要特殊的服务器配置。
    • BrowserRouter:需要服务器配置以支持 HTML5 History API。
  3. 浏览器兼容性
    • HashRouter:更好的兼容性,包括旧版浏览器。
    • BrowserRouter:依赖于 HTML5 History API,可能在旧版浏览器中不工作。
  4. 用户体验
    • HashRouter:URL 中包含 #,可能对某些用户来说不太自然。
    • BrowserRouter:提供更自然的 URL 和更好的用户体验。
  5. SEO
    • HashRouter:由于 URL 中包含 #,可能对 SEO 不太友好。
    • BrowserRouter:更符合 SEO 标准,因为 URL 更自然。

选择哪个?

  • 如果你的应用不需要考虑旧版浏览器,并且你想要更自然的 URL,那么 BrowserRouter 是更好的选择。
  • 如果你需要更好的兼容性,或者无法配置服务器以支持 HTML5 History API,那么 HashRouter 是更合适的选择。 在实际使用中,根据应用的需求和目标受众来选择合适的路由器是非常重要的。

12. Proxy 和 Object.defineProperty 的区别是啥?

ProxyObject.defineProperty 都是 JavaScript 中用于操作对象属性的工具,但它们在功能、用法和适用场景上有所区别:

Object.defineProperty

功能

  • 允许精确地添加或修改对象的属性。
  • 可以定义属性的值、可枚举性、可配置性、可写性,以及 getter 和 setter。 用法
Object.defineProperty(obj, prop, descriptor);
  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称。
  • descriptor:属性描述符,包含属性的特性。 示例
let obj = {};
Object.defineProperty(obj, 'name', {
  value: 'Alice',
  writable: true,
  enumerable: true,
  configurable: true
});

特点

  • 只能针对单个属性进行操作。
  • 可以实现数据绑定和响应式系统,如 Vue 2.x 的响应式原理。 局限性
  • 无法检测到对象属性的添加或删除。
  • 无法检测到数组索引和长度的变化。

Proxy

功能

  • 允许创建一个代理对象,这个代理对象可以拦截并定义基本操作的行为,如属性查找、赋值、枚举、函数调用等。
  • 提供了更强大的拦截能力,可以拦截13种操作。 用法
let proxy = new Proxy(target, handler);
  • target:要代理的目标对象。
  • handler:一个对象,其属性是当执行一个操作时定义代理行为的函数。 示例
let obj = { name: 'Alice' };
let proxy = new Proxy(obj, {
  get(target, prop) {
    return target[prop];
  },
  set(target, prop, value) {
    target[prop] = value;
    return true;
  }
});

特点

  • 可以针对整个对象进行操作,而不仅仅是单个属性。
  • 可以检测到对象属性的添加、删除,以及数组的变化。
  • 提供了更细粒度的控制,可以拦截更多的操作。 优势
  • 更灵活、更强大,可以用于实现复杂的代理模式。
  • 可以用于实现深度响应式系统,如 Vue 3.x 的响应式原理。

区别

  1. 拦截范围
    • Object.defineProperty:只能拦截属性的读取和设置。
    • Proxy:可以拦截13种操作,包括属性读取、设置、删除、函数调用等。
  2. 对象范围
    • Object.defineProperty:针对单个属性。
    • Proxy:针对整个对象。
  3. 性能
    • Object.defineProperty:在Vue 2.x中广泛使用,性能优化较为成熟。
    • Proxy:在Vue 3.x中采用,性能更优,但可能在旧版浏览器中性能较差。
  4. 兼容性
    • Object.defineProperty:兼容性较好,IE8及以上支持。
    • Proxy:兼容性较差,需要ES6环境,不支持IE。
  5. 使用场景
    • Object.defineProperty:适用于需要定义单个属性特性或实现简单响应式的场景。
    • Proxy:适用于需要全面拦截对象操作或实现复杂代理模式的场景。 在选择使用 Proxy 还是 Object.defineProperty 时,需要根据具体的需求、性能考虑和兼容性要求来决定。

13. css 中三栏布局的实现方案有哪些?

CSS 中实现三栏布局的方案有多种,以下是几种常见的实现方式:

1. 浮动布局(Float)

原理:通过给左右两栏设置浮动,中间栏自适应宽度。 示例

.left {
  float: left;
  width: 200px;
}
.right {
  float: right;
  width: 200px;
}
.middle {
  margin-left: 200px;
  margin-right: 200px;
}

缺点:浮动布局可能会出现高度塌陷的问题,需要清除浮动。

2. 绝对定位布局(Absolute)

原理:通过给左右两栏设置绝对定位,中间栏自适应宽度。 示例

.container {
  position: relative;
}
.left {
  position: absolute;
  left: 0;
  width: 200px;
}
.right {
  position: absolute;
  right: 0;
  width: 200px;
}
.middle {
  margin-left: 200px;
  margin-right: 200px;
}

缺点:绝对定位的元素会脱离文档流,可能会影响其他元素的布局。

3. flex布局(Flexbox)

原理:使用Flexbox布局模型,通过设置容器的display: flex;和子项的flex属性来实现。 示例

.container {
  display: flex;
}
.left {
  flex: 0 0 200px;
}
.middle {
  flex: 1;
}
.right {
  flex: 0 0 200px;
}

优点:Flexbox布局更加灵活,易于实现各种布局需求。

4. 网格布局(Grid)

原理:使用CSS Grid布局模型,通过定义网格容器和网格项来实现。 示例

.container {
  display: grid;
  grid-template-columns: 200px auto 200px;
}
.left {
  grid-column: 1;
}
.middle {
  grid-column: 2;
}
.right {
  grid-column: 3;
}

优点:网格布局提供了最强大的布局能力,可以轻松实现复杂的布局设计。

5. 圣杯布局(Holy Grail Layout)

原理:通过浮动和负边距来实现,中间栏先渲染,左右两栏通过负边距移动到指定位置。 示例

.container {
  padding-left: 200px;
  padding-right: 200px;
}
.left {
  float: left;
  width: 200px;
  margin-left: -100%;
  position: relative;
  left: -200px;
}
.right {
  float: right;
  width: 200px;
  margin-right: -200px;
}
.middle {
  float: left;
  width: 100%;
}
.clearfix::after {
  content: "";
  display: table;
  clear: both;
}

缺点:实现较为复杂,需要使用负边距和定位进行调整。

6. 双飞翼布局

原理:与圣杯布局类似,但通过给中间栏包裹一个父元素来解决布局问题。 示例

.container {
  overflow: hidden;
}
.middle-wrapper {
  float: left;
  width: 100%;
}
.middle {
  margin-left: 200px;
  margin-right: 200px;
}
.left {
  float: left;
  width: 200px;
  margin-left: -100%;
}
.right {
  float: right;
  width: 200px;
  margin-left: -200px;
}

优点:解决了圣杯布局中中间栏内容不被左右栏覆盖的问题。 选择哪种布局方式取决于具体的项目需求、浏览器兼容性要求以及个人偏好。现代前端开发中,flex布局和网格布局由于其灵活性和强大功能,越来越受到青睐。

14. es5 和 es6 使用 new 关键字实例化对象的流程是一样的吗?

ES5和ES6使用new关键字实例化对象的流程在本质上是相似的,但有一些细节上的差异,主要因为ES6引入了类(class)的概念。

ES5中的实例化流程:

  1. 创建一个新的空对象var obj = {}
  2. 设置新对象的__proto__属性:将其指向构造函数的prototype对象,即obj.__proto__ = Constructor.prototype
  3. 执行构造函数:将构造函数的作用域赋给新对象,即Constructor.call(obj, arguments)。这样,构造函数中的this就指向了新创建的对象,并且可以添加属性和方法。
  4. 返回新对象:如果构造函数没有显式返回一个对象,则返回新创建的对象。

ES6中的实例化流程(使用class):

  1. 创建一个新的空对象
  2. 设置新对象的__proto__属性:将其指向类的prototype对象。
  3. 执行类的构造方法constructor,将构造方法的作用域赋给新对象,即constructor.call(obj, arguments)。在ES6的类中,必须有一个constructor方法,它是类的默认构造函数。
  4. 返回新对象:如果构造方法没有显式返回一个对象,则返回新创建的对象。

主要差异:

  • 语法糖:ES6的类本质上是一个语法糖,它使得对象创建更加直观和易于理解。但在底层,它仍然使用原型和构造函数的概念。
  • constructor方法:在ES6的类中,constructor方法是必须的,而在ES5的构造函数中,没有这样的强制要求。
  • 原型方法的定义:在ES5中,原型方法是通过构造函数的prototype属性添加的,而在ES6中,可以直接在类定义中添加方法,这些方法会自动出现在类的prototype上。 尽管有一些细节上的差异,但使用new关键字实例化对象的基本流程在ES5和ES6中是相同的。都是为了创建一个新的对象,并将其与构造函数或类关联起来,以便能够访问构造函数或类的方法和属性。

15. 如何实现可过期的 localStorage 数据?

实现可过期的localStorage数据,可以使用以下方法:

  1. 设置一个过期时间:首先,你需要为存储的数据设置一个过期时间。这可以通过localStoragesetItem方法实现,该方法允许你为存储的数据设置一个过期时间。
  2. 使用setTimeout函数:然后,使用setTimeout函数来设置一个定时器,当时间到达时,执行一个回调函数来删除存储的数据。 以下是一个简单的示例代码:
// 设置localStorage数据并指定过期时间
localStorage.setItem('key', 'value', expirationTime);
// 设置定时器,在指定时间后删除数据
setTimeout(function() {
    localStorage.removeItem('key');
}, expirationTime);

在这个示例中,expirationTime是你为数据设置的过期时间(以毫秒为单位)。当时间到达时,setTimeout的回调函数将执行,删除对应的localStorage数据。 这种方法可以确保数据在指定时间后自动过期,从而实现可过期的localStorage数据。

16. 说说 axios 的拦截器原理及应用,并简单手写核心逻辑

Axios拦截器原理: Axios拦截器是一种在请求或响应被处理之前进行拦截的机制。它允许你在请求发送到服务器之前或从服务器收到响应之后,对请求或响应进行修改。 工作原理:

  1. 拦截器注册:通过axios.interceptors.request.use()axios.interceptors.response.use()方法注册请求和响应拦截器。
  2. 拦截器队列:注册的拦截器被添加到内部的请求拦截器队列和响应拦截器队列中。
  3. 请求拦截:当一个请求发起时,会依次执行请求拦截器队列中的拦截器函数,可以对请求配置进行修改。
  4. 发送请求:经过所有请求拦截器处理后的请求配置被用于发送请求。
  5. 响应拦截:收到响应后,会依次执行响应拦截器队列中的拦截器函数,可以对响应数据进行修改。
  6. 返回结果:经过所有响应拦截器处理后的响应数据被返回给调用者。 应用场景:
  • 请求前处理:如添加请求头、认证token、加载动画等。
  • 响应后处理:如统一处理错误、提取数据、关闭加载动画等。
  • 日志记录:记录请求和响应的数据,便于调试和监控。
  • 缓存策略:实现请求缓存,减少不必要的网络请求。 简单手写核心逻辑: 以下是一个简化的拦截器实现,仅包含核心逻辑:
class SimpleAxios {
  constructor() {
    this.requestInterceptors = [];
    this.responseInterceptors = [];
  }
  // 注册请求拦截器
  request.use(interceptor) {
    this.requestInterceptors.push(interceptor);
  }
  // 注册响应拦截器
  response.use(interceptor) {
    this.responseInterceptors.push(interceptor);
  }
  // 发送请求
  async fetch(config) {
    // 执行请求拦截器
    for (const interceptor of this.requestInterceptors) {
      config = await interceptor(config);
    }
    // 模拟发送请求
    const response = await this.sendRequest(config);
    // 执行响应拦截器
    for (const interceptor of this.responseInterceptors) {
      response = await interceptor(response);
    }
    return response;
  }
  // 模拟发送请求的方法
  async sendRequest(config) {
    // 这里应该是实际的请求发送逻辑,例如使用XMLHttpRequest或fetch API
    // 为了演示,我们直接返回一个模拟的响应对象
    return {
      data: '模拟的响应数据',
      status: 200
    };
  }
}
// 使用示例
const axios = new SimpleAxios();
// 添加请求拦截器
axios.request.use(config => {
  // 修改请求配置
  config.headers.Authorization = 'Bearer token';
  return config;
});
// 添加响应拦截器
axios.response.use(response => {
  // 修改响应数据
  response.data = '修改后的响应数据';
  return response;
});
// 发送请求
axios.fetch({ url: 'https://api.example.com/data' }).then(response => {
  console.log(response.data); // 输出:修改后的响应数据
});

在这个简化的实现中,我们创建了一个SimpleAxios类,它具有注册请求和响应拦截器的方法,以及一个fetch方法来模拟发送请求。拦截器被存储在数组中,并在请求发送前后依次执行。这只是一个核心逻辑的演示,实际的Axios库要复杂得多,包括错误处理、取消请求、并发请求管理等特性。

17. 怎么分析页面加载慢的原因?

分析页面加载慢的原因是一个多步骤的过程,涉及不同的工具和技术。以下是一些常用的方法和步骤:

1. 使用浏览器的开发者工具

Chrome开发者工具是一个强大的工具,可以用来分析页面加载性能。

  • 网络面板
    • 打开开发者工具(F12或右键选择“检查”),切换到“网络”面板。
    • 刷新页面以捕获加载过程中的网络请求。
    • 查看每个资源的请求时间和大小,找出延迟较大的资源。
  • 性能面板
    • 切换到“性能”面板。
    • 点击“录制”按钮,然后刷新页面。
    • 查看页面加载的瀑布图,分析CPU和GPU的活动,找出瓶颈。
  • 内存面板
    • 检查内存使用情况,查看是否有内存泄漏。

2. 使用性能分析工具

  • PageSpeed Insights:Google提供的工具,可以分析页面性能并提出优化建议。
  • Lighthouse:也是Google的工具,集成在Chrome开发者工具中,提供全面的性能评估。
  • WebPageTest:允许从不同地点和浏览器测试页面加载时间。

3. 检查服务器性能

  • 服务器响应时间:使用工具如Pingdom或直接检查服务器日志。
  • 数据库查询:慢查询可能影响页面加载时间,使用数据库分析工具检查。

4. 分析代码

  • 前端代码
    • 检查是否有大量的JavaScript或CSS阻塞渲染。
    • 优化图片和媒体文件的大小和格式。
    • 确保使用异步或延迟加载脚本。
  • 后端代码
    • 检查API响应时间。
    • 优化数据库查询和服务器端逻辑。

5. 检查网络问题

  • 带宽限制:网络带宽不足可能导致加载慢。
  • DNS解析时间:慢的DNS解析会影响页面加载。

6. 考虑第三方服务

  • CDN:内容分发网络的问题可能影响加载时间。
  • 外部脚本:如广告、分析脚本等可能造成延迟。

7. 用户行为和设备

  • 设备性能:低性能设备上加载可能更慢。
  • 用户网络:不同用户的网络条件不同。

8. 实时监控和日志

  • 监控工具:如New Relic、Datadog等可以实时监控页面性能。
  • 服务器日志:分析服务器日志以找出异常情况。

9. 进行A/B测试

  • 对不同版本的页面进行测试,看哪个版本加载更快。

10. 咨询专家

  • 如果自己无法解决问题,可以考虑咨询专业的性能优化专家。 通过这些步骤,你可以逐步缩小问题范围,找出页面加载慢的原因,并采取相应的优化措施。记住,性能优化是一个持续的过程,需要不断地监控和调整。

18. 以用户为中心的前端性能指标有哪些?

以用户为中心的前端性能指标主要关注用户实际体验到的页面加载和交互速度。以下是一些关键的性能指标:

1. 首次内容绘制(FCP)

  • 定义:测量从页面开始加载到页面内容的任何部分在屏幕上完成渲染的时间。
  • 重要性:给用户一个初步的反馈,表明页面正在加载。

2. 首次输入延迟(FID)

  • 定义:测量从用户首次与页面交互(如点击按钮)到浏览器能够响应这次交互的时间。
  • 重要性:反映了页面的可交互性。

3. 最大内容绘制(LCP)

  • 定义:测量页面主要内容加载完成的时间,通常是对用户最关心的内容的加载时间。
  • 重要性:反映了页面的主要内容何时变得可见。

4. 累积布局偏移(CLS)

  • 定义:测量页面加载过程中发生的视觉不稳定性的总量,即元素位置的意外移动。
  • 重要性:反映了页面的视觉稳定性,减少用户在阅读或交互时的干扰。

5. 可交互时间(TTI)

  • 定义:测量页面完全可交互的时间,即用户可以顺畅地与页面进行交互的时间点。
  • 重要性:确保用户在页面加载后能够立即使用。

6. 速度指数(Speed Index)

  • 定义:衡量页面内容可见的速度,数值越低,表示页面加载越快。
  • 重要性:提供了一个整体的速度感。

7. 总阻塞时间(TBT)

  • 定义:测量在FCP和TTI之间长时间主线程任务的累计时间。
  • 重要性:反映了页面在加载过程中的响应性。

8. 页面完全加载时间(Onload)

  • 定义:测量从页面开始加载到完全加载完成的时间。
  • 重要性:虽然不是用户体验的直接指标,但可以作为参考。

9. 请求响应时间(RTT)

  • 定义:测量从发送请求到接收响应所需的时间。
  • 重要性:影响了页面加载的起始速度。

10. 自定义指标

- 根据特定应用的需求,可以定义一些特定的性能指标,如特定功能的加载时间、特定用户的操作响应时间等。

这些指标可以通过各种工具来测量,如Google的Lighthouse、Chrome开发者工具的性能面板、WebPageTest等。通过关注这些以用户为中心的性能指标,开发者可以更好地优化前端性能,提升用户体验。

19. 如何组织 monorepo 工程?

Monorepo是一种项目组织方式,它将多个项目的代码库集中在一个单一的代码仓库中。这种组织方式有助于简化依赖管理、促进代码共享和重用,以及统一构建流程。以下是组织Monorepo工程的一些建议:

1. 目录结构

建立一个清晰的目录结构是Monorepo成功的关键。常见的结构如下:

/my-monorepo
|-- /packages
|   |-- /package-a
|   |-- /package-b
|   |-- /package-c
|-- /apps
|   |-- /app-a
|   |-- /app-b
|-- /tools
|-- /scripts
|-- /docs
|-- README.md
|-- package.json
  • packages:存放可重用的库或模块。
  • apps:存放独立的应用程序。
  • tools:存放用于构建、测试、部署等的工具。
  • scripts:存放用于自动化任务的脚本。
  • docs:存放项目文档。

2. 依赖管理

  • 使用统一的package.json管理所有项目的依赖,或者在每个子项目中使用独立的package.json
  • 利用yarnnpm的工作区(workspaces)功能来管理Monorepo中的依赖。

3. 构建系统

  • 使用支持Monorepo的构建工具,如Lerna、Nx、Turborepo等。
  • 定义统一的构建脚本和配置,以便于在所有子项目中共享。

4. 版本控制

  • 使用语义化版本控制(SemVer)来管理子项目的版本。
  • 利用Lerna等工具自动管理和发布子项目的版本。

5. 测试

  • 为每个子项目定义独立的测试套件。
  • 使用持续集成(CI)工具来运行所有子项目的测试。

6. 代码共享和重用

  • 鼓励通过内部包(internal packages)来共享和重用代码。
  • 定义清晰的API和文档,以便于其他开发者使用。

7. 模块边界

  • 明确每个模块的职责和边界,避免模块间的耦合。
  • 使用模块化的代码组织方式,如模块、包或命名空间。

8. 文档

  • 为整个Monorepo和每个子项目提供清晰的文档。
  • 包括如何构建、测试、部署以及如何贡献代码的指南。

9. 贡献指南

  • 制定贡献指南,说明如何提交问题、提出拉取请求以及代码审查的流程。

10. 权限和访问控制

  • 根据团队成员的角色和职责,设置适当的权限和访问控制。

11. 持续集成和持续部署(CI/CD)

  • 设置CI/CD管道来自动化构建、测试和部署流程。

12. 代码审查

  • 实施代码审查流程,确保代码质量和一致性。 通过遵循这些原则和组织结构,可以有效地管理Monorepo工程,提高开发效率,同时保持代码的清晰和可维护性。

20. HTTPS 中的 SSL/TLS 是什么?

HTTPS中的SSL/TLS是用于在计算机网络上提供安全通信的协议。它们确保了数据在传输过程中的机密性、完整性和身份验证。下面是SSL和TLS的简要解释:

SSL(Secure Sockets Layer)

  • 定义:SSL是一种安全协议,用于在互联网上提供安全的数据传输。它是由Netscape在1990年代中期开发的。
  • 版本:SSL有多个版本,包括SSL 1.0(从未公开发布)、SSL 2.0、SSL 3.0。由于安全漏洞,SSL 2.0和SSL 3.0已经不再使用。
  • 功能:SSL提供了数据加密、服务器认证、消息完整性以及可选的客户端认证。

TLS(Transport Layer Security)

  • 定义:TLS是SSL的后继协议,由互联网工程任务组(IETF)在1999年标准化。TLS旨在解决SSL的一些安全问题和限制。
  • 版本:TLS有多个版本,包括TLS 1.0、TLS 1.1、TLS 1.2和TLS 1.3。较新的版本提供了更强的安全性和性能改进。
  • 功能:TLS提供了与SSL类似的功能,包括数据加密、服务器和客户端认证、消息完整性等,但具有更好的安全性和性能。

SSL/TLS的工作原理

  1. 握手阶段
    • 客户端和服务器通过握手阶段协商使用的SSL/TLS版本、加密算法、交换密钥等。
    • 服务器发送其证书以证明其身份,并可能请求客户端证书。
    • 双方生成会话密钥,用于数据加密。
  2. 数据传输阶段
    • 使用会话密钥对数据进行加密和解密,确保数据在传输过程中的机密性。
    • 使用消息认证码(MAC)或类似机制确保数据的完整性。
  3. 关闭连接
    • 客户端和服务器交换消息以关闭SSL/TLS连接。

为什么使用SSL/TLS?

  • 安全性:保护数据免受窃听、篡改和伪造。
  • 信任:通过证书验证服务器的身份,建立用户对网站的信任。
  • 合规性:许多行业标准和法规要求使用SSL/TLS来保护数据。

HTTPS与SSL/TLS的关系

HTTPS(HyperText Transfer Protocol Secure)是HTTP的安全版本,它使用SSL/TLS来加密HTTP请求和响应。当你在浏览器中看到"https://"而不是"http://"时,这意味着连接是使用SSL/TLS加密的。 总之,SSL/TLS是HTTPS的核心,为互联网上的通信提供了必要的安全保障。随着网络安全威胁的不断演变,保持SSL/TLS协议的更新和实施最佳实践是至关重要的。

21. HTTPS 加密算法和加解密过程是啥?

HTTPS使用SSL/TLS协议来实现数据的加密传输,其中涉及多种加密算法和加解密过程。以下是HTTPS中常用的加密算法和加解密过程的概述:

常用的加密算法

  1. 对称加密算法
    • AES(Advanced Encryption Standard):目前最常用的对称加密算法,支持128位、192位和256位密钥。
    • 3DES(Triple Data Encryption Standard):使用三个56位的DES密钥对数据进行三次加密,但已逐渐被AES取代。
  2. 非对称加密算法
    • RSA(Rivest-Shamir-Adleman):广泛使用的非对称加密算法,用于密钥交换和数字签名。
    • ECC(Elliptic Curve Cryptography):基于椭圆曲线的加密算法,提供与RSA相当的安全性但使用更短的密钥。
  3. 散列函数
    • SHA(Secure Hash Algorithm):包括SHA-1、SHA-256、SHA-384和SHA-512等,用于数据完整性验证。
    • MD5(Message Digest Algorithm 5):曾经广泛使用,但由于安全漏洞,现已不推荐用于安全场景。
  4. 消息认证码(MAC)
    • HMAC(Hash-based Message Authentication Code):结合散列函数和密钥,用于验证消息的完整性和认证。

加解密过程

HTTPS的加解密过程主要发生在SSL/TLS握手阶段和数据传输阶段:

  1. 握手阶段
    • 密钥交换:客户端和服务器使用非对称加密算法(如RSA或ECC)来安全地交换对称加密密钥。
    • 服务器认证:服务器发送其数字证书,客户端验证证书的有效性以确认服务器的身份。
    • 客户端认证(可选):如果需要,客户端也可以发送其证书进行认证。
    • 会话密钥生成:双方根据交换的信息生成对称加密的会话密钥。
  2. 数据传输阶段
    • 数据加密:使用对称加密算法(如AES)和会话密钥对数据进行加密。
    • 数据传输:加密后的数据通过网络传输到对方。
    • 数据解密:接收方使用相同的会话密钥对数据进行解密。
  3. 完整性验证
    • MAC计算:发送方使用MAC算法(如HMAC)和密钥对数据进行完整性验证。
    • MAC验证:接收方计算并验证MAC,以确保数据在传输过程中未被篡改。

具体的加解密步骤

  1. 客户端发送请求
    • 客户端发起HTTPS请求,开始SSL/TLS握手。
  2. 服务器响应
    • 服务器发送其证书和所支持的加密算法列表。
  3. 密钥交换
    • 客户端验证服务器证书,然后生成预主密钥,并使用服务器的公钥加密后发送给服务器。
  4. 会话密钥生成
    • 双方根据预主密钥和随机数生成会话密钥。
  5. 完成握手
    • 客户端和服务器交换握手完成消息,此时握手结束,进入数据传输阶段。
  6. 数据加密传输
    • 双方使用会话密钥对数据进行加密和解密,确保数据的安全传输。
  7. 关闭连接
    • 数据传输完成后,双方通过交换特定消息来关闭SSL/TLS连接。 HTTPS的加密算法和加解密过程确保了数据在互联网上的安全传输,保护了数据的机密性、完整性和身份认证。随着网络安全技术的发展,SSL/TLS协议和所使用的加密算法也会不断更新和改进。

22. 如何监控前端页面的崩溃?

监控前端页面崩溃是确保网站或应用程序稳定性和用户体验的重要环节。以下是一些常用的方法和工具来监控前端页面的崩溃:

1. 错误捕获与报告

JavaScript 错误捕

  • try...catch:捕获运行时错误。
  • window.onerror:全局错误处理,可以捕获未处理的错误。
  • addEventListener('error'):用于捕获资源加载错误,如脚本、样式表或图片加载失败。

错误报告服务

  • Sentry:自动捕获和报告JavaScript错误,支持前端和后端。
  • Bugsnag:实时错误监控和报告服务。
  • Rollbar:提供实时的错误追踪和警报。

2. 性能监控

  • Performance API:监控页面加载时间和性能指标。
  • Lighthouse:Google提供的开源工具,用于改进网页质量和性能。
  • Web Vitals:Google推荐的核心网页性能指标。

3. 实时用户监控

  • FullStory:记录用户会话,重现崩溃时的用户行为。
  • Hotjar:用户行为分析和反馈工具。
  • Session Replay:回放用户会话,分析崩溃原因。

4. 日志管理

  • LogRocket:记录用户会话和日志,帮助诊断问题。
  • ELK Stack(Elasticsearch, Logstash, Kibana):用于日志收集、分析和可视化。

5. 自定义监控脚本

  • 编写自定义JavaScript脚本,捕获特定错误或异常,并通过AJAX发送到服务器进行记录。

6. 服务端监控

  • New Relic:应用性能监控(APM)服务,可以监控前端和后端性能。
  • Datadog:提供全面的监控和警报服务。

7. 使用浏览器扩展

  • 开发浏览器扩展来监控和报告页面崩溃。

8. CI/CD 集成

  • 在持续集成/持续部署(CI/CD)流程中集成前端测试和监控工具。

实施步骤

  1. 选择合适的工具:根据项目需求和预算选择合适的监控工具。
  2. 集成监控代码:在项目中集成监控服务的SDK或API。
  3. 配置警报:设置警报规则,以便在发生崩溃时及时通知开发团队。
  4. 分析报告:定期分析监控报告,识别常见问题和性能瓶颈。
  5. 优化代码:根据监控结果优化前端代码,减少崩溃发生。

注意事项

  • 隐私保护:确保用户数据的安全和隐私。
  • 性能影响:监控代码应尽量减少对页面性能的影响。
  • 全面覆盖:确保监控覆盖所有重要的页面和功能。 通过上述方法和工具的组合使用,可以有效地监控前端页面的崩溃,并及时做出响应,提高应用的稳定性和用户体验。

23. 实现 table header 吸顶, 有哪些实现方式?

实现表格头部(table header)吸顶功能,即当用户滚动页面时,表格头部始终固定在页面顶部,有几种常见的实现方式:

1. CSS 方式(使用 position: sticky

thead {
  position: sticky;
  top: 0;
  background-color: #fff; /* 保持背景颜色与表格一致 */
}

这种方法简单且性能较好,但需要注意浏览器的兼容性。

2. JavaScript 方式

通过 JavaScript 监听滚动事件,并动态修改表格头部的位置。

window.addEventListener('scroll', function() {
  var tableHeader = document.querySelector('thead');
  var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  if (scrollTop > tableOffsetTop) {
    tableHeader.style.position = 'fixed';
    tableHeader.style.top = '0';
  } else {
    tableHeader.style.position = 'static';
  }
});

这种方法兼容性好,但可能会影响性能,因为滚动事件会频繁触发。

3. CSS + JavaScript 方式

结合 CSS 和 JavaScript,使用 CSS 进行样式设置,JavaScript 进行位置判断。

.table-header-fixed {
  position: fixed;
  top: 0;
  background-color: #fff;
}
window.addEventListener('scroll', function() {
  var tableHeader = document.querySelector('thead');
  var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  if (scrollTop > tableOffsetTop) {
    tableHeader.classList.add('table-header-fixed');
  } else {
    tableHeader.classList.remove('table-header-fixed');
  }
});

这种方法兼顾了样式和功能,也是比较常见的实现方式。

4. 使用第三方库

例如使用 jQuery 插件 floatThead 或其他前端框架的插件来实现。

$('#myTable').floatThead();

这种方法简单快捷,但需要引入额外的库,可能会增加页面加载时间。

5. 使用 CSS 的 transform 属性

通过 transform 属性实现吸顶效果,可以避免影响文档流。

.table-header-fixed {
  transform: translateY(-100%);
  background-color: #fff;
}
window.addEventListener('scroll', function() {
  var tableHeader = document.querySelector('thead');
  var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  if (scrollTop > tableOffsetTop) {
    tableHeader.style.transform = 'translateY(' + (scrollTop - tableOffsetTop) + 'px)';
  } else {
    tableHeader.style.transform = 'none';
  }
});

这种方法可以避免固定定位可能带来的问题,如遮挡其他元素。

注意事项

  • 性能优化:避免在滚动事件中执行复杂操作,可以使用节流(throttle)或防抖(debounce)技术。
  • 兼容性:考虑不同浏览器的兼容性,特别是旧版浏览器。
  • 样式调整:确保吸顶后表格头部的样式与原样式一致,避免出现视觉上的不连贯。
  • 复杂表格:对于包含合并单元格、固定列等复杂情况的表格,可能需要更复杂的处理。 根据项目需求和浏览器兼容性要求,可以选择最适合的实现方式。

24. CSS 中的文档流是什么?

CSS中的文档流(Document Flow)是指页面中的元素按照HTML代码的顺序排列,从上到下,从左到右的布局方式。文档流是CSS布局的基础,理解文档流对于掌握CSS布局非常重要。

文档流的特点:

  1. 块级元素(如div, p, header, footer等)通常占据整个父元素的宽度,内容从上到下排列。
  2. 内联元素(如span, a, img等)不会独占一行,而是根据内容多少占据相应的宽度,内容从左到右排列。
  3. 浮动元素(使用float属性)会脱离文档流,但仍然会影响周围元素的布局。
  4. 定位元素(使用position属性)可以完全脱离文档流,根据定位方式(如绝对定位、相对定位)进行布局。

文档流的重要性:

  • 布局基础:文档流是CSS布局的起点,所有的布局技巧都是基于文档流进行的。
  • 影响浮动和定位:理解文档流有助于掌握浮动和定位的使用,因为这些属性会改变元素在文档流中的位置。
  • 解决布局问题:很多布局问题(如元素重叠、间距异常等)都可以通过理解文档流来解决。

文档流的典型应用:

  • 文本布局:段落、标题等文本内容的布局通常遵循文档流。
  • 列表布局:无序列表、有序列表等也遵循文档流。
  • 简单布局:对于简单的页面布局,直接使用文档流即可满足需求。

脱离文档流:

  • 浮动:使用float属性可以使元素脱离文档流,但仍然会影响周围元素的布局。
  • 定位:使用position属性(如absolute, fixed)可以使元素完全脱离文档流,根据定位方式独立布局。 理解文档流是学习CSS布局的关键,它有助于我们更好地掌握页面布局的原理和技巧。

25. 怎么在 koa 中,进行中间件的异常处理?

在Koa中,进行中间件的异常处理主要有两种方式:使用try...catch语句和利用Koa的错误处理机制。

方法一:使用try...catch

在每个中间件中,你可以使用try...catch来捕获可能发生的异常。

const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = { message: err.message };
    // 可以在这里记录日志
    console.error(err);
  }
});
// 一个可能会抛出异常的中间件
app.use(async (ctx, next) => {
  // 假设这里有一个可能出错的操作
  throw new Error('Something went wrong');
});
app.listen(3000);

方法二:利用Koa的错误处理机制

Koa提供了一个错误处理机制,你可以通过监听app.on('error')来捕获和处理异常。

const Koa = require('koa');
const app = new Koa();
// 错误处理中间件
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = { message: err.message };
    // 抛出异常,可以在下面的错误监听中捕获
    ctx.app.emit('error', err, ctx);
  }
});
// 一个可能会抛出异常的中间件
app.use(async (ctx, next) => {
  // 假设这里有一个可能出错的操作
  throw new Error('Something went wrong');
});
// 监听错误事件
app.on('error', (err, ctx) => {
  // 在这里可以进行错误日志记录等操作
  console.error('server error', err, ctx);
});
app.listen(3000);

最佳实践

在实际应用中,通常会将错误处理逻辑封装成一个单独的中间件,然后在应用的最顶层使用这个中间件,以确保所有下游中间件中的异常都能被捕获和处理。

const Koa = require('koa');
const app = new Koa();
// 错误处理中间件
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = { message: err.message };
    ctx.app.emit('error', err, ctx);
  }
});
// 其他中间件...
// 监听错误事件
app.on('error', (err, ctx) => {
  // 记录错误日志
  console.error('server error', err, ctx);
});
app.listen(3000);

这样,无论是同步代码还是异步代码中抛出的异常,都能够被有效地捕获和处理。记得在错误处理中设置合适的HTTP状态码,并提供有用的错误信息,但同时要注意不要泄露敏感信息给客户端。

26. 在没有async/await 的时候, koa是怎么实现的洋葱模型?

在没有async/await的情况下,Koa使用的是生成器(Generators)来实现洋葱模型(Middleware Stack)。生成器是ES6引入的一种新的函数类型,它可以在函数执行过程中多次暂停和恢复,这为异步流程控制提供了一种新的方式。 在Koa的早期版本(如Koa 1.x)中,中间件是通过生成器函数来实现的。下面是一个使用生成器实现洋葱模型的简单示例:

const Koa = require('koa');
const co = require('co'); // co是一个用于处理生成器函数的库
const app = new Koa();
// 中间件1:请求开始时打印日志
app.use(function *(next) {
  console.log('Middleware 1: Before');
  yield next; // 暂停执行,等待下一个中间件
  console.log('Middleware 1: After');
});
// 中间件2:处理业务逻辑
app.use(function *(next) {
  console.log('Middleware 2: Before');
  // 假设这里有一些异步操作
  this.body = 'Hello World';
  yield next; // 继续执行下一个中间件
  console.log('Middleware 2: After');
});
// 中间件3:请求结束时打印日志
app.use(function *(next) {
  console.log('Middleware 3: Before');
  yield next; // 这里没有下一个中间件,所以直接继续执行
  console.log('Middleware 3: After');
});
app.listen(3000);

在这个示例中,每个中间件都是一个生成器函数,它们通过yield next来暂停执行,并等待下一个中间件执行完成。当下一个中间件执行完成后,当前中间件会从yield next之后继续执行。 Koa使用了一个库 called co 来自动执行这些生成器函数,并处理它们之间的异步流程。co会接收一个生成器函数,然后自动迭代它,直到迭代器完成。 这种生成器的方式实现了洋葱模型,即请求进入时从外层中间件开始,逐层向内执行,直到最内层中间件,然后再从最内层中间件逐层向外执行,直到完成整个请求-响应周期。 随着JavaScript的发展,async/await成为了更简洁、更直观的异步编程方式,因此在Koa 2.x及以后的版本中,生成器被async/await所取代。但是,了解生成器的工作原理有助于理解Koa的早期实现和洋葱模型的概念。

27. Vue 的响应式数据流驱动页面和传统的事件绑定命令式驱动页面,分别有什么优缺点?

Vue的响应式数据流驱动页面和传统的事件绑定命令式驱动页面是两种不同的前端开发模式,它们各有优缺点。以下是它们的主要区别: Vue的响应式数据流驱动页面(声明式编程): 优点:

  1. 数据驱动:开发者只需关注数据的变化,Vue会自动更新DOM,减少了直接操作DOM的需求。
  2. 简洁易读:模板语法简洁,代码更易于理解和维护。
  3. 组件化:支持组件化开发,提高了代码的复用性和模块化。
  4. 响应式更新:数据变化时,只有相关的DOM会重新渲染,提高了性能。
  5. 双向绑定:简化了数据同步的过程,减少了事件监听和状态管理的复杂性。 缺点:
  6. 学习曲线:对于新手来说,需要学习Vue的特定概念和语法。
  7. 性能问题:在大型应用中,如果不当使用,可能会出现性能问题,如过度渲染。
  8. 调试困难:由于数据流是自动管理的,调试时可能难以追踪数据变化的具体原因。 传统的事件绑定命令式驱动页面: 优点:
  9. 直接控制:开发者对DOM的操作有更直接的控制,适合需要精细操作的场景。
  10. 简单直接:对于简单的页面,命令式编程可能更直接、更容易实现。
  11. 兼容性好:不需要依赖现代框架,可以在更老的浏览器上运行。 缺点:
  12. 代码冗余:需要手动绑定事件和处理DOM更新,代码容易变得冗余和复杂。
  13. 维护困难:随着应用复杂度的增加,代码维护变得困难。
  14. 状态管理复杂:需要手动管理状态和DOM的同步,容易出错。
  15. 性能问题:频繁的DOM操作可能导致性能问题。 总结来说,Vue的响应式数据流驱动页面更适合现代Web应用的开发,它提供了更高效、更简洁的开发方式,尤其适用于复杂的应用。而传统的事件绑定命令式驱动页面在一些简单或需要精细控制的场景中可能更有优势,但在大型应用中往往显得力不从心。随着前端技术的发展,声明式编程和响应式数据流已经成为了主流的开发模式。

28. 为什么小程序中无法使用 dom 相关的 api?

小程序中无法使用DOM相关的API的主要原因如下:

  1. 运行环境不同
    • 小程序主要运行在微信、支付宝等特定的APP环境中,这些环境与传统的Web浏览器环境不同。
    • 小程序的渲染层和逻辑层是分离的,渲染层运行在特定的渲染引擎中,而逻辑层运行在JavaScript引擎中。这种架构不支持传统的DOM操作。
  2. 安全性考虑
    • 小程序需要在APP内安全地运行,避免恶意代码通过DOM操作影响APP的安全和稳定性。
    • 禁用DOM API可以减少XSS(跨站脚本攻击)等安全风险。
  3. 性能优化
    • 小程序的设计初衷是提供更流畅的用户体验,直接操作DOM可能会导致性能问题。
    • 通过数据绑定和响应式更新,小程序可以更高效地管理UI的更新,减少不必要的DOM操作。
  4. 简化开发
    • 小程序提供了自己的组件系统和事件系统,这些系统简化了开发过程,使得开发者不需要直接操作DOM。
    • 通过声明式的方式编写UI,开发者可以更专注于业务逻辑而不是DOM细节。
  5. 跨平台一致性
    • 小程序需要在不同的平台上保持一致的行为和表现,使用统一的组件和API可以更好地实现这一点。
    • 直接操作DOM可能会因为不同平台的渲染差异而导致不一致的问题。
  6. 限制和能力
    • 小程序的能力是受限的,它只提供了必要的API来满足常见的开发需求。
    • 这种限制有助于保持小程序的轻量和快速启动。 总之,小程序的设计哲学与传统的Web开发有所不同,它更强调简单、安全和高效。因此,小程序中不提供DOM相关的API,而是提供了自己的组件系统、事件系统和数据绑定机制来满足开发需求。

29. Promise then 第二个参数和 Promise.catch 的区别是什么?

Promise.prototype.then() 方法和 Promise.prototype.catch() 方法都是用于处理 Promise 对象的决议(fulfillment 或 rejection)的回调函数。它们之间的主要区别在于它们被调用的条件和它们的行为:

  1. 参数位置和用途
    • Promise.prototype.then(onFulfilled, onRejected) 方法接受两个参数:第一个参数 onFulfilled 是当 Promise 成功解决(fulfilled)时调用的函数;第二个参数 onRejected 是当 Promise 被拒绝(rejected)时调用的函数。
    • Promise.prototype.catch(onRejected) 方法实际上是 then(null, onRejected) 的语法糖,它只接受一个参数,即当 Promise 被拒绝时调用的函数。
  2. 错误处理
    • then 方法中,如果你提供了第二个参数(onRejected),它将专门用于处理前面 Promise 的拒绝情况。
    • catch 方法则是专门用于捕获前面 Promise 链中的任何拒绝情况。它会在 Promise 链中传播错误,直到被某个 catch 处理器捕获。
  3. 链式调用
    • 使用 then 方法时,你可以链式调用多个 then,每个 then 都可以处理成功或失败的情况。
    • 使用 catch 方法时,它通常放在链式的末尾,用于捕获前面任何 then 中发生的错误。
  4. 返回值
    • then 方法无论成功或失败的回调函数都会返回一个新的 Promise,允许你继续链式调用。
    • catch 方法同样返回一个新的 Promise,允许你在捕获错误后继续链式调用。
  5. 语义和可读性
    • 使用 then 的第二个参数处理错误时,代码的意图可能不如使用 catch 方法清晰。catch 方法更直观地表明了错误处理的意图。 示例代码:
// 使用 then 的第二个参数处理错误
promise.then(
  value => { /* 处理成功 */ },
  error => { /* 处理错误 */ }
);
// 使用 catch 处理错误
promise.then(value => { /* 处理成功 */ }).catch(error => { /* 处理错误 */ });

在实际使用中,通常建议使用 catch 方法来处理错误,因为它更符合错误处理的语义,并且可以更方便地捕获链式中上游传来的错误。而 then 的第二个参数则较少使用,除非在特定场景下需要同时处理成功和失败的情况。

30. WebWorker、SharedWorker 和 ServiceWorker 有哪些区别?

WebWorker、SharedWorker 和 ServiceWorker 都是 Web APIs,它们允许你在后台线程中运行 JavaScript 代码,从而不会阻塞主线程。它们之间的主要区别在于它们的设计目的、作用域和功能:

WebWorker(也称为 DedicatedWorker)

  • 设计目的:用于在后台执行耗时的任务,如数据处理、复杂计算等,而不影响主线程的响应性。
  • 作用域:每个 WebWorker 是专属于创建它的页面或标签的。不同页面或标签的 WebWorker 之间是隔离的。
  • 通信:通过 postMessageonmessage 进行通信。
  • 生命周期:与创建它的页面或标签相关联,当页面关闭时,WebWorker 也会被终止。
  • 访问限制:不能直接访问 DOM,但可以执行 XHR 请求、使用 IndexedDB 等。

SharedWorker

  • 设计目的:允许多个页面或标签共享同一个 worker 实例,以便在不同上下文之间共享数据。
  • 作用域:SharedWorker 可以被多个页面或标签共享。不同页面可以通过同一个 SharedWorker 进行通信。
  • 通信:通过 postMessageonmessage 进行通信,但需要通过 SharedWorker.port 来连接。
  • 生命周期:独立于创建它的页面或标签,只有当所有连接它的页面都关闭时,SharedWorker 才会被终止。
  • 访问限制:与 WebWorker 类似,不能直接访问 DOM,但可以执行网络请求、使用 IndexedDB 等。

ServiceWorker

  • 设计目的:主要用于创建离线应用程序、缓存资源、拦截网络请求、发送推送通知等。它是 PWA(Progressive Web Apps)的关键技术之一。
  • 作用域:ServiceWorker 是一种特殊的 Worker,它的作用域是整个域名或特定的路径,不限于单个页面或标签。
  • 通信:通过 postMessageonmessage 进行通信,但通常用于与缓存和网络请求相关的操作。
  • 生命周期:独立于页面,可以在页面关闭后继续运行,用于处理后台同步、推送通知等。
  • 访问限制:不能直接访问 DOM,但可以拦截网络请求、管理缓存、发送推送通知等。

总结

  • WebWorker:适用于单个页面的后台任务,不与其他页面共享。
  • SharedWorker:适用于多个页面共享的后台任务,可以在不同页面之间通信。
  • ServiceWorker:适用于创建离线应用、缓存资源、拦截请求等,具有更广泛的作用域和生命周期。 每种 Worker 都有其特定的使用场景和优势,选择哪种 Worker 取决于你的具体需求。

31. React中,useRef、ref、forwardsRef 的区别是什么?

在 React 中,useRefrefforwardRef 都是用于处理引用(reference)的机制,但它们各自有不同的用途和场景:

useRef

  • 用途useRef 是一个 React Hook,用于在组件的整个生命周期内持续存储一个可变的值,这个值在组件的重新渲染之间保持不变。
  • 创建:通过调用 useRef(initialValue) 创建,返回一个可变的 ref 对象,其 .current 属性被初始化为传入的 initialValue
  • 场景:通常用于存储 DOM 元素的引用,也可以用于存储任何可变的值,比如定时器的 ID 或者某个对象的实例。
  • 特性:修改 ref 对象的 .current 属性不会引发组件的重新渲染。

ref

  • 用途ref 是一个属性,可以附加到任何 React 组件或 DOM 元素上,用于获取对组件实例或 DOM 节点的直接引用。
  • 创建:可以通过 useRef 创建,或者直接使用一个带有 current 属性的对象。
  • 场景:用于直接操作 DOM,例如聚焦输入框、测量元素尺寸等,或者获取类组件的实例。
  • 特性:在函数组件中,通常与 useRef 结合使用;在类组件中,可以通过在构造函数中创建 ref 并将其附加到元素上。

forwardRef

  • 用途forwardRef 是一个高阶组件,用于在函数组件中转发 ref 到子组件。
  • 创建:通过 React.forwardRef((props, ref) => {}) 创建,接收一个函数,该函数接收 propsref 作为参数。
  • 场景:当需要将 ref 从父组件传递到子组件时使用,特别是在使用函数组件时,因为函数组件本身不提供类似类组件的 this 上下文。
  • 特性:允许父组件通过 ref 访问到子组件的 DOM 节点或实例,即使子组件是函数组件。

总结

  • useRef:用于在函数组件中创建和存储引用,常用于 DOM 操作或存储可变值。
  • ref:用于附加到组件或 DOM 元素上,以获取对它们的直接引用。
  • forwardRef:用于在函数组件中转发 ref 到子组件,使得父组件可以访问子组件的引用。 这些机制在 React 中各有其独特的作用和用途,理解它们的区别和正确使用它们是构建高效 React 应用的重要组成部分。

32. useEffect的第二个参数,是如何判断实现依赖是否发生变化的?

useEffect 的第二个参数是一个依赖数组,它用于告诉 React 在哪些状态或属性变化时应该重新执行这个副作用函数。React 通过以下方式来判断依赖是否发生变化:

  1. 浅比较:React 使用浅比较(shal...依赖项的引用是否发生变化。如果依赖项是对象或数组,即使它们的内容没有变化,只要引用变了,也会被认为是变化。

示例

useEffect(() => {
  // 副作用代码
}, [dependency1, dependency2]);

在这个示例中,当 dependency1dependency2 发生变化时,副作用函数会重新执行。如果这两个依赖项都是基本类型(如数字、字符串),则直接比较它们的值。如果依赖项是对象或数组,则比较它们的引用。

注意事项

  • 空数组:如果依赖数组为空([]),则副作用函数只在组件挂载时执行一次,并在卸载时执行清理逻辑(如果有的话)。
  • 省略依赖数组:如果不提供第二个参数,则副作用函数会在每次组件渲染后执行。
  • 避免遗漏依赖:确保依赖数组中包含了所有外部作用域中使用的变量,否则可能导致副作用函数的行为不符合预期。 理解 useEffect 的依赖判断机制对于正确使用 Hooks 和避免常见的陷阱(如无限循环渲染)非常重要。

33. HTTP协议的不同版本的主要特点有哪些?

HTTP协议的不同版本主要特点如下:

HTTP/0.9

  • 特点:非常简单,只支持GET请求,没有HTTP头,服务器只能返回HTML格式的文档。
  • 状态:已过时,仅作为历史参考。

HTTP/1.0

  • 特点
    • 引入了HTTP头,允许传输更多类型的数据(如图片、视频等)。
    • 增加了POST、HEAD等请求方法。
    • 开始支持状态码,用于表示请求结果。
    • 每个连接只能发送一个请求,发送完请求后需要关闭连接。

HTTP/1.1

  • 特点
    • 引入了持久连接(Persistent Connections),允许在一个TCP连接上发送多个请求,减少了建立和关闭连接的开销。
    • 增加了缓存控制、带宽优化等机制。
    • 支持虚拟主机,可以通过主机名来区分不同的网站。
    • 引入了范围请求(Range Requests),允许从服务器请求资源的某个部分。
    • 增加了对长连接的支持,通过Connection: keep-alive头实现。

HTTP/2

  • 特点
    • 采用二进制分帧层,将数据分割为更小的帧,提高了传输效率。
    • 支持多路复用(Multiplexing),允许在单个连接上同时发送多个请求或响应,解决了HTTP/1.1中的队头阻塞问题。
    • 引入了头部压缩机制,减少了传输的数据量。
    • 支持服务器推送(Server Push),允许服务器在客户端请求之前主动发送资源。
    • 提高了安全性,强制要求使用TLS加密。

HTTP/3

  • 特点
    • 基于QUIC协议,使用UDP替代TCP作为传输层协议,减少了连接建立的时间。
    • 继承了HTTP/2的多路复用和头部压缩等特性。
    • 提供了更好的拥塞控制和流量控制机制。
    • 支持连接迁移(Connection Migration),允许在移动网络环境下保持连接不中断。 每个版本的HTTP协议都在前一个版本的基础上进行了改进和优化,以提高网络传输的效率、安全性和可靠性。随着技术的发展,未来还可能出现新的HTTP版本,带来更多的创新和改进。

34. http1.1 的持久连接和 http2 的多路复用有什么区别?

HTTP/1.1的持久连接和HTTP/2的多路复用都是用于提高网络传输效率的技术,但它们在实现方式和效果上有所区别:

HTTP/1.1 持久连接(Persistent Connections)

  • 实现方式:通过Connection: keep-alive头部实现,允许在一个TCP连接上发送多个请求和响应,而不是每次请求后都关闭连接。
  • 限制:虽然允许在一个连接上发送多个请求,但这些请求仍然是顺序处理的。如果前面的请求没有完成,后面的请求就会阻塞,这就是所谓的“队头阻塞”(Head-of-Line Blocking)问题。
  • 优点:减少了建立和关闭连接的开销,提高了连接的利用率。

HTTP/2 多路复用(Multiplexing)

  • 实现方式:HTTP/2引入了二进制分帧层,将数据分割为更小的帧,并在单个TCP连接上通过流(Stream)来传输这些帧。每个流都可以独立地发送请求和响应,互不干扰。
  • 优点:解决了HTTP/1.1中的队头阻塞问题,因为多个流可以并行传输,不会因为某个流的延迟而影响其他流。这大大提高了并发性能和传输效率。
  • 额外特性:HTTP/2还支持头部压缩、服务器推送等特性,进一步优化了网络传输。

区别总结

  • 并发性:HTTP/2的多路复用提供了真正的并发性,而HTTP/1.1的持久连接虽然减少了连接开销,但仍然存在队头阻塞问题。
  • 传输效率:由于HTTP/2的帧结构和支持的额外特性,其在传输效率上通常优于HTTP/1.1。
  • 复杂性:HTTP/2的实现比HTTP/1.1复杂,需要更多的资源来解析帧和流,但现代浏览器和服务器已经很好地支持了这些特性。 总的来说,HTTP/2的多路复用是对HTTP/1.1持久连接的改进,提供了更高的性能和更有效的网络资源利用。

35. http3 中的 QUIC 是什么协议?

QUIC(Quick UDP Internet Connections) 是一种基于UDP的传输层协议,由Google开发,旨在提供比TCP更快的网络连接。QUIC是HTTP/3的基础,它解决了TCP的一些长期存在的问题,并提供了额外的性能和安全性优势。

QUIC的主要特点和优势:

  1. 减少延迟
    • 0-RTT连接:如果客户端之前与服务器建立过连接,QUIC可以复用之前的连接信息,实现0-RTT(零往返时间)连接,即不需要额外的握手即可开始传输数据。
    • 快速握手:即使在没有0-RTT的情况下,QUIC的握手过程也比TCP快,因为它将加密和传输层握手合并在一起。
  2. 多路复用
    • 类似于HTTP/2,QUIC支持多路复用,允许在一个连接上同时发送多个流的数据,而不会出现队头阻塞问题。
  3. 连接迁移
    • QUIC的连接不受IP地址和端口的限制,即使客户端的IP地址或端口发生变化(例如,从WiFi切换到蜂窝网络),连接也可以保持不变。
  4. 前向加密
    • QUIC内置了加密功能,所有传输的数据都是加密的,提供了比TCP更高的安全性。
  5. 更快的错误恢复
    • QUIC使用更先进的拥塞控制和错误恢复机制,可以更快地恢复从丢包中恢复。
  6. 减少握手消息
    • QUIC减少了握手所需的往返次数,从而减少了延迟。

QUIC的工作原理:

  • UDP基础:QUIC建立在UDP之上,利用UDP的简单性和灵活性来避开TCP的一些限制。
  • 二进制帧格式:QUIC使用二进制帧来传输数据,这些帧可以包含各种类型的信息,如握手、数据、控制消息等。
  • 流和连接:QUIC中的流是独立的,可以在同一个连接上并行传输,而不会相互干扰。
  • 加密和认证:QUIC使用TLS 1.3进行加密和认证,确保数据的安全传输。

HTTP/3与QUIC的关系:

HTTP/3是HTTP协议的第三个主要版本,它使用QUIC作为其传输层协议。通过利用QUIC的优势,HTTP/3旨在提供比HTTP/2更快的网页加载速度和更稳定的连接。 总之,QUIC是一种创新的传输层协议,为现代网络应用提供了更高的性能、更低的延迟和更强的安全性。随着HTTP/3的逐渐普及,QUIC有望成为未来网络通信的重要基础。

36. HTTP/3 是基于 UDP 的协议, 那么他是如何保障安全性的?

HTTP/3是基于QUIC协议的,而QUIC协议在设计之初就内置了安全性特性。以下是HTTP/3如何保障安全性的几个关键点:

  1. 强制加密
    • QUIC协议要求所有连接都必须加密,没有非加密的选项。这意味着即使没有使用HTTPS,QUIC也会对数据进行加密传输。
  2. 使用TLS 1.3
    • QUIC使用TLS 1.3作为其加密和认证机制。TLS 1.3是当前最安全的传输层安全协议版本,它提供了强大的加密算法、改进的握手过程和更好的性能。
  3. 简化握手
    • TLS 1.3的握手过程比以前的版本更简单、更快,减少了潜在的攻击面。QUIC的握手也是为了减少延迟而设计的,但它仍然提供了强大的安全性。
  4. 连接迁移时的安全性
    • 即使在连接迁移(如客户端的IP地址或端口变化)时,QUIC也能保持连接的安全性和完整性。这是通过使用连接ID来实现的,连接ID在连接的生命周期内保持不变,与底层网络路径的变化无关。
  5. 防重放攻击
    • QUIC使用了序列号和nonce来防止重放攻击,确保每个加密的数据包都是唯一的,不能被恶意捕获和重放。
  6. 保护协议的版本协商
    • QUIC的版本协商过程也是加密的,这防止了降级攻击,即攻击者无法强制客户端和服务器使用较旧、可能不安全的协议版本。
  7. 加密的帧结构
    • QUIC的帧结构也是加密的,这增加了攻击者解析和理解通信内容的难度。
  8. 安全的拥塞控制
    • QUIC的拥塞控制机制也是设计为安全的,以防止诸如放大攻击等基于拥塞控制的攻击。 通过这些措施,HTTP/3在提供高性能的同时,也确保了数据传输的安全性。这种内置的安全特性使得HTTP/3成为了一个既快速又安全的网络协议。

37. 前端如何防止加载外域脚本?

前端防止加载外域脚本主要是为了保障网站的安全,避免跨站脚本攻击(XSS)和数据泄露等安全问题。以下是一些常用的方法来防止加载外域脚本:

  1. CSP(内容安全策略)
    • 通过设置HTTP响应头Content-Security-Policy,可以指定允许加载资源的域名,从而阻止非法的外域脚本加载。
    • 例如:Content-Security-Policy: script-src 'self' https://trusteddomain.com; 这表示只允许加载当前域名和指定域名的脚本。
  2. 验证脚本来源
    • 在引入外部脚本之前,验证脚本的URL是否属于允许的域名列表。
  3. 使用SRI(子资源完整性)
    • 通过在<script>标签中使用integrity属性,可以确保加载的脚本在客户端进行哈希校验,只有校验通过的脚本才会被执行。
    • 例如:<script src="https://example.com/script.js" integrity="sha384-Base64EncodedHash" crossorigin="anonymous"></script>
  4. 避免 Inline JavaScript
    • 尽量避免在HTML中直接写入JavaScript代码,因为这样可以减少XSS攻击的风险。
  5. 使用HTTPS
    • 使用HTTPS可以减少中间人攻击的风险,确保脚本在传输过程中不被篡改。
  6. 设置跨域策略
    • 对于那些需要与外部域进行交互的API,可以设置跨域资源共享(CORS)策略,只允许特定的域名进行跨域请求。
  7. 过滤用户输入
    • 对用户输入进行严格的过滤和消毒,防止恶意脚本通过用户输入注入到页面中。
  8. 使用Web应用防火墙(WAF)
    • WAF可以帮助检测和阻止恶意脚本的加载。
  9. 避免使用不受信任的CDN
    • 如果使用CDN加载资源,确保CDN是可信任的,并且有良好的安全记录。
  10. 定期更新和审计依赖库
    • 定期更新前端依赖库,并对依赖库进行安全审计,确保没有已知的安全漏洞。 通过结合这些方法,可以有效地防止前端加载外域脚本,提高网站的安全性。

38. 前端单页应用 History 路由模式, 需要如何配置 nginx?

前端单页应用(SPA)使用History路由模式时,所有的路由都会指向同一个HTML文件,这需要在服务器端进行相应的配置,以便正确地处理路由请求。以下是配置Nginx以支持History路由模式的步骤:

  1. 确保Nginx版本支持try_files指令
    • try_files指令是Nginx处理文件存在与否的关键指令,用于支持History路由。
  2. 配置server块
    • 在Nginx配置文件中的server块内,设置根目录和默认的index文件。
  3. 使用try_files指令
    • try_files指令尝试按顺序检查文件是否存在,如果都不存在,则返回最后一个参数指定的URI。
  4. 配置rewrite规则
    • 如果文件不存在,使用rewrite规则将请求重写回根目录的index文件。 以下是一个基本的Nginx配置示例:
server {
    listen 80;
    server_name yourdomain.com; # 替换为你的域名
    root /path/to/your/project; # 替换为你的项目根目录
    index index.html;
    location / {
        try_files $uri $uri/ /index.html;
    }
    # 如果有API代理需求,可以添加如下location块
    location /api {
        proxy_pass http://backend_upstream; # 替换为你的后端服务地址
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

解释

  • listen 80;:监听80端口,如果是HTTPS则监听443端口。
  • server_name yourdomain.com;:替换为你的域名。
  • root /path/to/your/project;:替换为你的项目根目录。
  • index index.html;:默认的索引文件。
  • try_files $uri $uri/ /index.html;:尝试查找请求的文件,如果找不到,则返回根目录的index.html文件。
  • location /api { ... }:如果需要代理API请求到后端服务,可以添加这个块,并替换为你的后端服务地址。 注意事项
  • 确保Nginx配置文件语法正确,可以使用nginx -t命令测试配置文件是否有语法错误。
  • 配置完成后,需要重新加载Nginx配置,使用命令nginx -s reload
  • 如果使用HTTPS,需要配置SSL证书相关设置。 通过这样的配置,当用户访问任何路由时,Nginx都会将请求指向同一个index.html文件,由前端路由库(如React Router、Vue Router等)来处理具体的路由逻辑。

39. 前端如何实现截图?

前端实现截图可以通过不同的技术手段,根据具体需求和浏览器支持情况选择合适的方法。以下是几种常见的前端截图实现方式:

1. 使用HTML5的Canvas元素

通过Canvas可以绘制页面上的任意内容,并将其转换为图片。这种方法适用于捕获整个页面或部分区域的截图。

// 截图整个页面
function captureFullPage() {
  const body = document.body;
  const html = document.documentElement;
  const width = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth);
  const height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = 'white';
  ctx.fillRect(0, 0, width, height);
  const data = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
    <foreignObject width="100%" height="100%">
      ${document.documentElement.outerHTML}
    </foreignObject>
  </svg>`;
  const DOMURL = window.URL || window.webkitURL || window;
  const img = new Image();
  const svg = new Blob([data], { type: 'image/svg+xml' });
  const url = DOMURL.createObjectURL(svg);
  img.onload = () => {
    ctx.drawImage(img, 0, 0);
    DOMURL.revokeObjectURL(url);
    const imgURI = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream');
    // 触发下载或进行其他操作
  };
  img.src = url;
}
// 截图特定元素
function captureElement(element) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  canvas.width = element.offsetWidth;
  canvas.height = element.offsetHeight;
  ctx.fillStyle = 'white';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  const data = `<svg xmlns="http://www.w3.org/2000/svg" width="${canvas.width}" height="${canvas.height}">
    <foreignObject width="100%" height="100%">
      ${element.outerHTML}
    </foreignObject>
  </svg>`;
  const DOMURL = window.URL || window.webkitURL || window;
  const img = new Image();
  const svg = new Blob([data], { type: 'image/svg+xml' });
  const url = DOMURL.createObjectURL(svg);
  img.onload = () => {
    ctx.drawImage(img, 0, 0);
    DOMURL.revokeObjectURL(url);
    const imgURI = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream');
    // 触发下载或进行其他操作
  };
  img.src = url;
}

2. 使用浏览器提供的API

现代浏览器提供了window.screen.capture等API,可以直接捕获屏幕内容。

async function captureScreen() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({ preferCurrentTab: true });
    const video = document.createElement('video');
    video.srcObject = stream;
    video.play();
    return new Promise((resolve) => {
      video.onloadedmetadata = () => {
        const canvas = document.createElement('canvas');
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
        stream.getVideoTracks()[0].stop(); // 停止视频流
        const imgURI = canvas.toDataURL('image/png');
        resolve(imgURI);
      };
    });
  } catch (err) {
    console.error('Error: ' + err);
  }
}

3. 使用第三方库

有一些JavaScript库可以简化截图过程,例如html2canvas

// 使用html2canvas库
import html2canvas from 'html2canvas';
function captureWithHtml2canvas(element) {
  html2canvas(element).then(canvas => {
    const imgURI = canvas.toDataURL('image/png');
    // 触发下载或进行其他操作
  });
}

4. 使用浏览器扩展

如果需要更复杂的截图功能,可以考虑使用浏览器扩展,如Chrome的截图扩展。

注意事项:

  • 跨域问题:在捕获跨域内容时,可能会遇到安全限制。
  • 性能问题:截图大页面或复杂页面时,可能会影响性能。
  • 兼容性:不同的方法和API在不同浏览器上的支持情况不同。 根据具体需求选择合适的方法,并在实际应用中进行测试和优化。

40. js 中对于超过 Number 最大值的数怎么处理?

在JavaScript中,数值类型有一个最大安全值,即Number.MAX_SAFE_INTEGER,其值为2^53 - 1(即9007199254740991)。超过这个值的整数可能会失去精度,因为JavaScript使用IEEE 754双精度浮点数来表示所有数字,这限制了它能精确表示的整数的范围。 对于超过Number.MAX_SAFE_INTEGER的数,你可以采取以下几种处理方式:

1. 使用BigInt

BigInt是ES2020引入的一种新的数据类型,可以表示任意大小的整数。使用BigInt,你可以进行大数的精确计算。

const bigNumber = BigInt("9007199254740993"); // 字符串形式
const anotherBigNumber = 9007199254740993n; // 后缀n形式
const sum = bigNumber + anotherBigNumber;
console.log(sum); // 输出:18014398509481986n

2. 使用第三方库

有一些第三方库,如bignumber.jsbig.js,可以处理大数运算。

// 使用bignumber.js
const BigNumber = require('bignumber.js');
let bigNumber = new BigNumber("9007199254740993");
let anotherBigNumber = new BigNumber("9007199254740993");
let sum = bigNumber.plus(anotherBigNumber);
console.log(sum.toString()); // 输出:18014398509481986

3. 字符串处理

如果只是需要显示大数而不进行复杂运算,可以使用字符串来表示和处理。

let bigNumberStr = "9007199254740993";
let anotherBigNumberStr = "9007199254740993";
let sumStr = BigInt(bigNumberStr) + BigInt(anotherBigNumberStr);
console.log(sumStr.toString()); // 输出:18014398509481986

4. 分割处理

对于某些算法,可以将大数分割成小块进行处理,最后再合并结果。

注意事项:

  • 兼容性BigInt是较新的特性,旧版浏览器可能不支持。第三方库也需要考虑兼容性和性能问题。
  • 运算性能:大数运算通常比原生数字运算要慢,尤其是在使用第三方库时。
  • 类型转换BigInt和常规数字类型不兼容,不能直接混合运算,需要显式转换。 根据具体需求和场景,选择合适的处理方式。对于大多数现代前端应用,推荐使用BigInt来处理大数问题。

41. 介绍下深度优先遍历和广度优先遍历,如何实现?

当然,下面是用JavaScript实现的深度优先遍历(DFS)和广度优先遍历(BFS)的示例。

深度优先遍历(DFS)

递归实现

function dfs(graph, start, visited = new Set()) {
    visited.add(start);
    console.log(start); // 访问节点
    for (let neighbor of graph[start]) {
        if (!visited.has(neighbor)) {
            dfs(graph, neighbor, visited);
        }
    }
    return visited;
}
// 示例图的邻接表表示
const graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
};
console.log("DFS:");
dfs(graph, 'A');

非递归实现

function dfsIterative(graph, start) {
    let stack = [start];
    let visited = new Set();
    while (stack.length > 0) {
        let current = stack.pop();
        if (!visited.has(current)) {
            visited.add(current);
            console.log(current); // 访问节点
            stack.push(...graph[current].reverse()); // 添加邻接节点到栈中
        }
    }
    return visited;
}
console.log("DFS (Iterative):");
dfsIterative(graph, 'A');

广度优先遍历(BFS)

function bfs(graph, start) {
    let queue = [start];
    let visited = new Set();
    while (queue.length > 0) {
        let current = queue.shift();
        if (!visited.has(current)) {
            visited.add(current);
            console.log(current); // 访问节点
            queue.push(...graph[current]); // 添加邻接节点到队列中
        }
    }
    return visited;
}
console.log("BFS:");
bfs(graph, 'A');

在这两个示例中,graph是一个对象,表示图的邻接表。dfsdfsIterativebfs函数分别实现了深度优先遍历的递归版本、非递归版本和广度优先遍历,并在控制台输出了遍历的顶点。 这些函数都可以用来遍历图中的所有顶点,确保每个顶点只被访问一次。在实际应用中,可以根据需要修改这些函数,例如,可以添加回调函数来处理每个访问的节点,或者收集遍历的节点而不是直接打印它们。

42. Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

Promise构造函数是同步执行的,而then方法则是异步执行的。

Promise构造函数

当你创建一个新的Promise对象时,传递给Promise构造函数的执行器函数(executor)会立即执行。这个执行器函数通常包含异步操作的代码,但它本身是同步调用的。例如:

new Promise((resolve, reject) => {
    console.log("Promise构造函数内的代码"); // 这行代码会同步执行
    // 异步操作...
});
console.log("Promise构造函数外的代码");

输出顺序将是:

Promise构造函数内的代码
Promise构造函数外的代码

这表明Promise构造函数内的代码是同步执行的。

then方法

then方法用于指定Promise对象状态改变时的回调函数。then方法本身是同步调用的,但它的回调函数是异步执行的。也就是说,then方法会返回一个新Promise对象,并在当前事件循环的末尾(即异步)执行其回调函数。

new Promise((resolve, reject) => {
    console.log("Promise构造函数内的代码"); // 同步执行
    resolve();
}).then(() => {
    console.log("then方法的回调函数"); // 异步执行
});
console.log("then方法外的代码"); // 同步执行

输出顺序将是:

Promise构造函数内的代码
then方法外的代码
then方法的回调函数

这表明then方法的回调函数是在当前事件循环的末尾异步执行的。

总结

  • Promise构造函数:同步执行,其内部的执行器函数会立即调用。
  • then方法:本身同步调用,但其回调函数是异步执行的,会在当前事件循环的末尾被调用。 这种设计允许Promise链中的代码顺序执行,同时不会阻塞主线程,从而实现高效的异步编程。

43. Redux 和 Vuex 的设计思想是什么?

Redux和Vuex都是用于状态管理的库,它们分别用于React和Vue框架,但它们的设计思想有相似之处,也有各自的特点。

Redux的设计思想

  1. 单一状态树
    • Redux应用中只有一个单一的状态树,所有的状态都存储在一个大的对象中。这有助于调试和追踪状态变化。
  2. 状态是只读的
    • 不能直接修改状态,只能通过触发动作(Actions)来描述如何改变状态。
  3. 使用纯函数执行状态修改
    • Reducers是纯函数,用于接收当前状态和动作,然后返回新的状态。它们不应该有副作用,如API调用或路由跳转。
  4. 单向数据流
    • Redux遵循严格的单向数据流:Action -> Reducer -> State。这种流程使得状态变化可预测和追踪。
  5. 中间件(Middleware)
    • Redux通过中间件来处理异步操作、日志记录、错误处理等。中间件可以在动作被派发到存储之前进行拦截和处理。

Vuex的设计思想

  1. 集中存储管理
    • Vuex也是集中存储管理应用的所有组件的状态,类似于Redux的单一状态树。
  2. 状态改变可追踪
    • Vuex通过提交突变(Mutations)来更改状态,这些突变必须是同步函数,确保状态的改变可追踪。
  3. Actions处理异步
    • 与Redux类似,Vuex中的Actions用于处理异步操作,它们可以提交突变来改变状态。
  4. Getters计算属性
    • Vuex提供了Getters,类似于Vue组件的计算属性,用于根据状态派生出一些状态。
  5. 模块化
    • Vuex支持将状态管理分割成模块,每个模块拥有自己的状态、突变、动作和getters,便于大型应用的状态管理。
  6. 严格的状态变更规则
    • Vuex强制要求通过突变来更改状态,不允许直接修改状态,以保证状态的变更可预测。

共同点

  • 集中管理:两者都提倡将应用的状态集中管理,避免状态分散导致的难以维护。
  • 可预测性:都通过严格的规则来确保状态变更的可预测性。
  • 异步处理:都提供了处理异步操作的机制(Redux的中间件和Vuex的Actions)。

区别

  • 集成度:Vuex更紧密地集成到Vue框架中,而Redux是一个独立的库,可以用于任何JavaScript应用。
  • API设计:Vuex的API更贴近Vue的使用习惯,如使用计算属性和组件的集成;而Redux的API更函数式,强调纯函数和不可变性。
  • 模块化:Vuex内置了模块化的支持,而Redux通常需要额外的库(如redux@Module)来实现模块化。 总的来说,Redux和Vuex都旨在解决大型应用中的状态管理问题,它们的设计思想都强调了状态的集中管理、可预测性和可维护性。不过,它们在具体实现和API设计上有所不同,以适应不同框架和开发者的需求。

44. 全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?

在ES6及更高版本的JavaScript中,constlet声明的变量具有块级作用域(block scope),而不是像var那样具有函数级作用域。这意味着它们不会像var声明的变量那样被自动添加到全局对象(在浏览器中是window对象)上。

变量的存储位置

constlet声明的变量存储在它们所在的作用域的环境中。在全局作用域中,它们存储在全局环境的一个特殊区域,这个区域与window对象是分离的。这个特殊区域通常被称为“词法环境”(Lexical Environment)或“变量环境”(Variable Environment)。

如何获取这些变量

虽然constlet声明的变量不在window对象上,但它们仍然在全局作用域中可用。你可以在全局作用域的任何地方直接通过它们的名称来访问它们,不需要通过window对象。 例如:

const myConst = 'I am a constant';
let myLet = 'I am a let variable';
console.log(myConst); // 输出: I am a constant
console.log(myLet);   // 输出: I am a let variable

如果你确实需要通过某种方式来访问这些变量,你可以使用以下方法:

  1. 直接访问
    • 在全局作用域中,直接使用变量名即可。
  2. 使用this(非严格模式)
    • 在非严格模式下,this在全局作用域中指向window对象,所以你可以使用this来访问constlet声明的变量。
    console.log(this.myConst); // 非严格模式下可能有效
    console.log(this.myLet);   // 非严格模式下可能有效
    
    但请注意,这种做法并不推荐,因为它依赖于JavaScript的严格模式行为,而且在严格模式下会失败。
  3. 使用函数封装
    • 你可以创建一个函数,然后在函数内部返回这些变量。
    function getGlobalVariables() {
      return {
        myConst,
        myLet
      };
    }
    const globalVars = getGlobalVariables();
    console.log(globalVars.myConst); // 输出: I am a constant
    console.log(globalVars.myLet);   // 输出: I am a let variable
    
  4. 使用Object.getOwnPropertyDescriptorObject.getOwnPropertyNames
    • 这些方法可以用来检查全局对象的属性描述符和属性名称,但它们不会直接显示constlet声明的变量,因为这些变量不是全局对象的属性。

注意事项

  • 在严格模式下,this不会指向window,因此不能使用this来访问constlet声明的变量。
  • constlet声明的变量不会被提升(hoisted)到作用域的顶部,而是在它们的声明位置处“暂时性死区”(Temporal Dead Zone)之后才可用。
  • 为了避免潜在的问题,最好始终使用constlet而不是var来声明变量,以利用块级作用域和避免意外修改全局对象。 总之,constlet声明的变量存在于它们所在的作用域的环境中,你可以在作用域内部直接通过变量名来访问它们,而不需要通过window对象。

45. 浏览器缓存中 Memory Cache 和 Disk Cache,有什么区别?

浏览器缓存是提升网页加载速度的重要机制,其中Memory Cache(内存缓存)和Disk Cache(磁盘缓存)是两种主要的缓存类型。它们在存储位置、速度、容量和生命周期等方面有所区别:

Memory Cache(内存缓存)

存储位置

  • 存储在计算机的内存中。 速度
  • 访问速度非常快,因为内存的读写速度远高于磁盘。 容量
  • 相对较小,受到浏览器分配给缓存使用的内存限制。 生命周期
  • 通常在浏览器标签页关闭后就会被清除。
  • 是一种短期缓存,用于存储最近访问过的资源。 适用场景
  • 适用于存储频繁访问的小型资源,如图片、脚本等。
  • 对于频繁变动的资源,如动态生成的页面,也可能被存储在内存缓存中,但生命周期较短。 特点
  • 由于内存的限制,缓存的数据可能随时被替换。
  • 适用于快速访问,但不适合长期存储。

Disk Cache(磁盘缓存)

存储位置

  • 存储在计算机的硬盘上。 速度
  • 访问速度较内存缓存慢,但仍然比从网络加载资源要快。 容量
  • 相对较大,可以根据硬盘的可用空间和浏览器的设置来存储更多的数据。 生命周期
  • 可以持久存储,即使在浏览器关闭后也不会立即清除。
  • 是一种长期缓存,用于存储不经常变动的资源。 适用场景
  • 适用于存储大型文件或不经常变动的资源,如视频、音频、大型脚本文件等。
  • 也用于存储那些不希望每次都从网络加载的资源。 特点
  • 由于存储在磁盘上,缓存的数据更持久,不易被替换。
  • 适用于长期存储,但访问速度不如内存缓存。

区别总结

  • 存储介质:Memory Cache使用内存,Disk Cache使用硬盘。
  • 访问速度:Memory Cache更快,Disk Cache相对较慢。
  • 容量大小:Memory Cache容量较小,Disk Cache容量较大。
  • 生命周期:Memory Cache短期存在,Disk Cache长期存在。
  • 适用资源:Memory Cache适用于小型、频繁访问的资源,Disk Cache适用于大型、不经常变动的资源。 浏览器在加载资源时,会根据资源的类型、大小、访问频率等因素自动选择使用哪种缓存。开发者可以通过设置HTTP头部字段(如Cache-Control)来控制资源的缓存行为。

46. [3, 15, 8, 29, 102, 22].sort(),结果是多少,为什么?

在JavaScript中,数组有一个.sort()方法,它可以用来对数组元素进行排序。但是,.sort()方法在默认情况下会将数组元素转换为字符串,并按照字符串Unicode码点进行排序。这意味着数字会按照它们的字符串表示来排序,这可能会导致一些非预期的结果。 对于给定的数组[3, 15, 8, 29, 102, 22],如果直接调用.sort()而不提供比较函数,排序结果可能会出乎意料。为了得到正确的数值排序,应该提供一个比较函数,例如(a, b) => a - b。 如果我们直接使用[3, 15, 8, 29, 102, 22].sort(),结果可能不是按照数值大小排序的。为了确切知道结果,我们可以执行这个操作并查看输出。

使用JavaScript的默认.sort()方法对数组[3, 15, 8, 29, 102, 22]进行排序后,结果为[102, 15, 22, 29, 3, 8]。这个结果是因为数字被转换成了字符串,然后按照字符串的Unicode码点进行比较。例如,'102'作为字符串排在'15'前面,因为'1'的Unicode码点小于'15'的第一个字符'1'。 为了按照数值大小进行排序,应该在.sort()方法中提供一个比较函数,如[3, 15, 8, 29, 102, 22].sort((a, b) => a - b)。这样,数组就会按照数值的正确顺序排序。

47. https 的证书验证过程是什么样的

HTTPS证书验证过程是确保网络通信安全的关键步骤,具体包括以下几步:

1. 请求数字证书

当浏览器尝试与服务器建立HTTPS连接时,会首先向服务器请求数字证书。服务器在收到请求后,会将自身的数字证书以及为服务器数字证书签名的CA机构的数字证书一同发送给浏览器。

2. 验证数字证书的有效期

浏览器在收到数字证书后,首先会验证证书的有效期。数字证书中包含了证书的有效期信息,浏览器只需判断当前时间是否在有效期内即可。如果证书已过期,浏览器将显示警告信息,并阻止用户继续访问该站点。

3. 验证数字证书是否被吊销

接下来,浏览器会验证数字证书是否已被吊销。吊销证书意味着该证书已不再有效,可能是由于证书私钥泄露、证书持有者身份变更或其他安全原因。浏览器可以通过下载吊销证书列表(CRL)或在线验证方式(OCSP)来检查证书是否被吊销。

4. 验证数字证书是否由合法CA机构颁发

这是验证流程中最关键的一步。浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发。如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。

5. 验证证书链

浏览器会验证证书链的完整性,从服务器的证书开始,逐级向上验证每个证书的签名是否有效,直到根证书。这一步确保了证书的层级关系和信任链是完整的。

6. 检查证书的域名

浏览器会检查证书中包含的域名是否与实际访问的域名一致,以防止域名欺诈。

7. 生成和交换密钥

在验证证书无误后,浏览器会生成一个随机值(密钥),并用证书中的公钥对该随机值进行加密,然后发送给服务器。服务器用私钥解密,得到相同的密钥,之后双方使用这个密钥进行对称加密通信。 通过以上步骤,HTTPS确保了数据传输的安全性和服务器的身份认证,从而保护了通信过程不被窃听和篡改。

48. Vue 中的双向绑定和单向数据流原则是否冲突?

Vue 中的双向绑定和单向数据流原则在表面上看起来可能有些冲突,但实际上它们可以在不同的场景和层级上和谐共存。

双向绑定

Vue 通过双向绑定实现了视图(View)和模型(Model)之间的同步。当数据在模型中发生变化时,视图会自动更新以反映这些变化;同样,当用户在视图上进行操作(如输入表单数据)时,模型中的数据也会相应更新。这种机制在处理表单输入等场景时非常方便。

单向数据流

单向数据流指的是数据只能从父组件流向子组件,子组件不能直接修改父组件传递给它的数据。这种设计原则有助于维护组件之间的清晰关系,使得数据流向更容易追踪,从而减少应用中的错误和不确定性。

如何共存

在 Vue 中,双向绑定通常发生在组件内部,即组件的视图和其自身的数据状态之间。而单向数据流则描述了组件之间传递数据的方式。当父组件向子组件传递数据时,子组件通过 props 接收这些数据,并根据这些数据渲染自己的视图。如果子组件需要修改数据,它应该通过事件向父组件发送消息,由父组件来更新数据,而不是直接修改 props。

这样,双向绑定和单向数据流就在不同的层面上发挥作用:双向绑定用于组件内部的状态和视图同步,而单向数据流用于组件之间的数据传递。两者结合使用,可以既方便地管理组件内部的状态,又保持组件间关系的清晰和可预测性。

总之,Vue 的设计允许这两种模式在同一应用中协同工作,从而充分利用它们各自的优势。

49. 实现 (5).add(3).minus(2) 功能

为了实现 (5).add(3).minus(2) 这样的链式调用功能,我们需要在 JavaScript 的 Number 对象的原型上添加 addminus 方法。这样,所有的数字都可以使用这些方法。下面是一个简单的实现:

Number.prototype.add = function(num) {
  return this + num;
};
Number.prototype.minus = function(num) {
  return this - num;
};
// 使用示例
let result = (5).add(3).minus(2);
console.log(result); // 输出 6

在这个实现中,add 方法接收一个数字作为参数,将其与当前数字(this 指向当前数字)相加,并返回结果。同样,minus 方法将当前数字减去参数数字,并返回结果。由于这两个方法都返回了数字,所以可以支持链式调用。 请注意,修改内置对象的原型(如 Number.prototype)通常不是一个好的做法,因为它可能会引起意外的副作用,尤其是在大型项目中或当使用第三方库时。一个更安全的做法是创建一个单独的函数或对象来提供这些功能。例如:

function createChainableNumber(num) {
  let currentNumber = num;
  return {
    add: functionaddComponent(num) {
      currentNumber += num;
      return this;
    },
    minus: function(num) {
      currentNumber -= num;
      return this;
    },
    value: function() {
      return currentNumber;
    }
  };
}
// 使用示例
let chainableNumber = createChainableNumber(5);
let result = chainableNumber.add(3).minus(2).value();
console.log(result); // 输出 6

在这个版本中,我们创建了一个 createChainableNumber 函数,它返回一个包含 addminusvalue 方法的对象。这样,我们就可以在不修改内置对象原型的情况下实现链式调用。

50. Vue 2 的响应式原理中 Object.defineProperty 有什么缺陷

Vue 2 的响应式原理主要依赖于 Object.defineProperty 来实现数据的劫持,从而在数据变化时能够自动更新视图。然而,Object.defineProperty 存在一些固有的缺陷:

  1. 无法检测数组索引的变化Object.defineProperty 无法检测到数组索引的直接修改,例如 arr[0] = 1。Vue 2 通过重写数组的 pushpopshiftunshiftsplicesortreverse 等方法来解决这个问题,但仍然无法检测到通过索引直接设置数组的项。
  2. 无法检测对象属性的添加或删除Object.defineProperty 无法检测到对象属性的添加(如 obj.newProp = 'value')或删除(如 delete obj.prop)。Vue 2 通过 Vue.setVue.delete 方法来解决这个问题,但这些方法需要手动调用。
  3. 性能问题: 使用 Object.defineProperty 对数据进行响应式处理时,需要遍历对象的所有属性并定义 getter/setter,这会导致一定的性能开销,尤其是在数据量大或属性多的情况下。
  4. 兼容性问题Object.defineProperty 是 ES5 的特性,不支持 IE8 及以下浏览器。虽然现代浏览器都支持这个特性,但仍然存在一定的兼容性问题。
  5. 无法劫持非对象属性Object.defineProperty 只能用于对象属性,无法用于非对象属性,如数组的长度属性(length)。
  6. 原型链上的属性无法被检测: 如果对象的原型链上有属性被修改,Object.defineProperty 无法检测到这些变化。
  7. 复杂的对象结构可能导致问题: 对于嵌套较深的对象结构,响应式系统的实现会变得复杂,可能导致一些难以调试的问题。 由于这些缺陷,Vue 3 采用了 Proxy 来实现响应式系统,Proxy 能够更好地处理这些情况,提供更强大、更灵活的拦截能力。Proxy 可以拦截对对象的各种操作,包括属性的添加、删除、数组索引的变化等,从而实现更完善的响应式系统。