2025面试大全(14)

377 阅读45分钟

1. taro 的实现原理是怎么样的?

Taro 的实现原理

Taro 是一个多端统一开发框架,允许开发者使用 React、Vue 等前端技术栈开发小程序、H5、React Native 等平台的应用。其实现原理主要涉及以下几个方面: 1. 编译时转换

  • 源码解析:Taro 使用 Babel 等工具解析开发者编写的源码,将其转换为抽象语法树(AST)。
  • AST 转换:根据目标平台(如小程序、H5 等)的语法规则,将 AST 转换为目标平台的代码结构。例如,将 React 组件转换为小程序的页面结构。
  • 代码生成:将转换后的 AST 生成为目标平台的代码文件。 2. 运行时适配
  • API 适配:Taro 提供了一层适配层,将不同平台的 API 进行统一封装,使得开发者可以无缝地调用各平台的功能。
  • 组件适配:Taro 将 React、Vue 等组件转换为对应平台的组件,例如将 React 组件转换为小程序的组件。
  • 样式适配:Taro 使用 PostCSS 等工具将 CSS 样式转换为各平台支持的样式格式。 3. 多端统一
  • 组件化:Taro 采用组件化的开发方式,使得开发者可以复用代码,提高开发效率。
  • 状态管理:Taro 支持使用 Redux、MobX 等状态管理库,方便开发者管理应用状态。
  • 路由管理:Taro 提供了统一的路由管理方案,方便开发者进行页面跳转和传参。 具体实现细节:
  • React 技术栈:Taro 使用 React 的虚拟 DOM diff 算法进行组件更新,提高性能。
  • Vue 技术栈:Taro 支持 Vue 2 和 Vue 3,通过 Vue 的响应式系统实现数据驱动视图。
  • 小程序:Taro 将 React、Vue 组件转换为小程序的页面和组件,并使用小程序的 API 实现功能。
  • H5:Taro 将代码编译为 Web 标准的 HTML、CSS 和 JavaScript,可以在浏览器中运行。
  • React Native:Taro 将 React 组件转换为 React Native 组件,实现原生应用的性能和体验。 总结: Taro 的实现原理主要是通过编译时转换和运行时适配,将开发者编写的代码转换为各平台支持的代码格式,实现多端统一开发。其核心在于组件化、状态管理和路由管理的统一,以及不同平台 API 和组件的适配。 补充:
  • Taro 还提供了丰富的插件和扩展功能,方便开发者进行定制化开发。
  • Taro 社区活跃,文档齐全,学习资源丰富,适合前端开发者快速上手。

以上信息仅供参考,如有需要,建议咨询 Taro 官方文档或相关专业人士。

2. 单页应用如何提高加载速度?

单页应用(SPA,Single Page Application)因其所有资源都打包在一个文件中,首次加载可能会比较慢。以下是一些提高单页应用加载速度的方法: 1. 代码分割(Code Splitting)

  • 动态导入(Dynamic Imports):使用动态导入可以减少初始负载,按需加载所需的代码块。
  • 懒加载(Lazy Loading):将路由页面或组件懒加载,只有在需要时才加载。 2. 资源优化
  • 压缩代码和资源:使用工具如 Webpack、Rollup 等进行代码和资源的压缩,减少文件大小。
  • Tree Shaking:移除未使用的代码和库。 3. 缓存优化
  • 浏览器缓存:利用浏览器缓存,减少重复加载的资源。
  • 服务端渲染(SSR,Server-Side Rendering):将首屏内容在服务器渲染,加快首次加载速度。 4. 网络优化
  • 使用 CDN:利用 CDN 加速资源加载。
  • 预加载资源:在用户访问之前预加载必要的资源。 5. 优化构建过程
  • 并行构建:使用多线程、多进程进行构建,加快构建速度。
  • 监控构建过程:监控构建过程,找出性能瓶颈。 6. 其他优化
  • 使用 PWA(Progressive Web App):利用 PWA 的缓存和服务员工作线程,提高性能。
  • 优化图片和字体:压缩图片,使用字体加载优化策略。 7. 使用性能分析工具
  • Chrome DevTools:使用 Chrome DevTools 的 Lighthouse 进行性能分析。
  • WebPageTest:使用 WebPageTest 等工具进行性能测试。 总结: 提高单页应用的加载速度需要从代码、资源、缓存、网络等多个方面进行优化。建议根据具体情况进行调整和优化。 补充
  • 优化是一个持续的过程,需要不断监控和调整。
  • 建议在开发过程中使用性能分析工具,找出性能瓶颈。 注意: 以上方法仅供参考,具体优化方法需要根据具体应用情况进行调整。如有需要,建议咨询相关专业人士。

3. 以下等式是成立的吗:1_000_000 === 1000000 ?

是的,等式 1_000_000 === 1000000 是成立的原因如下

  • 在 JavaScript 中,从 ES2021(ECMAScript 2021)开始,允许在数字字面量中使用下划线(_)作为分隔符,以提高数字的可读性。
  • 1_000_0001000000 都表示相同的数值,即一百万。
  • === 是严格相等运算符,比较两个值是否严格相等(即数值和类型都相同)。 因此,1_000_000 === 1000000 的比较结果为 true,等式成立补充
  • 这种语法也适用于其他编程语言,但具体支持情况可能有所不同。
  • 使用下划线作为数字分隔符时,不能放在数字的开头或结尾,也不能连续使用。 示例
console.log(1_000_000 === 1000000); // true
console.log(1__000__000 === 1000000); // true
console.log(_1_000_000 === 1000000); // SyntaxError
console.log(1_000_000_ === 1000000); // SyntaxError

注意

  • 在不支持该语法的环境中,使用下划线作为数字分隔符会导致语法错误。 希望以上解释对您有所帮助!如有其他问题,请随时提问。

4. 实现 useUpdate 方法,调用时强制组件重新渲染

在 React 中,通常不需要强制组件重新渲染,因为 React 的状态管理机制会自动处理渲染。但是,在某些特定情况下,如果确实需要强制组件重新渲染,可以通过以下方式实现一个 useUpdate 自定义 Hook:

import { useState, useCallback } from 'react';
function useUpdate() {
  const [, setTick] = useState(0);
  const update = useCallback(() => {
    setTick(tick => tick + 1);
  }, []);
  return update;
}
export default useUpdate;

使用方法:

import React from 'react';
import useUpdate from './useUpdate';
function MyComponent() {
  const update = useUpdate();
  return (
    <div>
      <button onClick={update}>强制重新渲染</button>
    </div>
  );
}
export default MyComponent;

在这个 useUpdate Hook 中,我们使用了一个状态变量 tick,它实际上并不用于存储任何有用的信息,而是用来触发组件的重新渲染。每次调用 update 函数时,都会通过 setTick 更新 tick 的值,从而触发组件的重新渲染。 请注意,这种方法应该谨慎使用,因为频繁的强制重新渲染可能会导致性能问题。在大多数情况下,应该通过合理的状态管理和生命周期方法来控制组件的渲染。

