2025面试大全(8)

5 阅读59分钟

1. 前端应用上线后,怎么通知用户刷新当前页面?

前端应用上线后,通知用户刷新当前页面的方法有多种,以下是几种常见的策略:

  1. 手动刷新提示
    • 在应用更新后,可以在页面上显示一个弹窗或通知条,提示用户有新版本可用,并建议他们手动刷新页面。
  2. Service Worker
    • 利用Service Worker的更新机制,可以在后台检测到新版本后,控制页面显示更新提示,或者在下次用户访问页面时自动刷新。
  3. WebSocket
    • 如果应用支持WebSocket,可以在服务器端通过WebSocket连接推送更新通知到客户端,客户端收到通知后提示用户刷新。
  4. 轮询机制
    • 客户端可以设置一个轮询机制,定期向服务器请求版本信息,如果发现版本有更新,则提示用户刷新页面。
  5. 使用HTTP缓存控制
    • 通过设置HTTP缓存头信息,如Cache-Control: no-cache, no-store, must-revalidate,可以确保每次请求都从服务器获取最新资源,从而间接促使页面刷新。
  6. 版本号检查
    • 在前端代码中嵌入版本号,并在页面加载时检查版本号是否与服务器上的版本号一致,如果不一致,则提示用户刷新。
  7. 强制刷新
    • 在某些情况下,可以通过服务器配置强制客户端刷新,例如通过设置HTTP状态码304(Not Modified)来控制缓存行为。
  8. 推送通知
    • 如果应用支持推送通知(如PWA的推送通知),可以在服务器端发送推送通知到用户设备,通知用户有新版本可用。
  9. 自动刷新
    • 在某些对实时性要求较高的应用中,可以设置页面定时自动刷新,但这可能会影响用户体验。
  10. 前端路由控制
    • 如果是单页应用(SPA),可以通过前端路由控制,在检测到更新后,通过路由跳转触发页面刷新。 选择哪种方法取决于应用的具体需求、用户体验考虑以及技术实现复杂度。在实际应用中,通常会结合多种方法来确保用户能够及时获得更新。

2. JS 的严格模式有什么作用?

JavaScript 的严格模式(Strict Mode)是一种在 ECMAScript 5 中引入的机制,通过在脚本或函数的开头添加 "use strict"; 这条指令来启用。严格模式的作用主要包括以下几点:

  1. 消除了一些静默错误
    • 在严格模式下,一些在非严格模式下可能不会报错的问题将会抛出错误,例如给未声明的变量赋值。
  2. 改进了错误提示
    • 严格模式提供了更清晰的错误提示,有助于开发者更快地定位和解决问题。
  3. 禁用了一些可能会在未来版本中定义的语法
    • 这有助于确保代码的未来兼容性。
  4. 禁止使用with语句
    • with语句在严格模式下会被禁用,因为它可能会导致代码运行时的性能问题和不清晰的代码。
  5. 不允许删除不可删除的属性
    • 在严格模式下,尝试删除不可删除的属性会抛出错误。
  6. 不允许重复的属性名
    • 在严格模式下,对象字面量中不允许有重复的属性名。
  7. 禁止使用八进字面量
    • 在严格模式下,不支持以0开头的八进制字面量。
  8. 全局变量必须显式声明
    • 在严格模式下,所有变量都必须使用varletconst显式声明,否则会抛出错误。
  9. 函数中的this对象始终为undefined
    • 在严格模式下,非构造函数调用时,函数内部的this值始终为undefined,而不是全局对象。
  10. 不允许使用arguments.callee和arguments.caller
    • 这两个属性在严格模式下被禁用,以防止一些潜在的性能问题和安全问题。
  11. 增强了安全性和可靠性
    • 严格模式有助于编写更安全、更可靠的代码,因为它强制执行了一些好的编程实践。
  12. 优化了JavaScript引擎的性能
    • 由于严格模式提供了一些明确的规则,JavaScript引擎可以针对这些规则进行优化,从而提高代码的执行效率。 启用严格模式是现代JavaScript开发中的一个推荐做法,它有助于开发者编写更清晰、更健壮的代码。

3. JS 的严格模式为什么会禁用 with 语句?

JavaScript 的严格模式禁用 with 语句的原因主要有以下几点:

  1. 性能问题
    • with 语句会改变作用域链,这会导致JavaScript引擎在查找变量时需要进行更多的查找操作,从而影响性能。在严格模式下,为了提高代码的执行效率,with 语句被禁用。
  2. 代码可读性和维护性
    • with 语句可以使代码更短,但也会使代码的上下文变得不清晰,增加理解的难度。禁用 with 语句可以促使开发者编写更清晰、更易于维护的代码。
  3. 避免潜在的错误
    • with 语句可能会导致一些意外的行为,特别是当作用域链中的变量和 with 对象的属性名相同时,容易引发混淆和错误。严格模式通过禁用 with 语句来避免这些潜在的问题。
  4. 未来兼容性
    • 为了确保代码在未来版本的JavaScript中仍然有效,严格模式禁用了一些可能在未来被修改或移除的语法,with 语句就是其中之一。
  5. 促进良好的编程实践
    • with 语句的使用通常被认为是一种不好的编程实践,因为它可能导致代码的意图不明确。严格模式鼓励开发者遵循更严格的编码规范。
  6. 安全性考虑
    • with 语句可能会被用于一些不安全的操作,例如修改对象的原型链,这可能会引发安全漏洞。严格模式通过禁用 with 语句来增强代码的安全性。 总的来说,禁用 with 语句是为了提高代码的性能、可读性、维护性、未来兼容性、安全性和促进良好的编程实践。这些原因共同促使严格模式中不再支持 with 语句。

4. 怎么遍历 ES6 中的 Set 对象?

在 ES6 中,Set 对象是一个集合,它存储的是唯一值的集合。遍历 Set 对象的方法有以下几种:

  1. 使用 forEach 方法
    const mySet = new Set([1, 2, 3]);
    mySet.forEach(value => {
      console.log(value);
    });
    
  2. 使用 for...of 循环
    const mySet = new Set([1, 2, 3]);
    for (const value of mySet) {
      console.log(value);
    }
    
  3. 使用扩展运算符(...
    const mySet = new Set([1, 2, 3]);
    [...mySet].forEach(value => {
      console.log(value);
    });
    
  4. 使用 Array.from() 方法
    const mySet = new Set([1, 2, 3]);
    Array.from(mySet).forEach(value => {
      console.log(value);
    });
    
  5. 使用 for...of 循环遍历键值对(如果需要同时获取值和键,尽管在 Set 中键和值是相同的):
    const mySet = new Set([1, 2, 3]);
    for (const [key, value] of mySet.entries()) {
      console.log(key, value);
    }
    
  6. 使用 values() 方法
    const mySet = new Set([1, 2, 3]);
    for (const value of mySet.values()) {
      console.log(value);
    }
    
  7. 使用 keys() 方法(在 Set 中,keys()values() 方法返回的是相同的迭代器):
    const mySet = new Set([1, 2, 3]);
    for (const key of mySet.keys()) {
      console.log(key);
    }
    

所有这些方法都可以有效地遍历 Set 对象中的元素。选择哪种方法取决于你的具体需求和代码风格。for...of 循环和 forEach 方法是遍历 Set 的最常见方式。

5. 怎么遍历 ES6 中的 Map 对象?

在 ES6 中,Map 对象是一种集合,它存储的是键值对,并且能够记住键的原始插入顺序。遍历 Map 对象的方法有以下几种:

  1. 使用 forEach 方法
    const myMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
    myMap.forEach((value, key) => {
      console.log(`${key}: ${value}`);
    });
    
  2. 使用 for...of 循环遍历键值对
    const myMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
    for (const [key, value] of myMap) {
      console.log(`${key}: ${value}`);
    }
    
  3. 使用 for...of 循环遍历 entries
    const myMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
    for (const entry of myMap.entries()) {
      console.log(`${entry[0]}: ${entry[1]}`);
    }
    
  4. 使用 for...of 循环遍历 keys
    const myMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
    for (const key of myMap.keys()) {
      console.log(key);
    }
    
  5. 使用 for...of 循环遍历 values
    const myMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
    for (const value of myMap.values()) {
      console.log(value);
    }
    
  6. 使用扩展运算符(...
    const myMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
    [...myMap].forEach(([key, value]) => {
      console.log(`${key}: ${value}`);
    });
    
  7. 使用 Array.from() 方法
    const myMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
    Array.from(myMap).forEach(([key, value]) => {
      console.log(`${key}: ${value}`);
    });
    

这些方法都可以用来遍历 Map 对象中的键值对。forEach 方法和 for...of 循环是最常用的遍历方式,它们可以方便地访问每个键值对。选择哪种方法取决于你的具体需求和代码风格。

6. 对于一个使用 Webpack 的项目,如果需要直接通过 script 标签引入第三方的资源,应该怎么处理?

在一个使用 Webpack 的项目中,如果需要直接通过 script 标签引入第三方资源,可以采取以下几种方法处理:

1. 使用 externals 配置

Webpack 的 externals 配置项允许你指定某些模块不应该被webpack解析,而是在运行时从环境中获取。

// webpack.config.js
module.exports = {
  // ...
  externals: {
    'library-name': 'LibraryGlobalName'
  }
  // ...
};

然后在你的代码中,你可以这样引入:

import LibraryGlobalName from 'library-name';

在HTML文件中,你需要手动添加 script 标签:

<script src="path/to/library.js"></script>

2. 直接在 HTML 中引入

如果你不想通过 Webpack 管理这个第三方资源,可以直接在 HTML 文件中通过 script 标签引入。

<!-- index.html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/library-name/1.0.0/library.min.js"></script>

然后在你的 JavaScript 文件中,可以直接使用这个库的全局变量。

3. 使用 html-webpack-plugin 插件

如果你使用 html-webpack-plugin 来生成 HTML 文件,可以在插件配置中添加 script 标签。

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: 'path/to/template.html',
      scriptLoading: 'blocking', // 确保脚本按顺序加载
      scripts: [
        {
          src: 'https://cdnjs.cloudflare.com/ajax/libs/library-name/1.0.0/library.min.js',
          type: 'text/javascript',
          async: false
        }
      ]
    })
  ]
  // ...
};

