前端面试汇总

3次阅读
没有评论

React

useState 是如何实现的?

  • 基于闭包:在函数组件外部维护状态存储。
  • 基于链表:通过一个稳定的调用顺序,将多个 Hook 与它们各自的状态正确关联。
  • 触发更新setState 会更新状态并通知 React 调度一次新的渲染。
  • 渲染重置:每次组件渲染时,内部指针都会重置,从而按顺序“消费”Hook。

setState 的实现原理和处理同步、异步

setState 的核心原理可以概括为:它不是立即同步更新状态的,而是将一个更新请求放入一个队列中,然后在一定时机下进行批量合并和更新,最后触发组件的重新渲染。

  • 开启一个批量更新事务: 当 React 开始执行一个事件处理函数时,它会先“打开”一个批量更新的开关(batchingUpdates)。
  • 入队: 每次调用 setState,都不是直接修改 this.state,而是将这次更新(包含新的状态值或更新函数)放入一个 待处理的更新队列(pending queue) 中。
  • 合并: 在同一个事件循环中,如果多次调用 setState,React 会将这些更新合并(Object.assign)成一个更新。
  • 出队与更新: 当事件处理函数执行完毕后,React 会“关闭”批量更新开关,然后一次性处理队列中的所有更新,计算出最终的新状态,并只进行一次重新渲染。
场景 表现 原理
React 事件、生命周期 异步、批量 React 通过“批量更新事务”机制,将更新收集到队列中,最后统一处理。
setTimeout、Promise、原生事件 同步 脱离了 React 的事务控制,每次  setState  都会立即触发更新。

为什么 usestate 返回的是数组而不是对象?

  • 自由命名(最主要的原因)
// ✅ 数组 - 可以自由命名
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);

// ❌ 如果是对象 - 必须使用固定的属性名
const {state: count, setState: setCount} = useState(0);
const {state: name, setState: setName} = useState('');
  • 代码简洁性
  • 易于组合和自定义 Hook
  • 一致性: 符合 React 模式

如何让 useEffect 支持 async/await?

  • 在内部定义一个异步函数
  • 在内部定义一个匿名的 IIFE 函数
  • 使用第三方 Hook:
import React, {useState} from 'react';
import useAsyncEffect from 'use-async-effect';

const MyComponent = () => {useAsyncEffect(async () => {const response = await fetch('https://api.example.com/data');
    const result = await response.json();
    setData(result);
  }, []); // 依赖数组

  return <div>{/* ... */}</div>;
};

react 中懒加载的实现原理是什么?

动态导入(Dynamic Import)

// 静态导入(打包时会包含整个模块)import MyComponent from './MyComponent';

// 动态导入(返回一个 Promise)import('./MyComponent').then(module => {
  const MyComponent = module.default;
  // 使用组件
});

React.lazy 的实现原理

const LazyComponent = React.lazy(() => import('./MyComponent'));
// 简化版的 React.lazy 实现:function lazy(loader) {
  let component = null;
  let status = 'pending'; // 'pending', 'success', 'error'
  let result = null;

  return function LazyComponent(props) {
    // 首次加载
    if (status === 'pending') {result = loader();
      result.then(module => {
          status = 'success';
          component = module.default; // 获取默认导出
        },
        error => {
          status = 'error';
          result = error;
        }
      );
    }

    // 处理不同状态
    if (status === 'pending') {throw result; // 抛出 Promise,Suspense 会捕获} else if (status === 'error') {throw result; // 抛出错误,Error Boundary 会捕获} else if (status === 'success') {return React.createElement(component, props);
    }
  };
}

Suspense 的工作原理:Suspense 组件通过 捕获子组件抛出的 Promise 来实现加载状态的管理