5. 说说对 requestIdleCallback 的理解

requestIdleCallback 是一个 Web API,它允许开发者将任务安排在浏览器的空闲时段执行。这意味着浏览器会在主线程空闲时调用回调函数,从而不会影响用户界面的响应性或导致帧率下降。以下是对 requestIdleCallback 的详细理解:

工作原理

  1. 任务排队:当调用 requestIdleCallback 时,浏览器会将提供的回调函数放入一个队列中。
  2. 空闲时执行:当浏览器的主线程空闲时(即没有高优先级的任务如用户输入、动画等需要处理时),它会从队列中取出回调函数并执行。
  3. 时间控制:可以提供一个可选的参数来指定回调函数执行的时间限制,例如 { timeout: 1000 } 表示如果在一秒内没有空闲时间,则强制执行回调。

使用场景

  • 非关键任务:适用于执行那些不紧急且不影响用户体验的任务,如数据分析、日志记录、缓存更新等。
  • 避免阻塞:在执行可能耗时较长的任务时,使用 requestIdleCallback 可以避免阻塞主线程,从而保持界面的流畅性。

语法

requestIdleCallback(callback, [options]);
  • callback:一个在空闲时期被调用的函数。这个函数接收一个 IdleDeadline 对象作为参数,该对象包含两个方法:didTimeout(表示是否因为超时而被调用)和 timeRemaining()(返回当前空闲时段剩余的时间)。
  • options(可选):一个包含可选配置的对象。目前只有一个属性 timeout,表示最长时间限制,单位为毫秒。

注意事项

  • 兼容性:不是所有浏览器都支持 requestIdleCallback,特别是在较旧的浏览器中。可以使用 polyfill 来实现类似的功能。
  • 取消任务:如果需要取消已经排队的任务,可以使用返回的句柄调用 cancelIdleCallback
  • 优先级requestIdleCallback 的任务优先级较低,如果浏览器始终忙碌,这些任务可能永远不会执行。

示例

requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0) {
    // 执行任务...
  }
}, { timeout: 2000 });

在这个示例中,只要浏览器空闲,就会执行回调函数中的任务,直到剩余时间不足或超过设定的超时时间。

总结

requestIdleCallback 是一种优化性能的工具,它允许开发者利用浏览器的空闲时间执行非关键任务,从而不会影响用户的交互体验。然而,由于它的任务优先级较低,需要谨慎使用,确保不会因为任务长时间未执行而影响应用的正常功能。

6. 怎么在代码中判断一个 React 组件是 class component 还是 function component?

在 React 中,判断一个组件是类组件(class component)还是函数组件(function component)可以通过检查组件的类型来实现。以下是一些常用的方法:

方法一:使用 typeof

function isClassComponent(component) {
  return typeof component === 'function' && !!component.prototype.isReactComponent;
}
function isFunctionComponent(component) {
  return typeof component === 'function' && !component.prototype.isReactComponent;
}

方法二:使用 instanceof

function isClassComponent(component) {
  return component instanceof React.Component;
}
function isFunctionComponent(component) {
  return typeof component === 'function' && !(component instanceof React.Component);
}

方法三:检查原型链

function isClassComponent(component) {
  return (
    typeof component === 'function' &&
    Object.prototype.isPrototypeOf.call(React.Component.prototype, component.prototype)
  );
}
function isFunctionComponent(component) {
  return typeof component === 'function' && !isClassComponent(component);
}

方法四:使用 React.createElement

function isClassComponent(component) {
  try {
    React.createElement(component);
    return !!(component.prototype && component.prototype.isReactComponent);
  } catch (e) {
    return false;
  }
}
function isFunctionComponent(component) {
  return typeof component === 'function' && !isClassComponent(component);
}

注意事项

  • 高阶组件(HOC):如果组件是高阶组件,这些方法可能不适用,因为高阶组件本身是一个函数,但它返回的是一个组件。
  • React.memo:对于使用 React.memo 包装的函数组件,这些方法可能需要额外的处理来正确识别。
  • 异步组件:如果组件是异步加载的,可能需要先等待组件加载完成后再进行判断。

示例使用

class MyClassComponent extends React.Component {
  render() {
    return <div>Hello, Class Component!</div>;
  }
}
function MyFunctionComponent() {
  return <div>Hello, Function Component!</div>;
}
console.log(isClassComponent(MyClassComponent)); // true
console.log(isFunctionComponent(MyFunctionComponent)); // true

在 React 16.8 及以上版本中,还可以通过检查组件是否包含 useStateuseEffect 等钩子来辅助判断函数组件,但这不是一种完全可靠的方法,因为类组件中也可能使用这些钩子(通过高阶组件或渲染属性)。 选择哪种方法取决于具体的需求和场景。在实际应用中,通常不需要频繁地进行这种判断,因为 React 的设计哲学是让开发者更多地关注组件的功能而不是其实现方式。

7. 说说对 React 中Element、Component、Node、Instance 四个概念的理解

在 React 中,Element、Component、Node 和 Instance 是四个重要的概念,它们在 React 的架构和组件生命周期中扮演着不同的角色。下面是对每个概念的详细解释:

1. Element(元素)

  • 定义:React Element 是一个简单的对象,它描述了你在屏幕上看到的内容。Element 可以理解为组件的实例化结果,但它并不是真实的 DOM 节点。
  • 创建:通过 React.createElement() 创建,或者在 JSX 中编写组件时,编译后会转换为 React.createElement() 调用。
  • 特性:Element 是不可变的,一旦创建,其内容不会改变。React 会使用 Element 来生成真实的 DOM 节点并渲染到页面上。

2. Component(组件)

  • 定义:Component 是一个函数或类,它负责告诉 React 如何渲染 UI。组件可以是类组件(Class Component)或函数组件(Function Component)。
  • 类组件:继承自 React.Component 的类,拥有状态(state)和生命周期方法。
  • 函数组件:一个纯函数,接收 props 作为参数,返回一个 React Element。随着 Hooks 的引入,函数组件也可以拥有状态和副作用。
  • 特性:组件是可复用的,可以接收 props 作为输入,并返回 Element 描述的 UI。

3. Node(节点)

  • 定义:在 React 的上下文中,Node 通常指的是 DOM Node,即浏览器中的真实 DOM 元素。
  • 关系:React Element 描述了 Node 应该是什么样子,而 React 负责根据 Element 创建和更新相应的 DOM Node。
  • 特性:Node 是浏览器渲染树的一部分,可以直接与用户交互。

4. Instance(实例)

  • 定义:Instance 通常指的是组件的实例,即某个组件在渲染过程中创建的具体实例。
  • 类组件实例:当类组件被渲染时,会创建一个组件实例,这个实例拥有自己的状态和生命周期方法。
  • 函数组件实例:在函数组件中,由于组件本身是函数,没有实例的概念,但可以使用 Hooks 来模拟实例的状态和副作用。
  • 特性:Instance 是组件在运行时的表现,它负责维护组件的状态和响应生命周期事件。