4. 使用 script-loader

script-loader 是一个 Webpack loader,它可以用来将 JavaScript 文件作为全局脚本注入到页面中。 首先,安装 script-loader

npm install --save-dev script-loader

然后在 Webpack 配置中添加规则:

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: require.resolve('library-name'),
        use: 'script-loader'
      }
    ]
  }
  // ...
};

在代码中引入:

require('library-name');

5. 使用 imports-loader

imports-loader 可以用来将全局变量注入到模块中。 首先,安装 imports-loader

npm install --save-dev imports-loader

然后在 Webpack 配置中添加规则:

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: require.resolve('library-name'),
        use: 'imports-loader?this=>window'
      }
    ]
  }
  // ...
};

在代码中引入:

require('library-name');

注意事项

  • 全局变量冲突:直接通过 script 标签引入的库可能会污染全局命名空间,导致变量冲突。
  • 模块化:如果第三方库是模块化的,直接通过 script 标签引入可能无法正常工作,需要使用 Webpack 的模块系统。
  • 加载顺序:确保第三方资源的加载顺序正确,特别是如果有依赖关系的话。 根据你的具体需求和项目结构,选择最适合的方法来处理直接通过 script 标签引入的第三方资源。

7. 在Babel里,stage0、stage1、stage2 和 stage3 分别代表什么含义?

在 Babel 中,stage0stage1stage2stage3 是指 ECMAScript 提案的不同成熟阶段。这些阶段反映了提案从初步想法到最终成为标准的过程。以下是每个阶段的详细含义:

Stage 0: Strawman(草案阶段)

  • 含义:这个阶段是提案的最初阶段,通常只是一个想法或初步的草案。
  • 特点:想法可能不完整,需要大量的讨论和修改。
  • 目的:收集社区意见,看看是否有足够兴趣继续推进。

Stage 1: Proposal(提案阶段)

  • 含义:提案已经有一个初步的规范,并且得到了一定的支持。
  • 特点:提案需要明确其目的和基本用法,可能还需要进行进一步的讨论和修改。
  • 目的:进一步细化提案,解决潜在的问题,为进入下一个阶段做准备。

Stage 2: Draft(草案阶段)

  • 含义:提案已经有一个更完整的规范,并且已经解决了大部分问题。
  • 特点:提案的语法和功能基本稳定,但可能仍有一些细节需要调整。
  • 目的:收集实现反馈,确保提案的可行性和兼容性。

Stage 3: Candidate(候选阶段)

  • 含义:提案已经非常成熟,接近最终的标准。
  • 特点**:提案的规范已经基本完成,只有少量的问题需要解决。**
  • 目的:进行最终的测试和审查,确保提案的质量和稳定性。

Stage 4: Finished(完成阶段)

虽然你没有提到 stage4,但为了完整性,这里也简单介绍一下:

  • 含义:提案已经正式成为 ECMAScript 标准。
  • 特点:提案的规范已经完全确定,不再进行修改。
  • 目的:提案已经被纳入标准,可以广泛使用。

Babel 中的使用

在 Babel 中,你可以通过安装相应的插件来使用这些不同阶段的特性。例如:

npm install --save-dev @babel/plugin-proposal-stage-0

然后在 Babel 配置文件中启用它:

// .babelrc 或 babel.config.js
{
  "plugins": ["@babel/plugin-proposal-stage-0"]
}

注意事项

  • 稳定性:阶段的数字越低,特性的稳定性和兼容性越低。使用低阶段的特性可能会带来更多的风险。
  • 更新:ECMAScript 提案会随着时间的推移而更新,阶段的划分也可能发生变化。 通过了解这些阶段,你可以更好地选择和使用 Babel 来转换你的代码,同时保持对最新 ECMAScript 特性的支持。

8. Proxy 和 Reflect 有什么关系?

ProxyReflect 是 ECMAScript 2015(ES6)引入的两个特性,它们通常一起使用,以提供更强大、更灵活的JavaScript对象操作能力。它们之间的关系和协同工作方式如下:

Proxy

  • 定义Proxy 对象用于创建一个对象的代理,这个代理可以拦截并定义对象的基本操作,如属性查找、赋值、枚举、函数调用等。
  • 用途:通过Proxy,你可以实现自定义的行为,例如实现数据绑定、验证、日志记录、实现私有属性等。
  • 语法
    let proxy = new Proxy(target, handler);
    
    • target:被代理的对象。
    • handler:一个对象,其属性是当执行一个操作时触发的函数。

Reflect

  • 定义Reflect 是一个内置对象,提供了一系列静态方法,用于访问和操作对象。这些方法与Proxy处理程序的方法相同。
  • 用途Reflect 的方法用于在Proxy的处理器函数中调用默认行为,确保语言的一致性,并且使得操作更加清晰和易于管理。
  • 方法Reflect 的方法包括getsetapplydefineProperty等,它们与Object对象上的同名方法功能相似,但有一些区别,例如返回值和异常处理。

关系和协同

  1. 拦截与默认行为
    • Proxy 用于拦截对象操作。
    • Reflect 用于执行默认行为。在Proxy的处理器函数中,通常使用Reflect来调用对应的默认行为。
  2. 保持一致性
    • 使用Reflect可以确保Proxy的行为与原生对象的行为保持一致。例如,Object.defineProperty在无法定义属性时抛出异常,而Reflect.defineProperty会返回false
  3. 代码可读性
    • 使用Reflect可以使代码更加清晰和易于理解。例如,Reflect.get(target, name, receiver)清晰地表示获取target对象的name属性。
  4. 组合使用
    • Proxy的处理器中,可以组合使用Reflect来执行默认行为,同时添加自定义逻辑。例如:
      let handler = {
        get(target, prop, receiver) {
          console.log(`Getting ${prop}!`);
          return Reflect.get(target, prop, receiver); // 执行默认的get行为
        }
      };
      let proxy = new Proxy(target, handler);
      

示例

let target = {
  name: 'Alice',
  age: 25
};
let handler = {
  get(target, prop, receiver) {
    if (prop === 'name') {
      return `Mr. ${Reflect.get(target, prop, receiver)}`;
    }
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    if (prop === 'age' && value < 0) {
      throw new Error('Age cannot be negative');
    }
    return Reflect.set(target, prop, value, receiver);
  }
};
let proxy = new Proxy(target, handler);
console.log(proxy.name); // Mr. Alice
proxy.age = 30; // 正常设置
console.log(proxy.age); // 30
proxy.age = -5; // 抛出错误

在这个示例中,Proxy拦截了getset操作,Reflect用于执行默认的getset行为,同时添加了自定义逻辑。 总之,ProxyReflect共同提供了一种机制,使得开发者可以以更灵活、更可控的方式操作对象,同时保持与JavaScript语言规范的一致性。

9. 浏览器对“队头阻塞”有什么优化?