function App() {
  return (<div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

React 中,fiber 是如何实现时间切片的?

Fiber 实现时间切片的核心是:将渲染任务分解成小的工作单元,在每个工作单元之间检查是否应该让出主线程给更高优先级的任务

  • 可中断的循环机制 :Fiber 不再使用递归遍历,而是改用 循环遍历,通过 while 循环处理每个 Fiber 节点,在循环中检查是否应该中断
  • 让出控制权的检查
    • 是否超时:判断当前时间片(通常 5ms)是否用完
    • 是否有更高优先级任务:如用户输入、动画等需要立即响应
  • 基于优先级的调度
    • 同步优先级:不可中断(如输入框输入)
    • 高优先级:用户交互、动画
    • 普通优先级:数据更新
    • 低优先级:非紧急任务
  • 增量式单元工作
    • 处理一个节点后检查时间
    • 保存当前进度(workInProgress 指针)
    • 下次从断点继续执行

Vue

Vue 中 Treeshaking 特性对比

特性维度 Vue 2 🟢 Vue 3 🔷
模块化设计 全局 API,整体打包 基于 ES Module 的模块化设计,API 可独立引入
打包结果 即使只使用部分功能,也需引入整个 Vue 库 仅打包你实际导入并使用的 API
体积控制 难以优化,包体积相对固定 精细可控,有效减小最终打包大小

在 Vue 3 中,你可以只引入需要的 nextTick 函数,未使用的 API 将被移除。

import {nextTick} from 'vue'; // 按需引入特定 API
nextTick(() => { // 直接使用
  // 一些和 DOM 有关的东西
});

vue3 相比较于 vue2,在编译阶段有哪些改进?

化维度 Vue 2 的情况 Vue 3 的改进
静态提升 无明确优化,每次渲染需重新创建静态节点 静态节点 提升到渲染函数外 ,或将 静态属性 提升为常量,减少重复创建开销。
Patch Flag 差异化比对时,需要全量对比虚拟节点的属性和内容 为动态节点添加Patch Flag  标记,在更新时仅比对动态部分,大幅减少比对范围。
Tree-shaking 内置特性(如过渡组件)无法被摇树优化 模块化 的设计 ,配合 ES 模块,使打包工具能 移除未使用的代码,减小打包体积
缓存处理 无明确优化 缓存事件处理函数(cacheHandlers),避免不必要的更新。

说说 Vue 页面渲染流程

  • 模板 -(编译)
    • 解析 :将模板字符串解析成一个 抽象语法树
    • 转换 :对 AST 进行优化和处理,例如 Vue 3 的 静态提升 Patch Flag标记。
      • 静态节点提升:识别出永远不会改变的静态节点,次重渲染时就不再重新创建它们,极大优化了性能。
      • 指令处理:解析和处理 Vue 的指令。例如,将 v-if/v-for 转换为对应的条件渲染和循环渲染逻辑。
      • 表达式解析:将文本中的 Mustache 插值(如 {{message}})解析为 JavaScript 表达式(如 _s(message))。
    • 生成 :将优化后的 AST 转换为一个字符串,该字符串是渲染函数的代码。最终,这个字符串通过 new Function() 变成一个可执行的  渲染函数
  • 渲染函数 -(执行)
  • 虚拟 DOM -(比对)
    • 通过 Diff 算法 (深度优先),计算出最小量的变更,将这些变更 应用到真实 DOM 上
  • 真实 DOM

文字描述 vue 是如何识别和解析指令的?

  • 编译时:模板解析编译成 AST,遍历 AST 树遇到一个属性是以 v- 开头(或 @: 等简写),它就会将这个属性 识别为一个指令。转换 / 优化对于不同类型的指令,编译器会进行不同的处理。
  • 运行时 :执行与响应,渲染:执行 render 函数,生成带指令信息的 VNode。打补丁:对比新旧 VNode,调用指令的 钩子函数 更新:在钩子函数中操作真实 DOM,实现指令的最终效果。

Vue 有了数据响应式,为何还要 diff?

  • 数据响应式  是 “侦察兵”,负责发现 哪里发生了改变 ,并通知到具体的 部队(组件)
  • Diff 算法  是 “作战参谋”(知道更新的颗粒度更细),负责为接到通知的部队制定 最高效、最精准的作战方案(如何更新 DOM)

vue3 的 ref 为什么要使用.value 方式获取,而 template 模板不需要

特性维度 在 JavaScript/TypeScript 中 在 Vue 模板中 主要原因说明
访问方式 必须使用  .value 自动 ” 解包 ”,无需  .value Vue 编译器在模板编译阶段自动处理了  .value  的访问
数据响应式原理 通过  RefImpl  类的  get/set  实现 同样基于  RefImpl,但访问被自动代理 ref  返回一个  RefImpl  响应式对象,其值存储在  _value  属性,需通过  .value  访问
设计目的 明确区分响应式对象和普通值 提升模板简洁性和开发体验 模板中自动解包减少了冗余代码,使模板更专注于结构

为什么需要 .value

  • 对于 基本类型(如字符串、数字),在 JavaScript 中,它们是按值传递的,无法直接附加追踪逻辑。ref 通过将其包装成对象,使得 Vue 能够利用 get 和 set 拦截(对于对象类型,ref 内部会使用 reactive 处理)来管理响应式
  • 明确标识响应式变量:在脚本中使用 .value,可以让你清晰地知道这是一个需要 Vue 追踪其变化的响应式变量,有助于代码的维护和阅读

模板中的自动 ” 解包 ”

  • 编译时的魔法 :当 Vue 编译器处理模板时,如果它识别到某个变量是一个 ref,它会 自动帮你加上 .value 来访问实际数据。你写的 {{count}} 在编译后实际上相当于 {{count.value}}
  • 保持模板简洁:这样做的目的是让模板保持简洁易读,毕竟在模板中频繁地写 .value 会比较繁琐

Vue2 和 Vue3 的 Diff 区别

特性 Vue 2 Vue 3
算法类型 双端比较 基于最长递增子序列的双端比较
比较方式 头头、尾尾、头尾、尾头、key 映射 头头、尾尾、最长递增子序列
移动优化 相对简单 使用 LIS 最小化 DOM 移动
静态标记 有 PatchFlags 静态标记
块优化 有 Block Tree 概念
性能 O(n) 但移动较多 O(n) 移动更少

Vue3.0 的设计目标是什么? 做了哪些优化?

设计目标 核心优化 / 新特性 关键实现与说明
🚀 追求极致性能 基于  Proxy  的响应式系统 替换 Object.
defineProperty,性能更好,并能原生监听数组变化、动态添加属性等
编译时优化 PatchFlag  与 静态提升:在编译阶段对静态节点做标记或提升,减少运行时对比
Tree-shaking  支持 借助 ES Module,打包工具能移除未使用的代码,减小项目体积
🎯 提升代码组织与复用 Composition API 提供基于函数的 组合式 API,允许将相关逻辑组织在一起,显著提升复杂组件的可读性和逻辑复用性
📐 增强开发体验与能力 更好的  TypeScript  支持 源码使用 TypeScript 重写,提供完善的类型推断,开发体验更佳
新内置组件: – Fragments: 组件支持多个根节点
– Teleport: 将组件渲染到任意 DOM 节点
– Suspense: 处理异步组件加载状态
Monorepo  与模块化架构 核心模块拆解,更易于维护和按需使用

说说 Vue 中 css scoped 的原理

scoped 的实现原理可以概括为:通过给组件模板中的 HTML 元素和样式选择器添加唯一的属性标识,然后利用 CSS 属性选择器来确保样式只匹配到当前组件的元素

React 和 Vue 在技术层面有哪些区别?

特性 React Vue
本质 库,专注于视图层 渐进式框架,提供完整解决方案
视图语法 JSX (JavaScript in XML) 基于 HTML 的模板
数据响应式 手动触发(setState/useState),单向数据流 自动追踪(Proxy/defineProperty),可变状态
逻辑组合 Hooks (函数组件) Options API & Composition API
样式方案 无官方方案,社区多样(CSS-in-JS 等) 单文件组件,原生支持 Scoped CSS
类型支持 与 TypeScript 集成良好 Vue 3 用 TS 重写,对 TS 支持极佳
性能瓶颈 大数据量的响应式追踪 大型组件树的递归渲染
学习曲线 较陡峭,需理解更多概念 较平缓,文档友好,易于上手
哲学 给予开发者自由,信仰 JavaScript 提供结构和便利,降低开发门槛

Vue 的 keep-alive 原理是什么,在 react 是怎么实现同样效果的

keep-alive 的核心目标是 缓存组件实例,避免组件切换时的重复销毁和创建。在组件不活跃时保留其状态,在需要时重新激活。

  • keep-alive 被标记为 抽象组件(abstract: true),不会出现在父子组件链中,不参与正常的 DOM 渲染,只负责管理子组件的缓存逻辑,自身不渲染任何内容
  • 初始化阶段
    • created 钩子:初始化 cache 对象和 keys 数组
    • mounted 钩子:监听 include/exclude 参数变化,动态调整缓存
  • 组件首次渲染 → 检查缓存资格 → 生成缓存 key → 存入 cache
    • 检查组件名称 → 匹配 include/exclude 规则 → 判断是否需要缓存
  • 组件再次渲染 → 检查缓存 key → 命中缓存 → 复用实例 → 激活组件
    • 检查 cache[key]是否存在?
    • 存在(缓存命中):- 从 cache 中取出缓存的 VNode-> 更新 keys 队列(LRU 移动), 标记 VNode.data.keepAlive = true
  • 组件切换隐藏 → 停用组件 → 保持实例在缓存中
  • 缓存数量超限 → LRU 淘汰 → 销毁最久未使用的实例
  • 参数变化监听 → 动态调整缓存内容 → 清理不符合条件的缓存项
特性 Vue keep-alive React 实现方案
内置支持 ✅ 官方内置 ❌ 需要自行实现或使用第三方库(react-activation)
生命周期 ✅ activated/deactivated ⚠️ 需要手动处理
缓存策略 ✅ LRU 算法 ⚠️ 需要手动实现
TypeScript ✅ 完整支持 ✅ 良好支持
使用复杂度 ✅ 简单易用 ⚠️ 相对复杂
性能优化 ✅ 自动优化 ⚠️ 需要手动优化

Webpack

webpackloader 和 plugin 实现原理

特性 Loader Plugin
作用对象 单个文件 整个构建过程
执行时机 文件加载阶段 编译的各个生命周期
功能 文件转换 更广泛的任务
使用方式 配置在 module.rules 中 配置在 plugins 数组中

前端工程化的理解

前端工程化是 将软件工程的方法和原则系统性地应用到前端开发中,通过工具、流程、规范的结合,提升开发效率、保证代码质量、优化项目维护性的系统性工程。

webpack 的 module、bundle、chunk 分别指的是什么?

  • module: 在 Webpack 的世界里,一切皆模块 。一个模块就是你项目中的一个源代码文件,它可以通过importrequire语句引入其他模块。
  • Chunk 是 Webpack 在打包过程中的一个 中间概念。它是多个 Module 的集合,是 Webpack 根据特定规则(如代码分割)进行分组后形成的。
  • Bundle 是 Chunk 经过一系列处理(如压缩、混淆)和转换后,最终写入输出目录(output)的物理文件。它是用户最终在浏览器中加载的文件。

treeshaking 机制的原理是什么?

是一个用于移除 JavaScript 上下文中未引用代码(dead code)的优化技术。

  • 静态分析:利用 ES6 模块的静态结构,在构建时分析出完整的依赖关系。
    • ES6 模块(import/export)是静态的 :模块的依赖关系在代码 运行前 就可以通过静态分析确定。
    • CommonJS(require/module.exports)是动态的 :依赖关系在 运行时 才能确定,无法在构建阶段可靠地分析。
  • 标记追踪:从入口开始,标记所有被使用的导出。
  • 安全移除:在压缩阶段(主要由 Terser 完成),将未被标记的代码安全地从最终 Bundle 中删除。
  • 深度优化:通过 package.json 的 sideEffects 属性,实现更激进的 ” 模块级 ” 摇树,跳过整个未使用的纯模块。

如何提高 webpack 的构建速度?

优化方向 具体措施 主要效果 / 原理
📊 分析构建过程 使用  speed-measure-webpack-plugin 分析 Loader 和 Plugin 耗时  
使用  webpack-bundle-analyzer 可视化分析打包产物体积  
🔧 优化 Loader 与模块解析 使用  include/exclude  缩小 Loader 处理范围 避免转译  node_modules  等已处理文件  
使用  noParse  忽略无需解析的库 避免解析如 jQuery 等无依赖的库  
配置  resolve  选项(aliasextensionsmodules 加快模块查找和解析速度  
⚡ 利用缓存 使用  cache-loader  或  babel-loader?cacheDirectory 缓存 Loader 处理结果  
使用  hard-source-webpack-plugin 为模块提供中间缓存步骤,显著提升二次构建速度  
Webpack 5 内置持久化缓存 使用  cache: {type: 'filesystem'} 
🚀 多进程 / 多线程构建 使用  thread-loader 将后续 Loader 置于 worker 池中运行  
使用  HappyPack (Webpack 4 及之前常用) 分解任务至多个子进程并发执行  
📦 分包与体积优化 使用  DllPlugin  和  DllReferencePlugin 提前打包不常变动的第三方库  
配置  SplitChunksPlugin  进行公共代码拆分 提取公共代码,避免重复打包  
使用  Tree Shaking  剔除无用代码 移除 JavaScript 和 CSS 中未使用的代码  
使用  externals  排除某些库不打入 bundle 通过 CDN 引入,如 React, Vue 等  
🛠 其他优化策略 使用  IgnorePlugin  忽略不必要的模块 如忽略 moment.js 的本地语言包  
开发环境关闭 Source Map 或使用轻量级选项 减少生成 Source Map 的时间  
升级到 Webpack 5 (或最新稳定版) 利用其内置的持久缓存等改进  

与 webpack 类似的工具还有哪些? 区别?

工具名称 核心定位 主要优势 典型适用场景
Vite 极速开发体验的构建工具 ✅ 开发环境基于原生 ESM,启动和热更新极快
✅  生产环境使用 Rollup,打包优化到位
中小型 SPA、追求极致开发体验的现代前端项目
Rollup 高效的 JavaScript 打包器 ✅ Tree-shaking 效果非常出色 ,打包代码简洁
✅ 输出代码 更小更快
开发JavaScript 库、工具函数库(如 Vue、React 本身)
Parcel 零配置的 Web 应用打包器 ✅ 开箱即用 ,无需配置
✅  构建速度快,内置支持多种资源类型
快速原型开发、简单的静态网站、新手入门
Rspack 高性能、Webpack 生态兼容的构建工具 ✅ 使用 Rust 编写,构建速度极快
✅  兼容 Webpack 配置与生态,迁移成本低
中大型项目,特别是希望从 Webpack 平滑迁移并提升构建速度的场景
Gulp / Grunt 基于任务的 流式自动化 工具 ✅ 灵活定义和处理任务流程
✅ 不负责模块化,但可 串联其他工具
网站上线流程自动化、旧项目维护

webpack-dev-server 的原理

部件 职责 说明
Express 静态资源服务器 提供静态文件访问能力。 负责响应浏览器对打包后资源的 HTTP 请求。
WebSocket 连接 在服务器和浏览器之间建立双向通信通道。 用于服务器主动向浏览器推送通知,如编译完成、需要热更新等。
webpack-dev-middleware Webpack 和 Express 服务器之间的桥梁。 这是 最核心 的部件,负责以 监听模式 启动 Webpack 编译器,并将编译结果输出到 内存 而非文件系统。

说下 Vite 的原理

维度 Vite 的核心机制 (开发环境) 传统打包工具 (如 Webpack)
启动方式 无打包启动:直接启动服务器,按需编译 打包后启动:先打包所有模块,再启动服务器
编译方式 按需编译:浏览器请求模块时才编译 全量打包:启动前预先编译和打包所有模块
模块处理 利用浏览器原生  ESM,通过 HTTP 请求按需加载 将所有模块打包成少数几个  Bundle
HMR (热更新) 精准而快速:仅编译变更文件,通过 WebSocket 推送更新 相对较慢:以变更文件为入口重新构建,涉及更多依赖
第三方依赖处理 预构建:使用  esbuild  将 CJS/UMD 依赖转为 ESM 并合并 作为应用代码一部分一同打包。

说说 webpack proxy 工作原理? 为什么能解决跨域?

作为一个 反向代理,拦截前端发出的特定请求,并将其透明地转发到真正的后端服务器。

将“浏览器 <-> 后端 API”的跨域请求,拆分成两段“浏览器 <-> 代理服务器”(同源)和“代理服务器 <-> 后端 API”(服务器间通信),从而绕过了浏览器的同源策略限制。

TypeScript

TypeScript 的主要特点是什么?

  • 静态类型系统
  • 类型推断
  • JavaScript 超集:所有合法的 JavaScript 代码都是合法的 TypeScript 代码
  • 接口和泛型
  • ECMAScript 特性支持:支持最新的 ES6+ 特性、可编译为不同版本的 JavaScript
  • 编译时检查:在编译阶段捕获类型错误、不会影响运行时性能

什么是 TypeScript Declare 关键字?

TypeScript Declare 关键字用于 声明 一个已经存在的变量、函数、类、枚举、模块或其他类型,而不需要提供具体的实现。

  • 声明全局变量:仅声明不实现 – 只提供类型信息,不包含具体实现
  • 声明第三方库
  • 声明命名空间
  • 编译时使用 – 在编译阶段被移除,不影响运行时
  • 类型安全 – 为现有 JavaScript 代码提供 TypeScript 类型支持
  • 环境声明 – 通常放在 .d.ts 声明文件中
// 声明全局变量
declare var MY_GLOBAL: string;

// 声明全局常量
declare const APP_VERSION: string;

// 声明全局函数
declare function myGlobalFunction(name: string): void;
// 声明第三方库
declare module 'my-library' {export function doSomething(): void;
  export const version: string;
}

// 使用
import {doSomething} from 'my-library';
doSomething();
// 环境变量声明 environment.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    readonly NODE_ENV: 'development' | 'production' | 'test';
    readonly API_KEY: string;
    readonly DATABASE_URL: string;
  }
}
// 使用
const apiKey = process.env.API_KEY;

Typescript 中的 getter/setter 是什么? 你如何使用它们?

Getter 和 Setter 是特殊的类方法,用于控制对类属性的访问和修改。它们允许你在属性访问时添加额外的逻辑。

class BankAccount {
  private _balance: number = 0;

  get balance(): number {return this._balance;}

  set balance(amount: number) {if (amount < 0) {throw new Error('余额不能为负数');
    }
    this._balance = amount;
  }
}

unknown 是什么类型?

unknown 是 TypeScript 3.0 引入的顶级类型,它表示 类型安全的 any。与 any (直接跳过类型安全校验)不同,unknown 要求在使用前进行类型检查或类型断言。

never 是什么类型,详细讲一下

never 类型表示 永远不会出现的值 的类型。它是 TypeScript 类型系统中最底层的类型,是所有类型的子类型(个人感觉有点像 null)。

// never 类型表示永远不会有返回值的函数
function throwError(message: string): never {throw new Error(message);
}
// 这些赋值都会报错
let a: never = 123;           // Error
let b: never = "hello";       // Error
let c: never = null;          // Error
let d: never = undefined;     // Error

// 唯一能赋值给 never 的只有 never 本身
let e: never = throwError("error"); // OK
// void - 函数正常执行完成,但没有返回值
function logMessage(message: string): void {console.log(message);
  // 隐式返回 undefined
}

let value: unknown;
value = 42;           // ✅
value = "hello";      // ✅
value = true;         // ✅
// 不能直接使用 unknown 类型的值
// value.toFixed(2);   // ❌ 错误:Object is of type 'unknown'
// 需要先进行类型检查
if (typeof value === "number") {value.toFixed(2);   // ✅ 现在 TypeScript 知道 value 是 number
}
// 或者使用类型断言
(value as number).toFixed(2);  // ✅ 但需要开发者自己确保类型正确

// never - 函数永远不会正常完成
function crashProgram(): never {throw new Error("程序崩溃");
  // 永远不会返回
}

// void、never 使用区别
let voidResult: void = logMessage("hello"); // OK
// let neverResult: never = logMessage("hello"); // Error

let neverResult: never = crashProgram(); // OK
// let voidResult: void = crashProgram(); // Error
  • 表示不可能的情况 – 函数永远不会返回
  • 穷尽性检查 – 确保处理所有可能的情况
  • 类型运算 – 在条件类型中过滤和转换类型
  • 防御性编程 – 捕获未处理的情况

TS 中 any、unknown、never、void 的区别

类型 可赋任何值 可赋给其他类型 可直接使用 类型安全
any
unknown
never
void

typescript 中的 is 关键字有什么用?

在 TypeScript 中,is 关键字用于 类型谓词(Type Predicates),它允许你在自定义函数中执行运行时检查,并帮助 TypeScript 编译器缩小变量的类型范围。

function isType(value: any): value is TargetType {// 返回布尔值}

interface Cat {meow(): void;
}
interface Dog {bark(): void;
}
function isCat(animal: Cat | Dog): animal is Cat {return (animal as Cat).meow !== undefined;
}
function handleAnimal(animal: Cat | Dog) {if (isCat(animal)) {
    // TypeScript 知道这里 animal 是 Cat 类型
    animal.meow(); // ✅ 正确
    // animal.bark(); // ❌ 错误:Property 'bark' does not exist on type 'Cat'} else {
    // TypeScript 知道这里 animal 是 Dog 类型
    animal.bark(); // ✅ 正确}
}

TS 类型

// Partial - 所有属性变为可选
interface Todo {
  title: string;
  description: string;
}
type PartialTodo = Partial<Todo>;

// Required - 所有属性变为必需
type RequiredTodo = Required<PartialTodo>;

// Readonly - 所有属性变为只读
type ReadonlyTodo = Readonly<Todo>;

// Pick - 选择部分属性
type TodoPreview = Pick<Todo, "title">;

// Omit - 排除部分属性
type TodoWithoutDescription = Omit<Todo, "description">;

// Record - 创建对象类型
type PageInfo = Record<"home" | "about" | "contact", {title: string}>;

// ReturnType - 获取函数返回类型
type T0 = ReturnType<() => string>; // string

// Parameters - 获取函数参数类型
type T1 = Parameters<(s: string) => void>; // [string]

Typescript 中 interface 和 type 的差别是什么?

特性 Interface Type
语法扩展 extends & (交叉类型)
声明合并 ✅ 支持 ❌ 不支持
类实现 ✅ 支持 ✅ 支持
联合类型 ❌ 不支持 ✅ 支持
元组类型 ❌ 不支持 ✅ 支持
映射类型 ❌ 不支持 ✅ 支持
性能 稍好 稍差(可忽略)
错误消息 更友好 可能显示完整定义
  • 定义对象形状时优先使用 interface
  • 需要联合类型、元组或复杂类型操作时使用 type
  • 在库的类型定义中使用 interface 以便用户扩展
  • 保持一致性,在项目中统一使用约定

其他

虚拟 DOM 一定更快吗?

虚拟 DOM 并不总是更快,它的主要优势在于为开发者提供了更好的开发体验和“足够好”的性能,尤其是在复杂应用中。

虚拟 DOM 并不总是更快,甚至在简单场景下更慢。虚拟 DOM 本身也是有开销的,它需要额外的时间和内存来。例如:虚拟 DOM 需要走完“生成新树 -> Diff -> 更新”的整个流程,开销反而更大。

虚拟 DOM 应用的首次渲染需要构建虚拟 DOM 树,然后再创建真实 DOM。而直接使用字符串模板或 innerHTML 在初始渲染时可能更快。

为何渲染可视化技术的,大多数都是 canvas,而很少用 svg 的?

Canvas 基于像素的位图,即时模式,处理海量数据时图形无实体对象,性能更优,内存占用更低。

缺点:

  • 需手动计算实现交互,开发复杂
  • 放大易失真,需手动重绘

SVG 基于数学公式的矢量图,保留模式,每个图形都是 DOM 元素,占用较多内存。元素过多时,DOM 性能下降明显

优点:

  • 每个图形自带 DOM 事件,交互实现简单
  • 矢量特性,无损缩放

如果某个页面有几百个函数需要执行,可以怎么优化页面的性能?

  • 任务拆分与异步化:
    • 不要一次性执行完所有函数,而是每执行完一小批,就把控制权交还给浏览器,让它有机会处理用户输入和渲染,然后再继续。
    • 使用 requestIdleCallback (RIC) API 专门用于执行非紧急任务,它会在浏览器 空闲时期 调用函数。非常适合那些不需要立刻完成的工作。注意requestIdleCallback 的执行时机不保证,如果页面一直繁忙,它可能迟迟得不到执行。对于有明确时间要求的任务,建议用分片策略
  • 使用 Web Workers:如果这些函数是 纯计算型 的,不涉及 DOM 操作,那么 Web Workers 是最佳的解决方案 限制:Worker 中不能操作 DOM,与主线程通信需要通过 postMessage
  • 架构与代码层面的优化
    • 不要在页面初始化时就执行所有函数。使用 “按需执行” 策略,利用 Intersection Observer API 来监听元素是否进入视口。
    • 函数去抖(Debounce)与节流(Throttle):如果这些函数是由高频率事件(如 scrollresizeinput)触发的,必须使用防抖或节流。
    •  算法优化:是否存在重复计算?能否合并?将多个相似功能的函数合并为一个。

举例说明你对尾递归的理解,以及有哪些应用场景

在一个递归函数中,如果函数在调用自身之后,除了返回它的返回值,没有再执行任何其他操作 ,那么这个递归调用就称为尾递归。 递归调用是整个函数体中最后执行的语句,并且它的返回值直接作为当前函数的返回值。

  • 阶乘函数

异步编程有哪些实现方式?

方式 特点 优点 缺点
回调函数 最基础,函数作为参数传递 简单、无处不在 回调地狱、错误处理难
Promise ES6 标准,代表未来值 链式调用、错误处理更佳 依然比同步代码复杂
Generator 可暂停执行的函数 以同步方式写异步代码 需要执行器、语法复杂
Async/Await Promise 的语法糖 代码最清晰、易于调试、错误处理简单 必须用在 async 函数中
事件监听 基于事件驱动,不关心顺序 解耦、适合处理流和重复事件 容易导致“事件满天飞”、流程不直观

前端灰度发布

方案类型 实现方式 优点 缺点 适用场景
Nginx 负载均衡 配置权重(加权轮询),结合 Cookie 标识用户 实现简单,性能较好。 规则较简单,不易与复杂业务逻辑结合;Cookie 清理可能导致版本切换 简单的按流量百分比灰度。
Nginx + 服务端 + Redis 请求先到 Nginx,服务端根据规则(可从 Redis 读取)决定版本,设置 Cookie 灵活性强,可结合复杂业务规则;可控性高 架构稍复杂,需后端支持。 需要精细控制灰度规则(如按用户 ID、地域等)的场景。
服务端渲染 (SSR) 分流 服务端根据灰度规则直接渲染不同版本的 HTML 内容 规则控制精准,用户体验一致。 对服务器有压力,多页面应用处理可能更复杂 服务端渲染应用 (如 Next.js, Nuxt.js)。
客户端动态渲染 客户端异步请求灰度规则(API),根据结果动态渲染不同 UI 或加载不同资源 灵活性高,与后端解耦。 可能看到界面闪动;需维护多套代码可能增加体积 单页面应用 (SPA),需快速实验和迭代。

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

window.onerror 和 try-catch:只能捕获同步和部分异步的错误,当页面进程本身因内存溢出、无限循环或复杂操作而卡死或崩溃时,这些机制就失效了。

方案 检测目标 可靠性 复杂度 推荐度
Service Worker 心跳 进程卡死、无响应 ★★★★★
MutationObserver DOM 白屏、内容异常 ★★★☆☆
ReportingObserver 浏览器崩溃 很高 ★★☆☆☆ (待普及)
beforeunload 对比 异常退出 ★☆☆☆☆

建议进行组合方案,以实现更全面的覆盖,避免浏览器兼容等问题

用户访问页面自屏了,原因是啥,如何排查?

  • 可能的原因
    • 网络层面:资源加载失败、CDN 问题、用户网络差
    • JavaScript 执行错误:语法错误、运行时错误、第三方库冲突、框架初始化失败
    • 资源加载与解析:HTML 结构问题(根节点 <div id="app"></div> 被意外删除或覆盖。)、CSS 加载阻塞 / 错误、主 JS 文件过大
    • 环境与兼容性:浏览器兼容性
  • 系统化排查方法
    • 确认问题范围: 是个别用户还是所有用户?
    • 本地开发环境复现与排查
    • 禁用缓存: 在 Network 标签下,勾选 “Disable cache”,模拟新用户首次访问的情况,排除缓存干扰。
    • 切换网络环境 / 模拟慢速网络
  • 生产环境排查(用户端)
    • 控制台查看
    • 使用远程调试工具:如微信浏览器可以使用 vConsole,PC 端可以使用 weinre 或浏览器自带的远程调试功能。
  • 代码层面与监控排查
    • 添加错误边界(对于 React)或全局错误捕获(对于 Vue)。React: 使用 ErrorBoundary 组件包裹你的根组件,在 componentDidCatch 方法中捕获错误并展示降级 UI,同时将错误信息上报。Vue: 使用 Vue.config.errorHandler 全局错误处理函数来捕获和处理错误。
    • 构建分析:webpack-bundle-analyzer 等工具生成的报告,看是否有意料之外的巨大依赖包
    • 接入 APM(应用性能监控):SentryFundebug 等前端监控 SDK

前端如何解决页面请求接口大规模并发问题

  • 减少后端请求:例如装修页,将所有接口数据封装成一个新的接口返回 json
  • 使用 HTTPS,请求头压缩
  • 数据缓存:静态资源本地缓存,地区数据本地缓存
  • 优化请求时序:懒加载、预加载
  • 提升用户体验(降级与反馈):使用骨架屏(Skeleton Screen)、优雅降级与重试机制

正文完
 0
评论(没有评论)
验证码