关系总结

  • Element 是组件的描述,它告诉 React 应该渲染什么。
  • Component 是 Element 的来源,它定义了如何根据输入(props)生成 Element。
  • Node 是 Element 在浏览器中的真实表现,即 DOM 元素。
  • Instance 是组件在运行时的具体表现,它负责管理组件的状态和生命周期。 理解这些概念之间的关系有助于更好地掌握 React 的工作原理和组件的生命周期管理。在实际开发中,开发者通常通过编写 Component 来定义 UI,React 则负责根据这些 Component 创建 Element,进而渲染为浏览器中的 Node,并在需要时管理 Component Instance 的状态和生命周期。

8. 说说你对 ToPrimitive 的理解

ToPrimitive 是 JavaScript 中的一种抽象操作,用于将非原始值(如对象)转换为原始值(即字符串、数字或布尔值)。这个过程通常发生在需要进行原始类型操作时,比如数学运算、逻辑运算或字符串拼接等。

ToPrimitive 的基本过程

  1. 检查输入值:首先检查输入值是否已经是一个原始值。如果是,直接返回该值。
  2. 调用 valueOf 方法:如果输入值是一个对象,JavaScript 会首先尝试调用该对象的 valueOf 方法。valueOf 方法应该返回一个原始值。如果返回的是原始值,则使用这个值。
  3. 调用 toString 方法:如果 valueOf 方法返回的不是原始值,或者没有 valueOf 方法,JavaScript 会接着尝试调用该对象的 toString 方法。toString 方法应该返回一个字符串。如果返回的是原始值,则使用这个值。
  4. 抛出错误:如果 toString 方法返回的也不是原始值,或者没有 toString 方法,JavaScript 会抛出一个类型错误(TypeError)。

注意事项

  • 优先级:在转换过程中,valueOf 方法的优先级高于 toString 方法。但是,对于某些类型(如日期对象),浏览器会优先调用 toString 方法。
  • 自定义转换:开发者可以自定义对象的 valueOftoString 方法,以控制对象转换为原始值的行为。
  • 隐式转换:ToPrimitive 通常在隐式类型转换时发生,例如在进行数学运算或字符串拼接时。

示例

const obj = {
  valueOf: () => 42,
  toString: () => "42"
};
console.log(obj + 10); // 52,因为 valueOf 返回了 42,这是一个数字
console.log(obj.toString() + 10); // "4210",因为 toString 返回了 "42",这是一个字符串
const date = new Date();
console.log(date + 10); // 日期字符串拼接数字,因为 Date 对象的 toString 方法返回了一个字符串

在这个示例中,obj 对象同时定义了 valueOftoString 方法。在进行加法运算时,JavaScript 首先调用 valueOf 方法,因为它返回了一个数字,所以直接使用这个数字进行运算。如果 valueOf 不存在或返回的不是原始值,JavaScript 会尝试调用 toString 方法。

总结

ToPrimitive 是 JavaScript 中将对象转换为原始值的重要机制,它通过 valueOftoString 方法提供了灵活的转换方式。理解 ToPrimitive 的行为有助于更好地掌握 JavaScript 的类型转换和隐式类型转换规则。

9. 页面加载的过程中,JS 文件是不是一定会阻塞 DOM 和 CSSOM 的构建?

JS 文件不一定会阻塞 DOM 和 CSSOM 的构建,这取决于脚本的具体情况和浏览器的优化策略

情况一:JS 文件阻塞 DOM 构建

  • 默认情况:当浏览器解析 HTML 遇到 <script> 标签时,会停止解析 DOM,转而下载并执行脚本。这是因为在执行脚本时可能会修改 DOM,所以浏览器需要等待脚本执行完毕后再继续解析 DOM。
  • 解决方案:可以使用 asyncdefer 属性来避免这种情况。async 属性表示脚本异步执行,不会阻塞 DOM 解析;defer 属性表示脚本延迟执行,直到 DOM 解析完成后再执行。

情况二:JS 文件阻塞 CSSOM 构建

  • CSSOM 优先:在构建渲染树时,浏览器需要同时解析 DOM 和 CSSOM。如果浏览器在解析过程中遇到 JS 文件,并且该文件尝试访问 CSSOM,浏览器会停止解析 JS,直到 CSSOM 构建完成。这是因为 JS 可能会修改 CSSOM,所以需要等待 CSSOM 就绪。
  • 解决方案:将 CSS 样式放在文档的头部,确保在 JS 执行前 CSSOM 已经开始构建。此外,避免在 JS 中直接访问 CSSOM,或者使用异步方式加载和执行 JS。

浏览器优化

  • 并行下载:现代浏览器会并行下载多个资源,包括 JS、CSS 和图片等。这意味着即使 JS 文件正在下载,浏览器仍然可以继续解析 HTML 和构建 DOM。
  • 预解析:浏览器会预解析 HTML,提前发现并下载所需的资源,如 JS 和 CSS 文件。这可以减少资源下载的等待时间。

总结

  • 默认情况下,JS 文件会阻塞 DOM 的构建,但可以通过 asyncdefer 属性来避免。
  • JS 文件可能会阻塞 CSSOM 的构建,特别是在脚本尝试访问 CSSOM 时。通过合理组织代码和资源,可以减少这种阻塞。
  • 现代浏览器有优化策略,如并行下载和预解析,可以减少 JS 文件对页面加载的影响。 因此,虽然 JS 文件有可能阻塞 DOM 和 CSSOM 的构建,但通过合理的设计和浏览器的优化,可以最大限度地减少这种阻塞,提高页面加载性能。

10. 说说你对轮询的理解

**轮询(Polling)**是一种常见的通信方式,特别是在客户端与服务器之间的交互中。它指的是客户端定时向服务器发送请求,以获取最新的数据或状态更新。以下是关于轮询的详细理解:

轮询的基本概念

  1. 定时请求:客户端按照固定的时间间隔(如每秒、每分钟等)向服务器发送请求。
  2. 数据更新:服务器接收到请求后,返回最新的数据或状态信息。
  3. 持续监控:通过不断重复上述过程,客户端可以持续监控服务器端的数据变化。

轮询的应用场景

  1. 实时数据更新:如股票行情、体育比分、新闻更新等,需要实时显示最新信息。
  2. 状态监控:如服务器状态、设备状态等,需要实时了解状态变化。
  3. 消息通知:如邮件到达通知、社交平台的新消息提醒等。

轮询的优点

  1. 简单易实现:轮询的实现相对简单,只需要设置定时器发送请求即可。
  2. 兼容性好:几乎所有的客户端和服务器都支持HTTP请求,因此轮询具有很好的兼容性。