队头阻塞(Head-of-Line Blocking,简称HOLB)是网络通信中的一个问题,特别是在HTTP/1.x中,由于浏览器限制了同一时间只能对一个域名进行有限的并发请求(通常为6个),如果一个请求被阻塞,后续的请求即使已经准备好发送,也必须等待,直到前面的请求完成。这在HTTP/1.x中是一个常见的问题,因为它会导致资源的延迟加载,从而影响页面加载性能。 为了优化队头阻塞问题,浏览器和HTTP协议本身都进行了一些改进:

浏览器层面的优化:

  1. 并发连接增加
    • 浏览器可以通过增加对同一域名的并发连接数来减轻队头阻塞的影响。但这并不是一个根本的解决方案,因为过多的并发连接可能会对服务器造成压力。
  2. 域名分片
    • 通过将资源分散到不同的域名下,浏览器可以同时对多个域名发起请求,从而绕过单个域名的连接限制。这种方法被称为域名分片(Domain Sharding)。
  3. 资源优先级
    • 浏览器可以根据资源的优先级来调度请求,确保关键资源(如CSS、JavaScript)优先加载,而非关键资源(如图片)可以在后面加载。
  4. 预连接(Preconnect)
    • 浏览器可以使用预连接来提前建立与服务器的连接,这样当需要发送请求时,可以立即使用已经建立的连接,减少等待时间。
  5. 请求合并
    • 将多个小请求合并为一个大的请求,可以减少请求的数量,从而减轻队头阻塞的影响。

HTTP协议层面的优化:

  1. HTTP/2
    • HTTP/2引入了多路复用(Multiplexing),允许在单个连接上同时发送多个请求和响应,而不需要等待前面的请求完成。这样可以有效解决队头阻塞问题。
    • HTTP/2还支持头部压缩(HPACK),减少了请求头的大小,进一步提高了传输效率。
  2. QUIC协议
    • QUIC是Google提出的一种基于UDP的传输协议,它支持多路复用和快速握手,可以在一定程度上避免队头阻塞。
    • QUIC还支持连接迁移,即使网络环境发生变化,也可以保持连接的稳定性。
  3. HTTP/3
    • HTTP/3是基于QUIC的HTTP协议版本,它继承了QUIC的所有优点,包括多路复用和避免队头阻塞。

其他优化措施:

  1. 服务器推送
    • HTTP/2支持服务器推送(Server Push),允许服务器在客户端请求之前主动发送资源,这样可以减少客户端的请求次数,从而减轻队头阻塞。
  2. 资源内联
    • 将小资源(如CSS、JavaScript)直接内联到HTML中,可以减少请求的数量,从而减轻队头阻塞。
  3. 使用CDN
    • 内容分发网络(CDN)可以将资源分布到全球多个节点,减少客户端与服务器之间的距离,从而减少延迟和队头阻塞。 通过这些优化措施,浏览器和HTTP协议可以在很大程度上减轻队头阻塞的影响,提高网页的加载速度和用户体验。

10. JS 的加载是否会阻塞浏览器渲染?

是的,JavaScript的加载和执行通常会阻塞浏览器的渲染过程。这是因为浏览器在解析HTML时,如果遇到<script>标签,会停止解析HTML文档,转而下载、解析并执行JavaScript代码。在这个过程中,浏览器的渲染进程会被阻塞,导致页面无法继续渲染,直到JavaScript代码执行完成。 这种阻塞行为的主要原因包括:

  1. 同步加载和执行:默认情况下,<script>标签是同步加载和执行的。这意味着浏览器必须等待脚本下载、解析和执行完成后,才能继续解析HTML和渲染页面。
  2. DOM修改:JavaScript可以修改DOM结构,如果在解析HTML时遇到<script>标签,浏览器需要先执行脚本,以确定是否有DOM结构的修改,然后再继续渲染。
  3. 样式计算:JavaScript可以修改CSS样式,这可能导致浏览器的样式计算和重排(reflow)过程,因此需要等待脚本执行完成后再进行渲染。 为了减轻JavaScript对浏览器渲染的阻塞,可以采取以下优化措施:
  4. 异步加载(Async):使用async属性可以让浏览器异步加载JavaScript文件,不会阻塞HTML的解析,但仍然会阻塞渲染直到脚本执行完成。
  5. 延迟加载(Defer):使用defer属性可以让浏览器延迟执行JavaScript文件,直到整个HTML文档解析完成后再执行。这可以确保HTML的解析不受阻塞,并且脚本按照它们在文档中出现的顺序执行。
  6. 将脚本放在底部:将<script>标签放在HTML文档的底部,接近</body>标签,可以确保HTML和CSS的解析和渲染优先进行,减少阻塞时间。
  7. 使用JavaScript模块:现代浏览器支持JavaScript模块(ES6 Modules),可以通过type="module"属性来异步加载和执行模块化的JavaScript代码。
  8. 避免长任务:避免在JavaScript中执行长时间运行的任务,可以使用Web Workers来在后台线程执行复杂计算,避免阻塞主线程。
  9. 优化脚本执行:减少JavaScript的执行时间,比如通过优化算法、避免不必要的DOM操作等。 通过这些优化措施,可以有效地减少JavaScript对浏览器渲染的阻塞,提高页面的加载速度和用户体验。

11. 在开发过程中, 使用过哪些 TS 的特性或者能力?

在开发过程中,TypeScript(TS)提供了许多强大的特性和能力,以下是一些常用的TS特性:

  1. 静态类型
    • 为变量、函数参数和返回值指定类型,例如 numberstringboolean 等。
    • 使用接口(Interfaces)和类型别名(Type Aliases)定义复杂的对象类型。
  2. 强类型
    • 通过强类型检查,提前发现潜在的错误,减少运行时错误。
  3. 泛型
    • 使用泛型创建可重用的组件,例如泛型函数、泛型接口和泛型类。
  4. 枚举(Enums):
    • 定义一组命名的常量,使代码更易读和维护。
  5. 接口(Interfaces):
    • 定义对象的结构,强制类型检查,确保对象符合预期的形状。
  6. (Classes):
    • 使用类来定义对象的结构和行为,包括属性、方法、构造函数和继承。
  7. 模块(Modules):
    • 使用模块来组织代码,提高可维护性。
  8. 装饰器(Decorators):
    • 为类、方法、访问符、属性或参数添加额外的行为。
  9. 联合类型(Union Types):
    • 允许变量有多种类型,增加灵活性。
  10. 交叉类型(Intersection Types):
    • 结合多个类型,创建新的类型。
  11. 类型守卫(Type Guards):
    • 在运行时检查变量的类型,确保类型安全。
  12. 映射类型(Mapped Types):
    • 基于旧类型创建新类型,例如将所有属性设置为只读。
  13. 条件类型(Conditional Types):
    • 根据条件表达式决定类型。
  14. 非空断言(Non-null Assertion):
    • 使用 ! 操作符告诉编译器某个值不会是 nullundefined
  15. 索引签名(Index Signatures):
    • 定义对象可以包含任意数量的属性。
  16. 类型推断(Type Inference):
    • 让编译器自动推断变量的类型。
  17. 类型兼容性(Type Compatibility):
    • 确保不同类型之间的兼容性,例如子类型可以赋值给父类型。
  18. 高级类型
    • 包括元组(Tuples)、字面量类型(Literal Types)、可选属性(Optional Properties)等。
  19. 工具类型
    • 使用内置的工具类型,如 Partial<T>Readonly<T>Pick<T, K> 等,来简化类型操作。
  20. 命名空间(Namespaces):
    • 组织代码,避免全局变量污染。 在开发过程中,根据项目需求和团队约定,可以选择适合的TS特性来提高代码的质量、可维护性和可扩展性。

12. babel 的核心库有哪些?

Babel 是一个广泛使用的 JavaScript 编译器,它可以将现代 JavaScript 代码(ES6+)转换为向后兼容的版本,以便在旧版浏览器或其他环境中运行。Babel 的核心库主要包括以下几个部分:

  1. @babel/core
    • Babel 的核心库,提供了编译 JavaScript 代码的主要功能。
  2. @babel/parser
    • 解析器,用于将 JavaScript 代码解析成抽象语法树(AST)。
  3. @babel/traverse
    • 遍历器,用于遍历 AST 并进行修改。
  4. @babel/generator
    • 生成器,用于将修改后的 AST 转换回 JavaScript 代码。
  5. @babel/types
    • 类型检查库,用于验证和构建 AST 节点。
  6. @babel/template
    • 模板库,用于构建和替换 AST 节点。
  7. @babel/transform-runtime
    • 提供了一个运行时助手,用于在转换代码时注入必要的 polyfills 和 helpers。
  8. @babel/cli
    • 命令行工具,允许从命令行运行 Babel。
  9. @babel/register
    • 模块注册器,用于在 Node.js 环境中实时转换模块。
  10. @babel/preset-env
    • 预设,根据目标环境自动确定需要转换的语法和特性。
  11. @babel/preset-react
    • React 预设,用于转换 React 代码。
  12. @babel/preset-typescript
    • TypeScript 预设,用于转换 TypeScript 代码。 这些核心库共同构成了 Babel 的编译流程,从解析源代码到生成目标代码,以及在这个过程中对代码进行各种转换和优化。开发者可以根据需要选择和使用这些库来定制 Babel 的行为。

13. 为什么 react 组件中, 都需要声明 import React from 'react';

在 React 组件中声明 import React from 'react'; 的原因主要有以下几点:

  1. JSX 转换
    • React 使用 JSX(JavaScript XML)语法来描述用户界面。JSX 语法不是原生 JavaScript 的一部分,因此需要通过 Babel 等工具转换成普通的 JavaScript 函数调用。在转换过程中,JSX 标签会被转换为 React.createElement 调用。因此,需要从 react 包中导入 React 以确保 React.createElement 可用。
  2. 组件定义
    • 在类组件中,需要继承 React.Component 或使用 React.PureComponent。这要求必须导入 React
  3. Hooks
    • 在函数组件中使用 Hooks(如 useState, useEffect 等)时,这些 Hooks 是从 react 包中导出的。因此,需要导入 React 以使用这些 Hooks。
  4. 上下文(Context)
    • 使用 React Context API 时,需要从 react 包中导入 React.createContext
  5. 其他 API
    • React 提供了许多其他 API,如 React.memo, React.forwardRef 等,这些 API 也需要从 react 包中导入。
  6. 一致性
    • 即使在某些情况下(如使用纯函数组件且不使用 JSX 或 Hooks)可能不需要显式导入 React,为了保持代码的一致性和可读性,通常也会导入 React
  7. 未来兼容性
    • 随着 React 的不断发展,未来可能会添加新的功能或 API,这些新功能可能需要 React 对象。因此,显式导入 React 可以确保代码在未来保持兼容。 尽管在最新的 JSX 转换中(如使用 @babel/plugin-transform-react-jsx 的最新版本),可以不需要显式导入 React,但这是一种较新的实践,并且不是所有环境都支持。因此,为了兼容性和稳定性,仍然建议在 React 组件中显式导入 React

14. 如何从 0 到 1 搭建前端基建?

从0到1搭建前端基建是一个复杂且系统的工程,需要考虑多个方面,包括技术选型、项目结构、开发流程、代码规范、自动化测试、持续集成与部署等。以下是一个大致的步骤指南:

1. 需求分析与规划

  • 明确目标:确定项目的目标、受众和功能需求。
  • 技术调研:研究市场上的前端技术栈,选择适合项目需求的技术。
  • 规划架构:设计高可用、可扩展的前端架构。

2. 技术选型

  • 前端框架:选择React、Vue、Angular等主流框架。
  • 状态管理:根据需要选择Redux、MobX、Vuex等。
  • 路由管理:如React Router、Vue Router等。
  • UI组件库:选择Ant Design、Element UI、Bootstrap等。
  • 打包工具:Webpack、Parcel、Rollup等。
  • 代码规范:ESLint、Prettier等。
  • 测试框架:Jest、Mocha、Cypress等。

3. 项目结构与配置

  • 初始化项目:使用脚手架工具(如Create React App、Vue CLI)快速初始化项目。
  • 目录结构:设计清晰的目录结构,如components、pages、services、utils等。
  • 配置文件:配置Webpack、Babel、ESLint等。

4. 开发环境搭建

  • 本地开发服务器:如Webpack Dev Server。
  • 热更新:实现模块热替换(HMR)提高开发效率。
  • API模拟:使用Mock服务模拟后端API。

5. 代码规范与风格

  • 编码规范:制定统一的编码规范,如使用ESLint。
  • 代码风格:使用Prettier等工具统一代码风格。
  • 代码审查:建立代码审查流程,确保代码质量。

6. 组件化开发

  • 原子组件:开发可复用的原子组件。
  • 复合组件:构建由原子组件组成的复合组件。
  • 页面开发:使用组件构建页面。

7. 状态管理

  • 设计状态结构:合理设计应用的状态结构。
  • 状态管理库:集成状态管理库,如Redux或Vuex。

8. 路由与导航

  • 路由配置:设置应用的路由结构。
  • 导航守卫:实现权限验证、登录拦截等。

9. 接口与数据管理

  • API封装:封装统一的API请求库。
  • 数据管理:处理数据获取、缓存、更新等。

10. 测试

  • 单元测试:编写单元测试确保组件功能正确。
  • 集成测试:进行集成测试验证组件间交互。
  • 端到端测试:使用Cypress等工具进行端到端测试。

11. 构建与部署

  • 打包优化:优化打包输出,如代码分割、tree shaking。
  • 部署策略:选择合适的部署策略,如CI/CD流水线。
  • 监控与日志:设置应用监控和日志收集。

12. 文档与培训

  • 编写文档:编写项目文档,包括开发指南、API文档等。
  • 团队培训:对团队成员进行技术培训和知识分享。

13. 持续迭代

  • 收集反馈:收集用户和团队的反馈。
  • 持续优化:根据反馈进行持续优化和迭代。

14. 安全与性能

  • 安全措施:实施前端安全措施,如XSS防护、CSRF防护等。
  • 性能优化:进行性能优化,如懒加载、服务端渲染(SSR)等。 搭建前端基建是一个持续的过程,需要根据项目的发展和团队的需求不断调整和优化。重要的是建立一个可持续、可维护的前端架构,以支持长期的业务发展。

15. webpack 在打包时,给生成的文件名上添加的 hash 码,是如何生成的?

Webpack在打包时给生成的文件名添加的hash码是一种用于实现缓存控制的机制。当文件内容发生变化时,hash码也会随之改变,从而使得浏览器能够识别出文件的新版本并重新下载,而不是使用旧的缓存版本。

Hash生成原理

Webpack使用以下步骤生成hash码:

  1. 文件内容摘要:Webpack会对每个文件的内容进行摘要,通常使用SHA256、MD5等散列函数。这个摘要过程会生成一个固定长度的字符串,这个字符串代表了文件内容的唯一标识。
  2. 整合摘要:对于整个打包过程,Webpack会整合所有文件的摘要,以及一些其他信息(如模块的依赖关系、编译时的选项等),然后对整合后的信息再次进行摘要。
  3. 生成hash码:最终生成的摘要字符串就是hash码。这个hash码会被添加到生成的文件名中。

Hash类型

Webpack支持几种不同的hash类型:

  • hash:对于整个编译过程,生成一个唯一的hash码。这意味着只要打包过程中有任何文件发生变化,整个应用的hash码都会改变。
  • chunkhash:基于每个chunk的内容生成hash码。一个chunk通常包含一个或多个模块。使用chunkhash可以确保只有当chunk中的文件发生变化时,该chunk的hash码才会改变。
  • contenthash:基于文件内容生成hash码。这是最细粒度的hash,只有当文件内容实际发生变化时,文件的hash码才会改变。这对于缓存控制非常有用,因为它可以确保只有变更的文件被重新下载。

示例

在Webpack配置文件中,你可以这样设置输出文件的名称,以包含hash码:

output: {
  filename: '[name].[contenthash].js',
  path: path.resolve(__dirname, 'dist')
}

在这个示例中,[name]是模块的名称,[contenthash]是基于文件内容生成的hash码。

注意事项

  • 缓存控制:使用hash码可以有效地控制浏览器缓存,但需要注意配置HTTP缓存头,以确保浏览器正确地使用这些hash码。
  • 性能考虑:生成hash码会增加构建过程的计算量,但对于现代构建工具和硬件来说,这种开销通常是可接受的。
  • hash碰撞:虽然hash函数设计的目的是尽可能减少碰撞,但在理论上仍然存在碰撞的可能性。不过,在实际应用中,这种概率极低。 通过这种方式,Webpack能够确保在文件内容发生变化时,生成的文件名也会随之变化,从而实现有效的缓存控制。

16. 说说你对 API IntersectionObserver 的理解,它有什么使用场景?