轮询的缺点

  1. 资源消耗:频繁的请求会导致服务器和网络的资源消耗增加。
  2. 延迟问题:轮询的时间间隔固定,如果间隔较长,可能导致数据更新不及时;如果间隔过短,又会增加资源消耗。
  3. 效率问题:在数据变化不频繁的情况下,很多请求可能都是无效的,因为服务器返回的数据没有变化。

轮询的改进方式

  1. 长轮询(Long Polling):客户端发送请求后,服务器会保持连接打开,直到有数据更新才返回响应。这种方式可以减少无效请求,提高效率。
  2. WebSocket:是一种全双工通信协议,允许服务器主动向客户端推送数据,实现实时通信,避免了轮询的缺点。
  3. Server-Sent Events(SSE):允许服务器主动向客户端发送事件,实现单向实时通信,适用于数据从服务器到客户端的实时推送。

总结

轮询是一种简单且广泛使用的通信方式,适用于需要实时数据更新的场景。然而,它也存在资源消耗和效率问题。在实际应用中,可以根据需求选择合适的改进方式,如长轮询、WebSocket或SSE,以实现更高效、更实时的通信。 轮询虽然有其局限性,但在某些场景下仍然是一种有效且实用的解决方案。随着技术的发展,我们有更多的选择来实现实时通信,以满足不同应用的需求。

11. 说说你对 CSS 模块化的理解

CSS模块化是一种将CSS代码组织成可复用、可维护、可扩展的模块的实践方法。它旨在解决传统CSS开发中遇到的命名冲突、覆盖问题、代码冗余和维护困难等问题。以下是对CSS模块化的详细理解:

CSS模块化的核心思想

  1. 封装:将CSS代码封装在特定的模块中,每个模块只负责特定的样式。
  2. 命名空间:通过特定的命名规则或编译工具,为CSS类名生成唯一的标识,避免命名冲突。
  3. 复用:模块化的CSS可以方便地在不同的页面或组件中复用,提高开发效率。
  4. 维护:模块化的结构使得CSS代码更易于维护和更新。

CSS模块化的实现方式

  1. CSS预处理器:如Sass、Less、Stylus等,提供了变量、嵌套、混合(Mixins)、函数等高级功能,有助于编写模块化的CSS代码。
  2. CSS-in-JS:将CSS代码直接写在JavaScript文件中,通过JavaScript动态生成样式,实现样式的模块化和组件化。
  3. CSS Modules:一种将CSS类名局部化的技术,通过编译工具将类名转换为唯一的标识,避免冲突。
  4. BEM(Block Element Modifier):一种命名约定,通过特定的命名规则(如block__element--modifier)来组织CSS代码,实现模块化。

CSS模块化的优点

  1. 避免冲突:通过命名空间或编译工具,有效避免类名冲突。
  2. 提高复用性:模块化的CSS代码易于在不同项目中复用。
  3. 便于维护:模块化的结构使得CSS代码更清晰、更易于维护。
  4. 增强可读性:合理的命名和组织方式提高代码的可读性。

CSS模块化的挑战

  1. 学习成本:采用新的工具或方法需要学习相应的语法和理念。
  2. 工具依赖:如使用CSS Modules或CSS-in-JS,需要依赖特定的构建工具。
  3. 兼容性问题:某些高级特性可能在旧版浏览器中不兼容。

CSS模块化的最佳实践

  1. 合理划分模块:根据功能或组件划分CSS模块,保持模块的独立性和单一职责。
  2. 遵循命名规范:使用BEM或其他命名规范,保持命名的一致性。
  3. 利用预处理器:使用Sass、Less等预处理器,提高CSS的编写效率和功能性。
  4. 文档和注释:为模块化的CSS代码编写文档和注释,方便团队协作和维护。

总结

CSS模块化是现代前端开发中的重要实践,它有助于提高CSS代码的质量、可维护性和可扩展性。通过采用合适的工具和方法,可以实现高效、可维护的CSS开发。随着前端技术的不断发展,CSS模块化将继续演进,为开发者提供更好的开发体验。

12. ::before 和::after 中双冒号和单冒号有什么区别、作用?

在CSS中,::before::after 是伪元素,而 :before:after 是伪类。双冒号和单冒号的区别主要体现在以下几个方面:

语法和标准化

  • 单冒号(:):早期CSS版本中使用的语法,用于伪类和伪元素。但在CSS2.1中,:before:after 被用作伪元素,尽管语法上是伪类的形式。
  • 双冒号(::):CSS3引入的语法,专门用于区分伪元素和伪类。::before::after 明确表示它们是伪元素。

语义和用途

  • 伪类(:):用于定义元素的特定状态,如:hover:active:focus等。
  • 伪元素(::):用于创建不在文档树中的元素,如::before::after::first-line::first-letter等。它们允许你为元素的特定部分添加样式。

兼容性

  • 单冒号:为了兼容旧版浏览器,很多开发者仍然使用:before:after
  • 双冒号:现代浏览器都支持双冒号语法,但为了兼容性,很多开发者会同时使用单冒号和双冒号,如:before, ::before

作用

  • :before:after:在CSS2.1及之前版本中,这两个伪类用于在元素的内容前后插入内容。它们可以用于添加装饰性的内容,如图标、引号等。
  • ::before::after:在CSS3及之后版本中,这两个伪元素用于同样的目的,但语法上更明确地表示它们是伪元素。它们允许你插入内容并为其添加样式,而不会影响文档的结构。

实际使用

在实际开发中,为了兼容性,很多开发者会同时使用单冒号和双冒号语法,如:

.element:before, .element::before {
  content: ">";
  /* 其他样式 */
}
.element:after, .element::after {
  content: "<";
  /* 其他样式 */
}

这样做的目的是确保在所有浏览器中都能正常工作,包括那些只支持单冒号语法的旧版浏览器。

总结

  • 单冒号:早期语法,用于伪类和伪元素,但存在语义不明确的问题。
  • 双冒号:现代语法,专门用于伪元素,语义更明确,但需要考虑兼容性。 在使用时,可以根据项目的兼容性要求选择使用单冒号、双冒号或两者同时使用。随着浏览器的更新和升级,双冒号语法将逐渐成为主流。

13. 树转数组

在TypeScript中,将树形结构转换为数组通常涉及到遍历树的所有节点。以下是一个简单的示例,展示了如何将一个简单的树形结构转换为数组。 假设我们有一个树形结构,每个节点可能有多个子节点,节点结构如下:

interface TreeNode {
  id: number;
  name: string;
  children?: TreeNode[];
}

我们需要一个函数来遍历这个树,并将每个节点放入一个数组中。这里我们使用递归的方式来实现:

function treeToArray(node: TreeNode, result: TreeNode[] = []): TreeNode[] {
  // 将当前节点添加到结果数组中
  result.push(node);
  // 如果当前节点有子节点,递归遍历每个子节点
  if (node.children && node.children.length > 0) {
    node.children.forEach(child => treeToArray(child, result));
  }
  return result;
}
// 示例树形结构
const tree: TreeNode = {
  id: 1,
  name: 'Root',
  children: [
    {
      id: 2,
      name: 'Child 1',
      children: [
        {
          id: 4,
          name: 'Grandchild 1'
        },
        {
          id: 5,
          name: 'Grandchild 2'
        }
      ]
    },
    {
      id: 3,
      name: 'Child 2'
    }
  ]
};
// 将树转换为数组
const array = treeToArray(tree);
// 输出结果
console.log(array);

在这个例子中,treeToArray函数接受一个TreeNode和一个结果数组result(默认为空数组)。函数将当前节点添加到结果数组中,然后检查是否有子节点,如果有,则对每个子节点递归调用treeToArray函数。 最终,array将包含树中的所有节点,以从上到下、从左到右的顺序。 请注意,这个实现假设树的结构是简单的,并且每个节点最多有一个children属性,该属性是一个TreeNode数组。如果树的结构更复杂,或者你需要特定的遍历顺序,你可能需要调整这个函数以满足你的需求。

14. 请实现下面的 sum 方法

当然,我可以帮您实现一个 sum 方法。不过,为了更准确地满足您的需求,我需要知道您希望 sum 方法具备哪些功能。例如,您希望它能够求一组数字的和,还是两个数字的和?您希望它能够处理什么类型的输入(如数字数组、可迭代对象等)? 假设您希望实现一个简单的 sum 方法,它接受一个数字数组作为参数,并返回这些数字的总和。以下是一个使用 TypeScript 实现的示例:

function sum(numbers: number[]): number {
  return numbers.reduce((accumulator, current) => accumulator + current, 0);
}
// 示例使用
const numbers = [1, 2, 3, 4, 5];
const total = sum(numbers);
console.log(total); // 输出:15

在这个实现中,sum 方法使用 Array.prototype.reduce 方法来遍历数字数组,并将它们累加起来。reduce 方法的第一个参数是一个回调函数,它接收两个参数:accumulator(累加器)和 current(当前值)。第二个参数是累加器的初始值,这里我们设置为 0。 如果您有其他特定的需求或功能,请告诉我,我会根据您的需求进行调整。

15. 请实现下面的 myAll 方法

myAll 方法听起来像是模拟 Promise.all 的行为,即等待所有给定的 promises 完成(每个 promise 都成功解决),或者如果任何一个 promise 失败,就立即返回失败。 以下是一个使用 TypeScript 实现的 myAll 方法:

type PromiseArray<T> = Array<Promise<T>>;
function myAll<T>(promises: PromiseArray<T>): Promise<T[]> {
  return new Promise((resolve, reject) => {
    const results: T[] = [];
    let completedPromises = 0;
    for (let i = 0; i < promises.length; i++) {
      promises[i]
        .then(result => {
          results[i] = result;
          completedPromises++;
          if (completedPromises === promises.length) {
            resolve(results);
          }
        })
        .catch(error => {
          reject(error);
        });
    }
    if (promises.length === 0) {
      resolve(results);
    }
  });
}
// 示例使用
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});
myAll([promise1, promise2, promise3]).then(values => {
  console.log(values); // 输出:[3, 42, "foo"]
});

在这个实现中,myAll 方法接受一个 promise 数组作为参数,并返回一个新的 promise。这个新的 promise 会在所有给定的 promises 成功解决时解决,返回一个包含所有 promise 结果的数组。如果任何一个 promise 失败,新的 promise 会立即被拒绝,并返回那个失败的原因。 请注意,这个实现假设输入数组中的每个元素都是 promise 或者是 promise-like 的值(即具有 then 方法)。如果输入数组为空,myAll 会立即解决,返回一个空数组。 这个实现是简化的,没有处理一些边缘情况,例如输入数组中包含非 promise 值的情况。在实际应用中,您可能需要进一步处理这些情况以确保 robustness。

16. 请实现下面的 sleep 方法

sleep 方法通常用于将代码的执行延迟特定的时间。在 JavaScript 中,这通常是通过 setTimeout 函数实现的。以下是一个使用 TypeScript 实现的 sleep 方法,它返回一个 Promise,该 Promise 在指定的时间后解决:

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}
// 示例使用
async function example() {
  console.log('开始等待');
  await sleep(2000); // 等待2000毫秒
  console.log('等待结束');
}
example();

在这个实现中,sleep 函数接受一个参数 ms,表示要延迟的毫秒数。函数内部创建了一个新的 Promise,并使用 setTimeout 来调用 resolve 函数,从而在指定的时间后解决这个 Promise。 你可以通过 await 关键字在异步函数中使用 sleep 来暂停执行,直到 Promise 解决。这样可以使异步代码的流程更加清晰和易于理解。 请注意,sleep 函数本身不会阻塞事件循环,因为它返回的是一个 Promise,而 await 会在等待时允许其他操作继续执行。这是异步编程的一个优点,可以避免阻塞主线程。

17. 请实现下面的 product 方法

在 TypeScript 中实现一个 product 方法,该方法可以接受一个数字数组作为参数,并返回这些数字的乘积。以下是一个简单的实现:

function product(nums: number[]): number {
  return nums.reduce((acc, current) => acc * current, 1);
}
// 示例使用
const numbers = [1, 2, 3, 4, 5];
const result = product(numbers);
console.log(result); // 输出: 120

在这个实现中,product 函数接受一个类型为 number[] 的参数 nums,表示一个数字数组。函数使用 reduce 方法来遍历数组,并将每个元素与累积值相乘。reduce 方法的第二个参数 1 是初始值,确保乘积从 1 开始。 这个实现假设数组中至少有一个数字,且不包含 0,因为任何数乘以 0 结果都是 0。如果需要处理空数组或包含 0 的情况,可以添加相应的逻辑:

function product(nums: number[]): number {
  if (nums.length === 0) {
    throw new Error('Array is empty');
  }
  return nums.reduce((acc, current) => acc * current, 1);
}
// 示例使用
try {
  const numbers = [1, 2, 3, 4, 5];
  const result = product(numbers);
  console.log(result); // 输出: 120
} catch (error) {
  console.error(error);
}

在这个改进的实现中,如果输入数组为空,product 函数会抛出一个错误。这样可以避免在空数组上调用 reduce 方法时返回初始值 1,这可能不是预期的行为。 请注意,这个实现没有处理数组中包含 NaNInfinity-Infinity 的情况。如果需要处理这些特殊情况,可以进一步添加检查逻辑。

18. 请实现下面的 treePath 方法

为了实现一个 treePath 方法,我们首先需要明确该方法的目的。假设 treePath 方法的目的是找到从树的根节点到指定节点之间的路径。这里我们假设树是由节点组成的,每个节点有一个唯一的标识符(比如 id)和一个可能的 children 数组,表示其子节点。 以下是一个简单的 TypeScript 实现,其中定义了一个 TreeNode 接口,以及一个 findPath 函数来找到路径:

interface TreeNode {
  id: number;
  children?: TreeNode[];
}
function findPath(root: TreeNode, targetId: number): number[] {
  // 递归函数来寻找路径
  function findNode(node: TreeNode, path: number[]): number[] | null {
    // 将当前节点id添加到路径中
    path.push(node.id);
    // 如果找到目标节点,返回路径
    if (node.id === targetId) {
      return path;
    }
    // 如果有子节点,递归搜索每个子节点
    if (node.children) {
      for (const child of node.children) {
        const result = findNode(child, [...path]); // 使用展开运算符复制路径数组
        if (result) return result;
      }
    }
    // 如果没有找到,返回null
    return null;
  }
  // 从根节点开始寻找
  const path = findNode(root, []);
  return path || []; // 如果没有找到路径,返回空数组
}
// 示例使用
const tree: TreeNode = {
  id: 1,
  children: [
    {
      id: 2,
      children: [
        { id: 4 },
        { id: 5 }
      ]
    },
    {
      id: 3,
      children: [
        { id: 6 },
        { id: 7 }
      ]
    }
  ]
};
const targetId = 5;
const path = findPath(tree, targetId);
console.log(path); // 输出: [1, 2, 5]

在这个实现中:

  • TreeNode 接口定义了树节点的结构,包含一个 id 和一个可选的 children 数组。
  • findPath 函数接受树的根节点和目标节点的 id,返回从根节点到目标节点的路径。
  • findNode 是一个递归辅助函数,它遍历树并寻找目标节点。如果找到,它返回路径数组;如果未找到,返回 null
  • findNode 函数中,我们使用展开运算符 (...) 来复制路径数组,以确保每个路径都是独立的,避免引用同一数组实例。 请注意,这个实现假设每个节点的 id 是唯一的,并且树不是无限的。如果树非常大,递归可能会导致性能问题或栈溢出。此外,如果需要处理更复杂的树结构或不同的节点属性,可能需要调整 TreeNode 接口和 findPath 函数的实现。

19. 请补充 objToArray 函数

为了补充 objToArray 函数的实现,我们需要明确该函数的目的是将对象转换为数组。假设我们希望将一个对象转换为一个数组,其中每个元素是一个包含键和值的对象。以下是一个简单的 TypeScript 实现:

interface ObjEntry<K, V> {
  key: K;
  value: V;
}
function objToArray<T extends Record<keyof any, any>>(obj: T): ObjEntry<keyof T, T[keyof T]>[] {
  return Object.keys(obj).map(key => ({
    key: key,
    value: obj[key]
  }));
}
// 示例使用
const obj = {
  a: 1,
  b: 2,
  c: 3
};
const array = objToArray(obj);
console.log(array);
// 输出: [{ key: 'a', value: 1 }, { key: 'b', value: 2 }, { key: 'c', value: 3 }]

在这个实现中:

  • ObjEntry 接口定义了数组中每个元素的结构,包含 keyvalue 属性。
  • objToArray 函数接受一个对象 T,该对象具有任意键和对应值。函数返回一个 ObjEntry 数组。
  • 使用 Object.keys(obj) 获取对象的所有键,然后使用 map 方法将每个键转换为一个 ObjEntry 对象。 这个实现假设对象的键是字符串或符号(因为 Object.keys 只返回这些类型的键)。如果对象的键有其他类型,或者你需要更复杂的转换逻辑,可能需要调整这个实现。 请注意,这个实现使用了泛型 T extends Record<keyof any, any> 来允许传入任意类型的对象,并且能够保留键和值的类型信息。这样,返回的数组中的 keyvalue 将具有与输入对象相同的类型。

20. 如何定义一个数组,它的元素可能是字符串类型,也可能是数值类型?

在 TypeScript 中,可以使用联合类型(Union Types)来定义一个数组,其元素可以是字符串类型或数值类型。以下是如何定义这样的数组:

// 定义一个类型,可以是字符串或数值
type StringOrNumber = string | number;
// 定义一个数组,其元素是字符串或数值
let array: StringOrNumber[];
// 示例使用
array = [1, 'two', 3, 'four']; // 正确
array = [1, 2, 3, 4]; // 正确
array = ['one', 'two', 'three', 'four']; // 正确
// array = [true, 'two', 3, 'four']; // 错误,布尔值不是字符串或数值

在这个例子中:

  • StringOrNumber 类型是一个联合类型,表示值可以是 stringnumber
  • array 是一个数组,其元素类型为 StringOrNumber,即数组中的每个元素都可以是字符串或数值。 这样定义后,array 可以包含任意数量的字符串或数值元素,但不能包含其他类型的元素,如布尔值或对象。TypeScript 的类型系统会在编译时检查这些约束。

21. js函数有哪几种声明方式?有什么区别?

在 JavaScript 中,函数的声明方式主要有以下几种:

  1. 函数声明(Function Declaration)
    function functionName(parameters) {
        // 函数体
    }
    
    特点:
    • 使用 function 关键字。
    • 函数声明提升(Hoisting):在代码执行之前,函数声明会被提升到作用域的顶部,因此可以在声明之前调用。
    • 有一个特定的名称。
  2. 函数表达式(Function Expression)
    var functionName = function(parameters) {
        // 函数体
    };
    
    特点:
    • 将函数赋值给一个变量。
    • 没有函数提升:必须在表达式定义之后才能调用。
    • 可以是匿名函数(没有名称)或命名函数。
  3. 箭头函数(Arrow Function)
    const functionName = (parameters) => {
        // 函数体
    };
    
    特点:
    • ES6 引入的语法,更加简洁。
    • 不绑定自己的 this,会捕获其所在上下文的 this 值。
    • 不适合用作构造函数,不能使用 new
    • 没有原型属性(prototype)。
  4. 立即执行函数表达式(Immediately Invoked Function Expression,IIFE)
    (function() {
        // 函数体
    })();
    
    特点:
    • 定义后立即执行,通常用于创建一个独立的作用域。
    • 可以避免污染全局作用域。
  5. 函数构造器(Function Constructor)
    var functionName = new Function('parameters', 'functionBody');
    
    特点:
    • 不常用,因为性能较差且不安全(可以执行任意代码)。
    • 使用 new 关键字和 Function 构造函数。 区别
  • 函数提升:函数声明有提升,而函数表达式和箭头函数没有。
  • this 绑定:箭头函数不绑定自己的 this,而函数声明和表达式会绑定。
  • 语法:箭头函数提供了更简洁的语法,特别适合用于回调函数和函数式编程。
  • 使用场景:立即执行函数表达式通常用于创建独立作用域,避免全局变量污染;函数构造器很少使用,因为存在性能和安全性问题。
  • 构造函数:箭头函数不能作为构造函数使用,而函数声明和表达式可以。 选择哪种函数声明方式取决于具体的需求和场景。例如,如果需要函数提升,可以选择函数声明;如果需要简洁的语法和不绑定 this 的特性,可以选择箭头函数。