API IntersectionObserver的理解 IntersectionObserver API 是一个用于异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的变化的API。简单来说,它允许你配置一个回调,该回调会在目标元素进入或退出另一个元素(或视窗)时执行。 核心概念

  1. 目标元素:你想要观察的元素。
  2. 根元素:目标元素与之交叉的元素,通常是视窗,但也可以是其他元素。
  3. 交叉:目标元素与根元素之间的交集区域。
  4. 交叉阈值:一个数值或数组,用于定义何时触发回调。例如,可以设置当目标元素的10%可见时触发回调。 工作原理
  • 创建一个IntersectionObserver实例,并传入一个回调函数和配置对象。
  • 配置对象可以设置根元素、交叉阈值等选项。
  • 调用实例的observe方法,并传入目标元素。
  • 当目标元素与根元素的交叉状态发生变化时,回调函数会被执行,并传入一个包含交叉信息的数组。 使用场景
  1. 无限滚动:当用户滚动到页面底部时,自动加载更多内容。
  2. 懒加载:只有当图片进入视窗时,才开始加载图片,以节省资源并提高页面加载速度。
  3. 广告可见性检测:检测广告是否真正被用户看到,以优化广告投放效果。
  4. 动画触发:当元素进入视窗时,触发动画或过渡效果。
  5. 导航栏固定:当页面滚动到一定位置时,固定导航栏。
  6. 元素曝光统计:统计页面中某个元素被用户看到的次数和时长。 优点
  • 性能优化:相比传统的事件监听方式(如滚动事件),IntersectionObserver不需要在每个滚动事件中都执行计算,从而减少了计算量和不必要的布局重排。
  • 简洁易用:API设计简洁,易于理解和实现。
  • 灵活性:可以灵活配置根元素、交叉阈值等选项,以满足不同需求。 注意事项
  • 浏览器兼容性:虽然大部分现代浏览器都支持IntersectionObserver,但仍然需要考虑旧版浏览器的兼容性问题。
  • 阈值设置:合理的阈值设置对于实现预期效果至关重要。 总之,IntersectionObserver API 为我们提供了一种高效、简洁的方式来观察元素间的交叉状态,并在多种场景下发挥了重要作用。

17. 在表单校验场景中, 如何实现页面视口滚动到报错的位置

在表单校验场景中,如果需要将页面视口滚动到报错的位置,可以使用以下步骤实现:

  1. 获取报错元素的位置: 使用Element.getBoundingClientRect()方法可以获取报错元素相对于视口的位置。
  2. 计算滚动位置: 根据报错元素的位置,计算需要滚动的距离。通常,你可能希望将报错元素滚动到视口顶部或某个特定位置。
  3. 执行滚动操作: 使用window.scrollTo()Element.scrollTo()方法执行滚动操作。 以下是一个具体的实现示例:
// 假设有一个函数用于校验表单,并在错误发生时调用scrollToError函数
function validateForm() {
  // ...进行表单校验
  if (errorOccurred) {
    const errorElement = document.getElementById('error-field'); // 获取报错元素的引用
    scrollToError(errorElement);
  }
}
// 定义scrollToError函数,将视口滚动到报错元素的位置
function scrollToError(element) {
  // 获取元素的位置信息
  const elementRect = element.getBoundingClientRect();
  
  // 计算滚动位置,这里我们选择将元素顶部对齐到视口顶部
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  const scrollPosition = scrollTop + elementRect.top - (window.innerHeight - elementRect.height) / 2;
  // 执行滚动操作,可以添加平滑滚动效果
  window.scrollTo({
    top: scrollPosition,
    behavior: 'smooth' // 平滑滚动
  });
}

在这个示例中,validateForm函数是表单校验的逻辑,当检测到错误时,会获取报错元素的引用并调用scrollToError函数。scrollToError函数计算了需要滚动的位置,并使用window.scrollTo方法将视口滚动到该位置。通过设置behavior: 'smooth',可以实现平滑滚动的效果。 注意事项

  • 兼容性scrollTo方法的平滑滚动效果在旧版浏览器中可能不支持,需要考虑兼容性处理。
  • 布局变化:如果页面布局发生变化(如响应式设计中的尺寸变化),可能需要重新计算滚动位置。
  • 性能:在复杂的应用中,频繁的滚动操作可能会影响性能,需要合理控制滚动事件的触发。 通过这种方式,可以在表单校验场景中实现页面视口滚动到报错位置的功能,提升用户体验。

18. Husky 和 lint-staged 有什么区别?

Husky 和 lint-staged 都是前端开发中常用的 Git 钩子工具,它们可以用来在代码提交前执行一些自动化任务,如代码格式化、代码风格检查、单元测试等。尽管它们的目标相似,但它们在功能和用法上有所区别:

Husky

功能

  • Husky 是一个用于管理 Git 钩子的工具,它可以让你在 Git 的不同生命周期阶段(如 pre-commitpre-pushcommit-msg 等)运行脚本。 特点
  • 灵活性:Husky 允许你运行任何类型的脚本,不仅限于代码检查。
  • 配置:通常通过在 package.json 中配置或在项目根目录下创建 .husky 目录来设置钩子。
  • 集成:可以与其他工具(如 lint-staged)集成使用。 用法
  • 安装 Husky 并在 package.json 中配置钩子,例如:
{
  "husky": {
    "hooks": {
      "pre-commit": "npm test",
      "pre-push": "npm run lint"
    }
  }
}

lint-staged

功能

  • lint-staged 是一个专门用于在 Git 暂存文件上运行 linters 的工具,它只检查你将要提交的文件。 特点
  • 针对性:仅对暂存区(staged)的文件执行操作,不会影响未暂存的文件。
  • 效率:因为只检查暂存文件,所以执行速度较快。
  • 配置:通常与 Husky 结合使用,作为 pre-commit 钩子的一部分。 用法
  • 安装 lint-staged 并在 package.json 中配置,例如:
{
  "lint-staged": {
    "*.js": ["eslint --fix", "git add"]
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  }
}

区别总结

  • 范围:Husky 可以在多个 Git 钩子中运行任意脚本,而 lint-staged 专注于在 pre-commit 钩子中对暂存文件运行代码检查和修复。
  • 用途:Husky 更通用,可以用于各种自动化任务;lint-staged 更专一,主要用于代码质量和风格检查。
  • 配置:Husky 需要配置钩子和要运行的命令,lint-staged 需要配置针对特定文件类型的操作。 结合使用
  • 通常,开发者会结合使用 Husky 和 lint-staged,利用 Husky 设置钩子,而 lint-staged 负责在 pre-commit 钩子中执行对暂存文件的检查和修复。 这样,两者相辅相成,可以有效地提高代码质量和开发效率。

19. 在 React 项目中, 是否可以不用 react-router, 使用浏览器原生 history 路由来组织页面路由?

**是的,可以在 React 项目中不使用 react-router,而使用浏览器原生的 History API 来组织页面路由。**但是,这种做法会相对复杂,需要手动处理更多的细节。

使用浏览器原生 History API 的方法:

  1. 监听历史记录变化
    • 使用 window.addEventListener('popstate', handler) 来监听浏览器历史记录的变化,如后退和前进操作。
  2. 改变历史记录
    • 使用 history.pushState(state, title, url) 来添加新的历史记录。
    • 使用 history.replaceState(state, title, url) 来替换当前历史记录。
  3. 渲染组件
    • 根据当前 URL,手动决定渲染哪个组件。
    • 这通常涉及到解析 URL 并匹配到相应的组件。

示例代码:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.handleNavigation = this.handleNavigation.bind(this);
    this.state = { route: window.location.pathname };
  }
  componentDidMount() {
    window.addEventListener('popstate', this.handleNavigation);
  }
  componentWillUnmount() {
    window.removeEventListener('popstate', this.handleNavigation);
  }
  handleNavigation(event) {
    this.setState({ route: window.location.pathname });
  }
  render() {
    const { route } = this.state;
    let PageComponent;
    if (route === '/') {
      PageComponent = Home;
    } else if (route === '/about') {
      PageComponent = About;
    } else {
      PageComponent = NotFound;
    }
    return <PageComponent />;
  }
}

考虑因素:

  • 复杂性:手动管理路由状态和组件渲染会增加代码的复杂性。
  • 功能限制:原生 History API 不提供 react-router 的所有功能,如嵌套路由、动态路由、路由守卫等。
  • 开发效率:使用 react-router 可以大大提高开发效率,因为它提供了丰富的路由功能和便捷的 API。

结论:

虽然可以不使用 react-router 而使用原生 History API,但通常不推荐这样做,除非有特别的理由。react-router 提供了更高级、更便捷的路由管理功能,可以大大简化 React 应用中的路由处理。如果项目需求简单,或者需要最大程度的控制路由行为,可以考虑使用原生 History API。但在大多数情况下,使用 react-router 是更明智的选择。

20. react-router 和原生路由有什么区别?

react-router 和原生路由(即浏览器原生的 History API)在实现页面路由方面有本质的区别,主要体现在以下几个方面:

1. 实现方式

  • react-router
    • 是一个专门为 React 应用设计的路由库。
    • 提供了 <Router>, <Route>, <Switch>, <Link>, <NavLink> 等组件来简化路由的定义和使用。
    • 内部使用了原生 History API,但对其进行了封装和扩展。
  • 原生路由(History API)
    • 是浏览器提供的一组 API,用于操作浏览器历史记录。
    • 包括 history.pushState(), history.replaceState(), window.popstate 事件等。
    • 需要手动监听历史记录变化并更新页面内容。

2. 易用性

  • react-router
    • 提供了高级抽象,使得定义和管理路由变得简单直观。
    • 支持嵌套路由、动态路由、路由守卫等高级功能。
    • 有丰富的文档和社区支持。
  • 原生路由
    • 需要手动处理路由逻辑,如解析 URL、匹配组件、更新视图等。
    • 实现复杂路由功能时,代码量和工作量较大。
    • 缺乏开箱即用的解决方案和社区支持。

3. 功能丰富性

  • react-router
    • 支持多种路由模式,如 BrowserRouter, HashRouter, MemoryRouter 等。
    • 提供了路由参数、查询参数、重定向、懒加载等实用功能。
    • 可以方便地与 Redux、MobX 等状态管理库集成。
  • 原生路由
    • 功能相对基础,主要限于操作历史记录和监听历史变化。
    • 实现高级路由功能需要自行开发或寻找第三方库。

4. 性能

  • react-router
    • 作为一个库,有一定的性能开销,但通常可以忽略不计。
    • 通过优化和懒加载等技术,可以有效地管理大型应用的路由性能。
  • 原生路由
    • 性能开销较小,因为直接使用浏览器提供的 API。
    • 但手动实现复杂路由逻辑可能导致性能问题。

5. 适用场景

  • react-router
    • 适用于大多数 React 应用,特别是需要复杂路由管理的应用。
    • 提供了更好的开发体验和可维护性。
  • 原生路由
    • 适用于小型应用或对路由需求非常简单的场景。
    • 在需要最大程度控制路由行为或避免引入额外依赖时可以考虑。

结论:

react-router 为 React 应用提供了强大、易用且功能丰富的路由解决方案,适用于大多数场景。而 原生路由 更适合于简单应用或特殊需求,但在复杂应用中可能会显得力不从心。选择哪种方式取决于项目的具体需求和开发者的偏好。

21. 怎么给某个元素的背景设置渐变色?

给某个元素的背景设置渐变色可以通过CSS的background-image属性来实现。以下是几种常见的渐变色设置方法:

1. 线性渐变(Linear Gradient)

线性渐变可以沿着一条直线改变颜色。你可以指定渐变的方向以及起始颜色和结束颜色。

.element {
  background-image: linear-gradient(direction, color1, color2, ...);
}
  • direction 可以是角度(如 45deg)或关键字(如 to right)。
  • color1, color2, ... 是渐变的颜色-stop。 示例:
.element {
  background-image: linear-gradient(to right, red, yellow);
}

2. 径向渐变(Radial Gradient)

径向渐变是从一个点向外发散的渐变效果。

.element {
  background-image: radial-gradient(shape size at position, color1, color2, ...);
}
  • shape 可以是 circleellipse
  • size 定义渐变的尺寸,如 closest-side, farthest-side, closest-corner, farthest-corner
  • position 定义渐变的中心位置。
  • color1, color2, ... 是渐变的颜色-stop。 示例:
.element {
  background-image: radial-gradient(circle at center, red, yellow);
}

3. 圆锥渐变(Conic Gradient)

圆锥渐变是围绕一个中心点旋转的渐变效果。

.element {
  background-image: conic-gradient(from angle at position, color1, color2, ...);
}
  • from angle 定义渐变的起始角度。
  • position 定义渐变的中心位置。
  • color1, color2, ... 是渐变的颜色-stop。 示例:
.element {
  background-image: conic-gradient(from 0deg at center, red, yellow, green);
}

4. 重复渐变

你可以使用 repeating-linear-gradientrepeating-radial-gradient 来创建重复的渐变效果。 示例:

.element {
  background-image: repeating-linear-gradient(to right, red, yellow 10%, green 20%);
}

注意事项:

  • 渐变背景可以与其他背景属性(如 background-color, background-size 等)一起使用。
  • 为了兼容性,可以提供多个背景图像,用逗号分隔。
  • 使用渐变时,考虑性能影响,尤其是在移动设备上。 将这些CSS规则应用到你的HTML元素上,就可以实现背景的渐变效果。记得测试在不同浏览器上的表现,以确保兼容性。

22. React 为什么要自己实现调度器, 而不是直接使用 requestIdleCallback ?

React 自行实现调度器(Scheduler)而不是直接使用 requestIdleCallback 的原因主要有以下几点:

  1. 兼容性
    • requestIdleCallback 是一个相对较新的API,并不是所有的浏览器都支持。为了确保React能够在更多的环境中运行,需要提供一个兼容性的解决方案。
  2. 控制权
    • 使用 requestIdleCallback 会将控制权交给浏览器,React团队可能希望有更多的控制权来优化调度逻辑,以适应React特定的需求。
  3. 调度策略
    • React的调度器需要支持多种调度策略,包括但不限于时间分片、优先级调度等。requestIdleCallback 只提供了在浏览器空闲时执行回调的功能,并不能完全满足React的复杂调度需求。
  4. 性能优化
    • React团队可以通过自定义调度器来优化性能,比如通过时间分片来减少主线程的负担,提高应用的响应性。
  5. 灵活性
    • 自定义调度器可以提供更多的灵活性,比如可以根据任务的优先级来调度任务,而 requestIdleCallback 并不提供这样的功能。
  6. polyfill
    • 在不支持 requestIdleCallback 的环境中,React可以提供一个polyfill来模拟相似的功能,这样可以保证在这些环境中React仍然可以正常工作。
  7. 实验和迭代
    • 通过自行实现调度器,React团队可以更容易地进行实验和迭代,尝试不同的调度算法和策略,以找到最佳的性能和用户体验平衡。
  8. 避免限制
    • requestIdleCallback 有一些限制,比如回调可能在帧的开始时执行,而不是在空闲时间,这可能会影响React的调度策略。 总之,React团队选择自行实现调度器是为了更好地控制调度过程,提供更好的兼容性、性能和灵活性,以适应React框架的特定需求和优化目标。

23. express 里面的"中间件"和"插件"是同一个东西吗?

在 Express 框架中,“中间件”(Middleware)和“插件”(Plugin)是两个相关但不同的概念。下面是它们之间的区别:

中间件(Middleware):

  1. 定义:中间件是 Express 中处理请求和响应的函数,这些函数可以访问请求对象(req)、响应对象(res)以及下一个中间件函数(next)。
  2. 用途:中间件用于执行各种任务,如请求解析、内容压缩、日志记录、身份验证、路由控制等。
  3. 执行顺序:中间件按照在应用程序中注册的顺序执行。
  4. 类型:中间件可以是内置的(如 express.static)、第三方的(如 body-parser)或自定义的。
  5. 示例
    app.use((req, res, next) => {
      console.log('Middleware executed');
      next();
    });
    

插件(Plugin):

  1. 定义:插件通常是指第三方包或模块,它们为 Express 应用程序添加特定的功能或服务。
  2. 用途:插件可以提供各种功能,如数据库集成、身份验证机制、模板引擎等。
  3. 安装:插件通常需要通过 npm 安装到项目中。
  4. 类型:插件可以是专门为 Express 设计的,也可以是通用的 Node.js 模块。
  5. 示例
    // 安装并使用 body-parser 插件
    const bodyParser = require('body-parser');
    app.use(bodyParser.urlencoded({ extended: true }));
    

关系和区别:

  • 关系:插件可以提供中间件功能。例如,body-parser 插件实际上是一组中间件,用于解析请求体。
  • 区别
    • 中间件是更具体的概念,指的是在请求处理过程中执行的函数。
    • 插件是更广泛的概念,可以包括中间件,也可以包括其他类型的模块或服务。 简而言之,所有中间件都可以被视为插件(如果它们是作为独立包提供的),但并非所有插件都是中间件。插件可能包含中间件,但也可能包含其他功能,如配置对象、工具函数等。

24. 前端渲染和后端渲染分别有什么优缺点, 为什么现在的技术大方向,又逐渐往“后端渲染”方向靠了呢?

前端渲染和后端渲染是构建网页应用的两种主要方式,它们各有优缺点。以下是它们的对比以及为什么现代技术趋势又逐渐向后端渲染靠拢的原因:

前端渲染(Frontend Rendering,FR)

优点

  1. 减轻服务器负担:前端渲染将渲染任务交给客户端的浏览器,减轻了服务器的计算压力。
  2. 提升用户体验:可以实现更流畅的交互和动态内容更新,无需重新加载页面。
  3. SEO优化:通过SPA(单页应用)框架结合SSR(服务器端渲染)或ISR(增量静态生成)等技术,可以改善SEO。
  4. 开发灵活:前端框架(如React、Vue、Angular)提供了丰富的组件和状态管理,使得开发更加灵活和高效。 缺点
  5. 首次加载时间较长:需要下载完整的JavaScript包才能开始渲染,可能导致首次加载时间较长。
  6. SEO挑战:纯前端渲染的SPA在初始加载时可能没有内容,对SEO不利。
  7. 依赖客户端性能:渲染性能受客户端设备性能的影响。

后端渲染(Backend Rendering,BR)

优点

  1. 快速首次加载:服务器直接返回渲染好的HTML,减少了首次加载时间。
  2. 更好的SEO:搜索引擎可以更容易地抓取和索引服务器渲染的内容。
  3. 减少客户端负担:渲染工作在服务器完成,减轻了客户端的负担。
  4. 安全性:可以在服务器端进行更多的安全检查和验证。 缺点
  5. 服务器压力较大:每个请求都需要服务器进行渲染,增加了服务器的计算压力。
  6. 交互性有限:传统的后端渲染通常需要重新加载页面来更新内容,交互性不如前端渲染。

为什么技术趋势向后端渲染靠拢?

  1. SEO优化:随着搜索引擎对动态内容的抓取能力提升,后端渲染仍然是最可靠的SEO优化方式。
  2. 性能提升:现代服务器性能提升,可以更高效地处理渲染任务。
  3. 用户体验:通过结合前端技术(如AJAX、Vue、React等),后端渲染也可以实现良好的用户体验。
  4. 安全性:后端渲染可以在服务器端进行更多的安全控制,减少XSS等安全风险。
  5. 混合渲染:现代技术趋势是采用混合渲染策略,如SSR(服务器端渲染)与CSR(客户端渲染)结合,以充分利用两者的优势。 总之,后端渲染在SEO、安全性和首次加载性能方面具有优势,而前端渲染在交互性和开发灵活性方面表现更好。现代技术趋势是结合两者的优势,采用混合渲染策略,以实现更优的用户体验和开发效率。

25. documentFragment api 是什么, 有哪些使用场景?

DocumentFragment 是一个轻量级的文档对象,它包含了一组节点,但自身并不是文档树的一部分。由于DocumentFragment不会像直接操作DOM那样引起页面重排和重绘,因此使用它可以提高性能。

DocumentFragment API

DocumentFragment 的API相对简单,主要包括以下方法:

  • appendChild():向DocumentFragment中添加一个子节点。
  • insertBefore():在DocumentFragment中的指定节点前插入一个新节点。
  • replaceChild():替换DocumentFragment中的某个子节点。
  • removeChild():从DocumentFragment中移除一个子节点。

使用场景

  1. 批量更新DOM: 当你需要一次性插入多个节点时,可以先把这些节点添加到一个DocumentFragment中,然后一次性将这个DocumentFragment插入到DOM中,这样可以减少页面重排的次数。
  2. 模板渲染: 在动态生成大量DOM节点时,可以使用DocumentFragment作为临时容器,待所有节点生成完毕后再一次性添加到文档中。
  3. 性能优化: 在复杂的应用中,频繁的DOM操作可能会导致性能问题。使用DocumentFragment可以减少直接对DOM的操作,从而提高性能。
  4. 组件化开发: 在开发Web组件时,可以使用DocumentFragment来构建组件的内部结构,然后再将整个组件插入到文档中。
  5. 文档片段的复用: 由于DocumentFragment不会成为文档树的一部分,它可以被重复使用,这在需要重复创建相似结构时非常有用。

示例代码

以下是一个使用DocumentFragment来批量添加列表项的示例:

// 创建一个空的DocumentFragment
var fragment = document.createDocumentFragment();
// 创建多个列表项
for (var i = 0; i < 3; i++) {
  var li = document.createElement('li');
  li.textContent = '列表项 ' + (i + 1);
  fragment.appendChild(li);
}
// 获取ul元素
var ul = document.getElementById('myList');
// 将所有列表项一次性添加到ul中
ul.appendChild(fragment);

在这个示例中,我们首先创建了一个DocumentFragment,然后循环创建了三个<li>元素并将它们添加到fragment中。最后,我们一次性将这个fragment添加到<ul>元素中,从而避免了多次重排和重绘。

26. 实现一个函数, 通过 id 来查找 tree 数据结构对应的节点

为了实现一个通过 ID 来查找树数据结构中对应节点的函数,我们首先需要定义树的数据结构。通常,树可以表示为一个对象,其中每个节点都有一个唯一的 ID,以及一个子节点的数组。以下是一个简单的实现:

树的数据结构示例

const tree = {
  id: 1,
  name: "Root",
  children: [
    {
      id: 2,
      name: "Child 1",
      children: [
        {
          id: 4,
          name: "Grandchild 1",
          children: []
        },
        {
          id: 5,
          name: "Grandchild 2",
          children: []
        }
      ]
    },
    {
      id: 3,
      name: "Child 2",
      children: []
    }
  ]
};

查找函数的实现

我们将使用递归来实现查找函数。函数将遍历树的每个节点,如果找到匹配的 ID,则返回该节点。

function findNodeById(node, id) {
  // 检查当前节点是否是目标节点
  if (node.id === id) {
    return node;
  }
  // 如果当前节点有子节点,递归搜索每个子节点
  if (node.children && node.children.length > 0) {
    for (const child of node.children) {
      const found = findNodeById(child, id);
      // 如果在子节点中找到目标节点,返回它
      if (found) {
        return found;
      }
    }
  }
  // 如果没有找到,返回null
  return null;
}
// 使用示例
const targetNode = findNodeById(tree, 5);
console.log(targetNode); // { id: 5, name: "Grandchild 2", children: [] }

解释

  1. 函数定义findNodeById函数接受两个参数:node(当前节点)和id(要查找的节点ID)。
  2. 检查当前节点:如果当前节点的ID与目标ID匹配,则返回当前节点。
  3. 递归搜索子节点:如果当前节点有子节点,函数将递归地对每个子节点调用自身。
  4. 返回结果:如果在子节点中找到匹配的节点,将其返回。如果遍历完所有节点都没有找到,最终返回null。 这个函数可以有效地在树结构中查找具有特定 ID 的节点。注意,这个实现假设每个节点的 ID 是唯一的。如果树中的 ID 不是唯一的,可能需要进一步的逻辑来处理这种情况。

27. 说说你对 MutationObserver 的理解

MutationObserver 是一个能够监听DOM树变化的API,它能够捕获到DOM元素的添加、删除、属性变化、子树变化等事件。以下是对MutationObserver的一些关键理解:

1. 基本概念

  • 观察者模式:MutationObserver采用了观察者模式,允许开发者订阅DOM变化事件,当发生变化时,观察者会收到通知。
  • 异步通知:MutationObserver的通知是异步的,这意味着所有的DOM变化都会被收集起来,然后在下一个任务周期(task)中一起通知,这有助于提高性能。

2. 使用方法

  • 创建观察者:通过new MutationObserver(callback)创建一个观察者实例,其中callback是当DOM变化时会被调用的函数。
  • 配置选项:在调用observe方法时,可以传递一个配置对象,指定需要观察的变化类型,如属性变化、子节点变化等。
  • 开始观察:调用观察者实例的observe(target, options)方法,开始观察target元素的变化。
  • 停止观察:调用观察者实例的disconnect()方法,停止观察。

3. 配置选项

  • childList:设置为true以观察目标节点的子节点的变动,比如添加或删除子节点。
  • attributes:设置为true以观察目标节点的属性变动。
  • characterData:设置为true以观察目标节点或子节点的文本内容变动。
  • subtree:设置为true以观察目标节点及其所有后代节点的变动。
  • attributeOldValue:设置为true以记录变动前的属性值。
  • characterDataOldValue:设置为true以记录变动前的文本内容。
  • attributeFilter:一个数组,包含需要观察的特定属性名称。

4. 优点

  • 性能优化:相比旧API如MutationEvents,MutationObserver不会在每次DOM变化时立即触发事件,而是将变化记录下来,然后在适当的时候批量处理,减少了重绘和重排的次数,提高了性能。
  • 更细粒度的控制:可以精确指定需要观察的变化类型,避免了不必要的事件处理。