22. 说说你对“立即执行函数”的理解

立即执行函数(Immediately Invoked Function Expression,简称IIFE)是一种在定义后立即执行的JavaScript函数。它通常用于创建一个独立的作用域,以避免污染全局作用域,并且可以封装一些私有变量。 我对立即执行函数的理解如下

  1. 基本结构: 立即执行函数通常由一个函数定义和一个立即调用的括号组成。其基本结构有两种形式:
    (function() {
        // 函数体
    })();
    (function() {
        // 函数体
    }());
    
    第一种形式中,函数定义后直接跟一个括号表示调用;第二种形式中,函数定义和调用括号分开,但效果相同。
  2. 创建独立作用域: 立即执行函数的主要用途之一是创建一个独立的作用域。在这个作用域内定义的变量和函数不会影响到外部作用域,从而避免了全局变量的污染。
  3. 封装私有变量: 由于立即执行函数创建了一个独立的作用域,因此可以在这个作用域内定义私有变量。这些变量只能在函数内部访问,外部无法访问,实现了一定程度的封装。
  4. 避免变量提升问题: 在JavaScript中,变量声明会被提升到作用域的顶部。使用立即执行函数可以避免这种变量提升带来的问题,因为函数内部的变量声明不会影响到外部作用域。
  5. 模块化: 在ES6模块系统出现之前,立即执行函数常用于实现模块化。通过立即执行函数可以封装模块的代码,并返回一个对象或函数,从而实现模块的导出和导入。
  6. 初始化代码: 立即执行函数也常用于执行一些初始化代码,这些代码只需要执行一次,并且不需要在后续的代码中再次调用。 示例
(function() {
    var privateVariable = 'I am private';
    function privateFunction() {
        console.log('I am also private');
    }
    // 可以在函数内部使用这些私有变量和函数
    console.log(privateVariable);
    privateFunction();
})();
// 外部无法访问privateVariable和privateFunction
console.log(privateVariable); // 报错:privateVariable未定义
privateFunction(); // 报错:privateFunction未定义

在这个示例中,privateVariableprivateFunction 被封装在立即执行函数的独立作用域内,外部无法访问,从而实现了封装和避免全局污染的目的。 总之,立即执行函数是JavaScript中一种非常有用的模式,它提供了一种简单而有效的方式来管理作用域和封装代码。

23. react 中怎么捕获异常?

在React中,捕获异常通常有几种方法:

  1. 使用try...catch语句: 在组件的函数或方法中,你可以使用try...catch语句来捕获同步代码中发生的异常。
    class MyComponent extends React.Component {
        handleClick = () => {
            try {
                // 可能抛出异常的代码
            } catch (error) {
                console.error('Caught an error:', error);
            }
        };
        render() {
            return <button onClick={this.handleClick}>Click me</button>;
        }
    }
    
  2. 使用错误边界(Error Boundaries): 错误边界是一种React组件,可以捕获其子组件树中发生的JavaScript错误,并记录这些错误,同时展示一个备用的UI。错误边界只能捕获其子组件中的错误,不能捕获事件处理器中的错误。
    class ErrorBoundary extends React.Component {
        constructor(props) {
            super(props);
            this.state = { hasError: false };
        }
        static getDerivedStateFromError(error) {
            // 更新state,使下一次渲染能够显示备用UI
            return { hasError: true };
        }
        componentDidCatch(error, errorInfo) {
            // 你也可以将错误日志上报给服务器
            logErrorToMyService(error, errorInfo);
        }
        render() {
            if (this.state.hasError) {
                // 你可以渲染任何自定义的备用UI
                return <h1>Something went wrong.</h1>;
            }
            return this.props.children; 
        }
    }
    // 使用错误边界
    function MyComponent() {
        return (
            <ErrorBoundary>
                <MyWidget />
            </ErrorBoundary>
        );
    }
    
  3. 使用componentDidCatch生命周期方法: 在类组件中,你可以使用componentDidCatch生命周期方法来捕获组件中的异常。这个方法会在组件渲染过程中以及其子组件的渲染过程中捕获到错误时被调用。
    class MyComponent extends React.Component {
        componentDidCatch(error, errorInfo) {
            // 处理错误,例如记录错误日志
            console.error('Component did catch:', error, errorInfo);
        }
        render() {
            // ...
        }
    }
    
  4. 使用useEffecttry...catch捕获异步异常: 在函数组件中,如果你需要捕获异步操作中的异常,可以在useEffect钩子中使用try...catch
    import React, { useEffect } from 'react';
    function MyComponent() {
        useEffect(() => {
            async function fetchData() {
                try {
                    // 异步操作,可能抛出异常
                } catch (error) {
                    console.error('Caught an async error:', error);
                }
            }
            fetchData();
        }, []); // 空依赖数组表示这个effect只在组件挂载时运行一次
        return <div>My Component</div>;
    }
    
  5. 全局异常处理: 对于未捕获的异常,你可以使用window对象的onerror事件来捕获。
    window.onerror = function (message, source, lineno, colno, error) {
        console.error('Global error caught:', { message, source, lineno, colno, error });
        return true; // 返回true可以阻止默认的处理机制,比如浏览器控制台的错误提示
    };
    

请注意,错误边界不能捕获以下类型的错误:

  • 事件处理器中的错误
  • 异步代码中的错误(例如setTimeout或请求回调中的错误)
  • 服务端渲染中的错误
  • 错误边界自身的错误 对于这些情况,你需要使用其他错误处理机制,如try...catch或全局的window.onerror

24. vue 中怎么捕获异常?

在 Vue 3 中,捕获异常的方式与 Vue 2 类似,但也有一些新的特性和改进。以下是一些常见的捕获异常的方法:

  1. 使用 try...catch 语句: 在组件的 methods、computed 属性或生命周期钩子中,你可以使用 try...catch 语句来捕获同步代码中发生的异常。
    <script setup>
    import { ref } from 'vue';
    const count = ref(0);
    function increment() {
        try {
            // 可能抛出异常的代码
            count.value++;
        } catch (error) {
            console.error('Caught an error:', error);
        }
    }
    </script>
    <template>
        <button @click="increment">Increment</button>
    </template>
    
  2. 使用生命周期钩子: Vue 3 提供了 onErrorCaptured 生命周期钩子,可以用来捕获子组件中的异常。
    import { defineComponent, onErrorCaptured } from 'vue';
    export default defineComponent({
        setup(props, { slots }) {
            onErrorCaptured((error, instance, info) => {
                console.error('Error captured:', error, instance, info);
                // 返回 true 表示阻止错误继续向上传递
                return true;
            });
            return () => slots.default();
        }
    });
    
  3. 使用 watchwatchEffect 来捕获响应式引用中的异常: 当你使用 Vue 3 的 Composition API 时,可以使用 watchwatchEffect 来监视响应式引用,并在回调函数中使用 try...catch 来捕获异常。
    import { ref, watchEffect } from 'vue';
    const data = ref(null);
    watchEffect(() => {
        try {
            // 响应式引用中的代码,可能抛出异常
            console.log(data.value);
        } catch (error) {
            console.error('Caught an error in watchEffect:', error);
        }
    });
    
  4. 全局异常处理: 对于未捕获的异常,你可以使用全局的 errorHandler 钩子来捕获。
    import { createApp } from 'vue';
    const app = createApp({ /* 根组件 */ });
    app.config.errorHandler = (err, vm, info) => {
        console.error('Global error handler:', err, vm, info);
        // 处理错误,例如发送错误报告到服务器
    };
    app.mount('#app');
    
  5. 使用 asyncawait 捕获异步异常: 在异步函数中,你可以使用 asyncawait 结合 try...catch 来捕获异常。
    import { ref } from 'vue';
    const data = ref(null);
    async function fetchData() {
        try {
            const response = await fetch('/api/data');
            data.value = await response.json();
        } catch (error) {
            console.error('Caught an async error:', error);
        }
    }
    fetchData();
    
  6. 使用 v-on 指令的 .catch 修饰符: 在 Vue 3 中,你可以使用 v-on 指令的 .catch 修饰符来捕获事件处理器中的异常。
    <template>
        <button @click.catch="handleClick">Click me</button>
    </template>
    <script setup>
    function handleClick() {
        // 可能抛出异常的代码
    }
    </script>
    

通过这些方法,你可以在 Vue 3 应用中有效地捕获和处理异常。选择合适的方法取决于你的具体需求和代码结构。

25. 环形链表

在 TypeScript 中实现环形链表,首先需要定义链表节点的结构,然后实现环形链表的基本操作,如插入节点、删除节点和检测链表是否为环形等。 以下是一个简单的环形链表的 TypeScript 实现:

class ListNode {
    public val: number;
    public next: ListNode | null;
    constructor(val: number = 0, next: ListNode | null = null) {
        this.val = val;
        this.next = next;
    }
}
class CircularLinkedList {
    private head: ListNode | null;
    private tail: ListNode | null;
    constructor() {
        this.head = null;
        this.tail = null;
    }
    // 插入一个新节点到环形链表
    public insert(val: number): void {
        const newNode = new ListNode(val);
        if (!this.head) {
            this.head = newNode;
            this.tail = newNode;
            newNode.next = newNode; // 形成环形
        } else {
            newNode.next = this.head;
            this.tail!.next = newNode;
            this.tail = newNode;
        }
    }
    // 删除一个节点从环形链表
    public delete(val: number): void {
        if (!this.head) {
            return;
        }
        let current = this.head;
        let prev: ListNode | null = null;
        do {
            if (current.val === val) {
                if (prev) {
                    prev.next = current.next;
                    if (current === this.tail) {
                        this.tail = prev;
                    }
                } else {
                    // 删除的是头节点
                    if (this.head === this.tail) {
                        this.head = null;
                        this.tail = null;
                    } else {
                        this.tail!.next = this.head.next;
                        this.head = this.head.next;
                    }
                }
                return;
            }
            prev = current;
            current = current.next!;
        } while (current !== this.head);
    }
    // 检测链表是否为环形
    public isCircular(): boolean {
        if (!this.head) {
            return false;
        }
        let slow = this.head;
        let fast = this.head;
        while (fast && fast.next) {
            slow = slow.next!;
            fast = fast.next.next!;
            if (slow === fast) {
                return true;
            }
        }
        return false;
    }
    // 打印环形链表
    public print(): void {
        if (!this.head) {
            console.log('List is empty');
            return;
        }
        let current = this.head;
        let result = [];
        do {
            result.push(current.val);
            current = current.next!;
        } while (current !== this.head);
        console.log(result.join(' -> '));
    }
}
// 使用示例
const circularList = new CircularLinkedList();
circularList.insert(1);
circularList.insert(2);
circularList.insert(3);
circularList.print(); // 应该输出: 1 -> 2 -> 3 -> 1 ...
circularList.delete(2);
circularList.print(); // 应该输出: 1 -> 3 -> 1 ...
console.log(circularList.isCircular()); // 应该输出: true

在这个实现中,ListNode 类代表链表的节点,包含一个值 val 和一个指向下一个节点的引用 nextCircularLinkedList 类实现了环形链表的基本操作,包括插入节点、删除节点、检测是否为环形和打印链表。 请注意,这个实现是一个简单的示例,没有处理所有可能的边缘情况,例如插入或删除节点时可能需要更新头节点或尾节点。在实际应用中,你可能需要根据具体需求进一步优化和扩展这个实现。

26. 使用 redux 有哪些原则?

使用 Redux 时,应遵循以下原则以确保应用的维护性和可预测性:

  1. 单一事实来源(Single Source of Truth)
    • 应用的所有状态都存储在一个单一的存储对象(store)中。这个对象就是应用状态的唯一真实来源。
  2. 状态是只读的(State is Read-only)
    • 不能直接修改状态。改变状态的唯一方式是触发一个动作(action),这个动作描述了发生了什么。
  3. 使用纯函数执行修改(Changes are made with pure functions)
    • 为了描述如何基于当前状态和动作来改变应用的状态,你需要编写reducers。Reducers 是纯函数,它们接受先前的状态和动作作为参数,并返回新的状态。
  4. 严格的动作(Strict Typing of Actions)
    • 动作应该是普通对象,并且必须有一个 type 属性来表示将要执行的动作类型。建议使用字符串常量来定义动作类型。
  5. 避免突变(Avoid Mutations)
    • 在 Redux 中,应该避免直接修改对象和数组。使用新对象和新数组来表示状态的改变。
  6. 保持 Reducers 纯净(Keep Reducers Pure)
    • Reducers 应该没有副作用。它们不应该修改传入的参数,也不应该执行异步操作或调用非纯函数。
  7. 分离关注点(Separation of Concerns)
    • Redux 应该只用于管理应用的状态。将 UI 组件、业务逻辑和状态管理分开。
  8. 使用中间件处理副作用(Use Middleware for Side Effects)
    • 如果需要处理异步逻辑或副作用,应该使用 Redux 中间件,如 redux-thunkredux-saga
  9. 组件不应该直接修改状态(Components Should Not Modify State Directly)
    • 组件应该通过 dispatching actions 来请求状态的改变,而不是直接修改状态。
  10. 保持状态的扁平化(Keep State Flat)
    • 尽可能将状态结构保持扁平,这样可以更容易地管理和维护状态。
  11. 代码组织(Code Organization)
    • 合理组织代码,例如将动作类型、动作创建函数、reducers 和存储配置放在不同的文件中。 遵循这些原则可以帮助你构建出结构清晰、易于维护和扩展的 Redux 应用。不过,随着应用规模的增加,可能需要根据具体情况进行适当的调整和优化。