5. 应用场景

  • 动态数据绑定:例如,在MVVM框架中,可以用来实现数据与视图的自动同步。
  • DOM操作库:用于开发需要监听DOM变化的库或框架。
  • 性能监控:监控页面中的DOM变化,以优化性能或进行调试。

6. 注意事项

  • 内存管理:如果不正确地使用MutationObserver,可能会导致内存泄漏。确保在不再需要时调用disconnect()方法。
  • 兼容性:虽然现代浏览器都支持MutationObserver,但在使用前仍需检查浏览器兼容性。 总之,MutationObserver是一个强大而灵活的API,用于监听DOM变化,适用于各种需要响应DOM变动的场景。正确使用它可以大大简化DOM操作的复杂性,并提高应用的性能。

28. mouseEnter、mouseLeave、mouseOver、mouseOut 有什么区别?

mouseentermouseleavemouseovermouseout 是 JavaScript 中的鼠标事件,它们在鼠标指针与元素交互时被触发。尽管它们看起来相似,但它们之间有一些关键的区别:

mouseenter 和 mouseleave

  • 不冒泡mouseentermouseleave 事件不会冒泡。这意味着事件只会被触发在目标元素上,而不会在其父元素上触发。
  • 不跨越边界:这些事件只在鼠标指针进入或离开元素时触发,不会因为子元素而触发。

mouseover 和 mouseout

  • 冒泡mouseovermouseout 事件会冒泡。当鼠标指针进入或离开一个元素或其子元素时,这些事件会在该元素上触发,并会冒泡到父元素。
  • 跨越边界:这些事件会在鼠标指针进入或离开元素及其子元素时触发。

具体区别

  1. 触发条件
    • mouseenter:当鼠标指针进入元素时触发。
    • mouseleave:当鼠标指针离开元素时触发。
    • mouseover:当鼠标指针进入元素或其子元素时触发。
    • mouseout:当鼠标指针离开元素或其子元素时触发。
  2. 冒泡行为
    • mouseenter/mouseleave:不冒泡。
    • mouseover/mouseout:冒泡。
  3. 子元素影响
    • mouseenter/mouseleave:不受子元素影响,只在鼠标进入或离开目标元素时触发。
    • mouseover/mouseout:受子元素影响,鼠标进入或离开目标元素或其子元素时都会触发。

示例

假设有一个父元素 div,其中包含一个子元素 span

<div id="parent">
  <span id="child">Hello</span>
</div>
  • 使用 mouseenter on #parent:只有当鼠标进入 #parent 时触发,进入 #child 不会触发。
  • 使用 mouseleave on #parent:只有当鼠标离开 #parent 时触发,离开 #child 不会触发。
  • 使用 mouseover on #parent:当鼠标进入 #parent#child 时都会触发。
  • 使用 mouseout on #parent:当鼠标离开 #parent#child 时都会触发。

使用场景

  • mouseenter/mouseleave:当你只想在鼠标进入或离开特定元素时触发事件,而不关心子元素时使用。
  • mouseover/mouseout:当你需要在鼠标进入或离开元素或其子元素时都触发事件时使用。 了解这些区别有助于你根据具体需求选择合适的事件来处理鼠标交互。

29. 如果你有一个内容站点,怎么防止其他人爬虫?

防止爬虫访问你的内容站点是一个常见的任务,但需要注意的是,完全阻止所有爬虫是不现实的,因为爬虫技术也在不断发展。然而,你可以采取一些措施来减少不希望的爬虫访问,同时尽量不妨碍合法的搜索引擎爬虫。以下是一些常用的方法:

1. Robots.txt 文件

  • 作用:指定哪些页面允许或不允许爬虫访问。
  • 注意:这只是一个建议,不良爬虫可能会忽略它。

2. 用户代理检测

  • 作用:检查请求的用户代理字符串,识别已知爬虫。
  • 注意:用户代理可以被伪造。

3. 频率限制(Rate Limiting)

  • 作用:限制来自同一IP地址的请求频率,防止爬虫过度访问。
  • 工具:可以使用Nginx、Apache的模块或专门的中间件来实现。

4. 验证码

  • 作用:对于可疑的自动化访问,弹出验证码进行人机识别。
  • 注意:可能会影响用户体验。

5. IP封禁

  • 作用:识别并封禁频繁访问的IP地址。
  • 注意:需要定期更新和维护IP列表。

6. 动态内容加载

  • 作用:使用JavaScript动态加载内容,增加爬取难度。
  • 注意:现代爬虫可以执行JavaScript。

7. 自定义字体或图片内容

  • 作用:将文本内容以图片或自定义字体形式展示,增加解析难度。
  • 注意:影响可访问性和SEO。

8. 使用API密钥

  • 作用:对于需要访问的数据,要求使用API密钥。
  • 注意:需要管理密钥分发和认证。

9. HTTPS和SSL证书

  • 作用:增加安全性和识别难度。
  • 注意:只是辅助手段。

10. 法律声明和版权声明

  • 作用:在网站上明确声明禁止爬虫,并保留法律追诉权。
  • 注意:主要起威慑作用。

11. 反爬虫服务

  • 作用:使用第三方反爬虫服务,如Cloudflare、Akamai等。
  • 注意:可能需要付费,且依赖第三方服务。

12. 行为分析

  • 作用:分析访问行为,识别异常模式。
  • 工具:可以使用日志分析工具或专门的软件。

实施策略

  • 组合使用:通常需要组合多种方法来有效防止爬虫。
  • 持续更新:爬虫技术不断进步,需要定期更新防爬策略。
  • 平衡用户体验:在防止爬虫的同时,确保不影响正常用户的访问体验。

示例:Robots.txt

User-agent: *
Disallow: /admin/
Disallow: /private/

示例:Nginx 频率限制

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
  location / {
    limit_req zone=mylimit;
  }
}

在实施这些措施时,要确保不会误伤合法的搜索引擎爬虫,如Googlebot、Bingbot等,因为它们对于站点的SEO至关重要。可以通过识别这些爬虫的用户代理并进行白名单处理来实现。

30. 在 JS 中, 如何解决递归导致栈溢出问题?

在JavaScript中,递归导致的栈溢出问题通常是因为递归深度过深,超过了JavaScript引擎允许的最大调用栈大小。以下是一些解决栈溢出问题的方法:

1. 尾递归优化

尾递归是一种特殊的递归形式,在函数返回时不再进行任何操作,而是直接返回递归函数的调用结果。一些JavaScript引擎支持尾递归优化,可以将尾递归转换为循环,避免增加调用栈深度。

function factorial(n, acc = 1) {
  if (n === 0) return acc;
  return factorial(n - 1, n * acc);
}

2. 使用循环代替递归

将递归逻辑转换为循环可以避免调用栈的增长。

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

3. 限制递归深度

设置一个最大递归深度,超过这个深度时停止递归或转换为其他处理方式。

let maxDepth = 1000;
let currentDepth = 0;
function recursiveFunction(args) {
  if (currentDepth > maxDepth) {
    throw new Error('Maximum recursion depth exceeded');
  }
  currentDepth++;
  // 递归逻辑
  currentDepth--;
}

4. 分而治之

将大问题分解为小问题,逐个解决,避免一次性递归过深。

5. 使用异步递归

通过异步函数(如使用setTimeoutPromise)来避免调用栈过深。

function asyncRecursiveFunction(args, callback) {
  setTimeout(() => {
    if (/* 递归终止条件 */) {
      callback(result);
    } else {
      asyncRecursiveFunction(newArgs, callback);
    }
  }, 0);
}

6. 非递归算法

对于某些问题,可以寻找非递归的算法来解决,从而避免递归带来的栈溢出问题。

7. 增加调用栈大小

在某些JavaScript环境中,可以尝试增加调用栈的大小,但这通常不是推荐的做法,因为它只是推迟了问题的发生,并没有真正解决。

8. 使用Web Workers

对于Web应用,可以使用Web Workers在后台线程中执行递归操作,避免影响主线程。

示例:将递归阶乘转换为循环

function factorial(n) {
  let result = 1;
  for (let i = 2; i <= n; i++) {
    result *= i;
  }
  return result;
}
console.log(factorial(5)); // 输出: 120

注意事项

  • 尾递归优化:不是所有的JavaScript引擎都支持尾递归优化,因此不能完全依赖这种方法。
  • 性能考虑:递归和循环在性能上可能有所不同,需要根据具体情况选择。
  • 代码可读性:递归通常比循环更具可读性,但在解决栈溢出问题时,可能需要牺牲一些可读性。 在实际应用中,应根据具体情况选择合适的方法来解决递归导致的栈溢出问题。