1.11 前端核心面试题详细解答
1. JavaScript本地存储的方式、区别及应用场景
JavaScript本地存储是指将数据存储在客户端浏览器中,无需每次都从服务器请求,常见的方式有4种:cookie、localStorage、sessionStorage、indexedDB。
各存储方式区别:
- 存储大小:cookie较小,仅4KB;localStorage和sessionStorage通常为5-10MB;indexedDB为大容量存储,理论上无上限(受浏览器和设备存储限制)。
- 生命周期:cookie可通过expires或max-age设置生命周期,未设置则为会话级(关闭浏览器失效);localStorage永久存储,除非手动删除;sessionStorage为会话级存储,关闭标签页或浏览器即失效;indexedDB永久存储,手动删除才会消失。
- 与服务器交互:cookie会随每次HTTP请求自动发送到服务器;其他三种均仅在客户端存储,不主动与服务器交互。
- 存储类型:cookie仅能存储字符串;localStorage和sessionStorage也以字符串存储,需手动转换复杂类型;indexedDB支持存储对象、数组等复杂数据类型。
- 操作复杂度:cookie操作需手动封装(原生API繁琐);localStorage和sessionStorage原生API简洁;indexedDB操作较复杂,需通过异步API进行增删改查。
应用场景:
- cookie:适用于存储少量需与服务器交互的数据,如身份认证token、用户偏好设置(如主题)、会话跟踪等。
- localStorage:适用于存储长期需保留的客户端数据,如用户登录状态(非敏感信息)、离线应用数据、页面配置信息等。
- sessionStorage:适用于存储当前会话临时数据,如表单临时数据、页面跳转间的临时参数传递、单页应用的会话状态等。
- indexedDB:适用于大容量客户端数据存储,如离线应用的大量数据缓存、复杂查询场景(如本地日志管理、数据可视化的本地数据源)等。
2. Vue 3中Tree-shaking特性及示例说明
Tree-shaking定义:Tree-shaking直译“摇树”,是一种基于ES模块(ESM)的代码优化技术,核心作用是在打包构建时,自动移除项目中未被使用的冗余代码(死代码),从而减小最终打包产物的体积。Vue 3中通过重构代码为ES模块,全面支持Tree-shaking,让开发者可以按需引入所需的API,而非引入整个Vue库。
Vue 3实现Tree-shaking的核心前提:代码基于ES模块编写(import/export),而非CommonJS模块(require),因为ES模块是静态分析的,打包工具(如Webpack、Vite)可在编译阶段确定哪些模块被使用,进而剔除未使用部分。
示例说明:
1. 按需引入Vue的核心API:在Vue 2中,即使只使用Vue的部分功能(如reactive、computed),也需引入整个Vue库(import Vue from 'vue');而在Vue 3中,可直接按需引入所需API,未引入的API会被Tree-shaking移除。
例如,仅使用响应式API reactive和computed:
import { reactive, computed } from 'vue' // 仅引入所需API,未引入的(如ref、watch等)不会被打包
此时打包工具会分析到仅使用了reactive和computed,自动剔除Vue库中其他未被使用的代码(如虚拟DOM相关的部分、编译相关的部分等)。
2. 按需引入Vue的组件:Vue 3的组件库(如VueRouter、Pinia)也支持Tree-shaking。例如使用VueRouter时,可按需引入createRouter、createWebHistory等API,未使用的(如createWebHashHistory)不会被打包。
import { createRouter, createWebHistory } from 'vue-router' // 按需引入,未使用的API被剔除
3. Vue中nextTick的理解及作用
nextTick的定义:nextTick是Vue提供的一个全局API,其核心作用是延迟执行一段回调函数,直到下一次DOM更新周期结束后再执行。简单来说,当Vue的数据发生变化时,DOM并不会立即更新,而是会先将数据变化缓存起来,在同一事件循环中批量处理所有数据变化,完成后再统一更新DOM;nextTick就是让回调函数等待DOM更新完成后再执行。
核心原理:nextTick内部优先使用浏览器原生的微任务(如Promise.then、MutationObserver),若浏览器不支持则降级使用宏任务(如setTimeout)。通过将回调函数放入微任务/宏任务队列,确保其执行时机晚于当前事件循环中的DOM更新操作。
作用:
1. 获取更新后的DOM:当数据变化后,若立即访问对应的DOM元素,得到的还是更新前的旧DOM;通过nextTick的回调函数,可在DOM更新完成后准确获取到更新后的DOM状态。
例如:数据msg变化后,要获取msg对应的DOM元素的内容,需在nextTick中执行获取操作,否则得到的是旧内容。
2. 避免多次DOM操作:当多次修改数据时,Vue会批量处理数据变化并统一更新DOM,使用nextTick可将多个基于更新后DOM的操作合并到同一个回调中,避免重复触发DOM更新,提升性能。
3. 配合DOM操作的业务逻辑:某些业务逻辑需要依赖更新后的DOM(如DOM的高度、位置计算、初始化第三方DOM插件等),此时需通过nextTick确保DOM更新完成后再执行这些逻辑。
4. Real DOM 和 Virtual DOM 的区别、优缺点
定义区分:
Real DOM(真实DOM):是浏览器中真实存在的DOM节点,是文档对象模型的具体实现,每个DOM节点都包含大量属性和方法,用于描述节点的结构、样式和行为,直接与浏览器渲染引擎交互。
Virtual DOM(虚拟DOM):是对Real DOM的抽象描述,本质是一个JavaScript对象,仅包含DOM节点的核心信息(如标签名、属性、子节点、文本内容等),不包含Real DOM的复杂属性和方法,用于在内存中模拟DOM结构。
区别:
- 存在形式:Real DOM是浏览器中的真实节点,占用浏览器内存;Virtual DOM是JS对象,占用JS内存,与浏览器渲染引擎无关。
- 操作性能:操作Real DOM会直接触发浏览器的重排(回流)或重绘,性能消耗大;操作Virtual DOM仅是修改JS对象,性能消耗小,且修改后会通过“diff算法”计算出最小更新范围,再批量更新Real DOM。
- 跨平台性:Real DOM依赖浏览器环境,无法跨平台;Virtual DOM是JS对象,可在不同环境(浏览器、Node.js、移动端)中使用,为跨平台框架(如Vue、React、React Native)提供基础。
Real DOM 优缺点:
优点:1. 直接与浏览器交互,无需中间转换,原生支持所有浏览器特性;2. 开发简单,直接通过原生API操作即可。
缺点:1. 操作性能差,频繁操作会导致大量重排/重绘,影响页面流畅度;2. 无法跨平台;3. 批量操作DOM需手动优化,开发成本高。
Virtual DOM 优缺点:
优点:1. 操作性能优,内存中模拟DOM修改,批量更新Real DOM,减少重排/重绘次数;2. 支持跨平台,可适配不同运行环境;3. 简化开发,框架自动处理DOM更新优化,开发者无需关注底层DOM操作;4. 便于实现时间旅行、状态回溯等高级功能(如Vue的devtools、React的Redux)。
缺点:1. 存在一定的内存开销(维护Virtual DOM对象);2. 首次渲染时,因需先构建Virtual DOM再转换为Real DOM,可能比直接操作Real DOM略慢(但后续更新优势明显);3. 抽象层增加了学习成本,需理解框架的Virtual DOM机制。
5. React Router中HashRouter和BrowserRouter的区别和原理
React Router是React生态中用于实现单页应用(SPA)路由管理的库,HashRouter和BrowserRouter是其提供的两种核心路由模式,核心区别在于URL的表现形式和底层实现原理。
原理:
1. HashRouter:基于浏览器URL中的哈希值(#后面的部分)实现路由跳转。哈希值的特点是:改变哈希值不会触发浏览器的页面刷新,只会触发window对象的hashchange事件。HashRouter通过监听hashchange事件,感知URL中哈希值的变化,进而匹配对应的路由组件并渲染。
2. BrowserRouter:基于HTML5的History API(pushState、replaceState、popstate事件)实现路由跳转。History API允许开发者在不刷新页面的情况下,修改浏览器的历史记录栈,改变URL的路径部分(无#)。BrowserRouter通过调用pushState/replaceState修改URL,同时监听popstate事件(用户点击前进/后退按钮时触发),进而匹配路由组件并渲染。
区别:
- URL格式:HashRouter的URL包含#(如http://xxx.com/#/home),哈希值后面是路由路径;BrowserRouter的URL无#(如http://xxx.com/home),路径直接拼接在域名后。
- 底层依赖:HashRouter依赖哈希值和hashchange事件,兼容性好(支持所有现代浏览器及IE8+);BrowserRouter依赖HTML5 History API,兼容性稍差(仅支持IE10+及现代浏览器)。
- 服务器配置:HashRouter无需服务器额外配置,因为哈希值不会发送到服务器,服务器只需返回单页应用的入口HTML文件即可;BrowserRouter需要服务器配置支持,因为当用户直接访问非根路径(如http://xxx.com/home)时,服务器会尝试查找/home对应的资源,若未配置则会返回404,需配置服务器将所有路由请求都指向入口HTML文件(如Nginx的try_files配置、Apache的Rewrite规则)。
- 路由参数传递:HashRouter的参数只能拼接在哈希值后(如#/user?id=1);BrowserRouter支持更灵活的参数传递方式(如路径参数/user/:id、查询参数?id=1),URL更美观、直观。
6. React中Fiber架构的理解及解决的问题
Fiber架构的定义:Fiber是React 16中引入的一种全新的协调(Reconciliation)引擎,核心是对React的虚拟DOM更新机制进行重构,将原本同步的递归协调过程,拆分为可中断、可恢复、优先级可控的异步过程。Fiber本质上是一个工作单元(JS对象),每个Fiber对应一个组件或DOM节点,用于记录组件的类型、属性、状态、子节点等信息,同时记录工作进度。
核心设计思路:
1. 时间切片(Time Slicing):将整个虚拟DOM的diff过程拆分为多个小的工作单元,每个工作单元执行时间控制在浏览器一帧(约16.6ms)内;若执行时间超过阈值,就暂停当前工作,将控制权交还给浏览器(处理用户交互、重绘等),待浏览器空闲后再恢复工作。
2. 优先级调度(Priority Scheduling):为不同的更新任务设置优先级(如用户交互、动画更新优先级高,数据请求回调优先级低),高优先级任务可中断低优先级任务的执行,待高优先级任务完成后,低优先级任务再重新执行(可复用已完成的工作)。
3. 链表结构:Fiber采用链表结构存储组件树,每个Fiber有三个指针(child:子节点、sibling:兄弟节点、return:父节点),通过遍历链表替代原本的递归遍历,实现工作单元的中断和恢复(递归无法中断,会一直占用调用栈)。
解决的问题:
Fiber架构主要解决了React 16之前同步协调机制的“卡顿”问题。在React 16之前,虚拟DOM的diff过程是同步的递归过程:当组件树层级较深、节点数量较多时,diff过程会持续占用浏览器主线程,导致浏览器无法及时处理用户交互(如点击、输入)、动画渲染等任务,出现页面卡顿、响应延迟的情况。
具体解决的问题:
1. 解决同步递归的性能瓶颈:将同步递归拆分为可中断的异步工作单元,避免长时间占用主线程,提升页面响应速度。
2. 支持优先级调度:确保高优先级任务(如用户输入、动画)优先执行,避免因低优先级任务阻塞导致的交互卡顿。
3. 提升错误边界的可用性:Fiber架构允许在协调过程中捕获错误,通过错误边界组件处理错误,避免整个应用崩溃。
7. React中类组件和函数组件的理解及区别
定义理解:
1. 类组件:通过ES6的class语法定义的组件,继承自React.Component或React.PureComponent,内部通过render方法返回组件的UI结构。类组件是有状态组件(早期React中,状态管理主要依赖类组件的state),支持生命周期钩子函数,可实现更复杂的逻辑控制。
2. 函数组件:以函数形式定义的组件,接收props参数并返回组件的UI结构。早期的函数组件是无状态组件,仅能根据props渲染UI;React 16.8引入Hooks(如useState、useEffect)后,函数组件也可拥有状态和类似生命周期的功能,成为React推荐的组件编写方式。
核心区别:
- 定义方式:类组件使用class语法定义,需继承React.Component/PureComponent;函数组件使用函数定义,直接接收props并返回UI。
- 状态管理:类组件通过state属性和setState方法管理状态;函数组件通过useState、useReducer等Hooks管理状态。
- 生命周期:类组件拥有完整的生命周期钩子(如componentDidMount、componentDidUpdate、componentWillUnmount等);函数组件无原生生命周期,需通过useEffect Hook模拟(useEffect可覆盖挂载、更新、卸载三个阶段的逻辑)。
- this指向:类组件中this指向组件实例,需注意this绑定问题(如在事件处理函数中需绑定this,或使用箭头函数);函数组件中无this,直接通过闭包访问props和状态。
- 性能优化:类组件可通过继承React.PureComponent实现浅比较优化(避免不必要的重渲染),或手动实现shouldComponentUpdate方法进行深比较;函数组件可通过React.memo(浅比较props)和useMemo、useCallback Hooks(缓存计算结果和函数,避免子组件不必要的重渲染)实现性能优化。
- 代码简洁度:类组件代码冗余,需编写render方法、构造函数等;函数组件代码简洁直观,逻辑与UI结合更紧密,易于维护和理解。
- Hooks支持:类组件不支持Hooks;函数组件是Hooks的唯一载体,通过Hooks可复用状态逻辑。
8. TypeScript 中的类型断言
类型断言的定义:类型断言是TypeScript中用于告诉编译器“我比你更清楚这个变量的类型”的一种方式,它不会改变变量的实际类型,仅用于覆盖编译器的类型推断,让编译器按照我们指定的类型去检查代码。简单来说,类型断言是一种“类型转换”的声明,帮助开发者处理编译器无法准确推断的类型场景。
核心作用:
1. 解决类型推断不准确的问题:当编译器推断的类型过宽(如推断为any或联合类型),而开发者明确知道变量的具体类型时,可通过类型断言缩小类型范围,获得更精确的类型提示和类型检查。
2. 兼容历史代码或第三方库:当使用无类型定义的第三方库,或处理从JavaScript迁移过来的代码时,可通过类型断言为变量指定明确的类型,避免大量any类型的使用,提升代码的类型安全性。
使用场景与注意事项:
常见使用场景:
1. 联合类型转具体类型:当变量是联合类型时,若开发者明确知道当前变量的类型,可通过断言转换为具体类型,访问该类型特有的属性或方法。
2. 父类型转子类型:当变量被推断为父类型,而实际是子类型时,可通过断言转换为子类型,访问子类型的扩展属性。
3. any类型转具体类型:当变量类型为any时,通过断言指定具体类型,恢复类型检查和代码提示。
注意事项:
1. 类型断言不改变变量实际类型:若断言的类型与变量实际类型不匹配,运行时仍可能报错,编译器仅在编译阶段按照断言类型检查。
2. 避免滥用类型断言:类型断言是开发者手动干预类型检查,滥用会降低TypeScript的类型安全性,应优先通过类型推断、类型守卫等更安全的方式处理类型问题。
TypeScript提供两种类型断言语法:<目标类型>变量(JSX中不支持)和变量 as 目标类型(推荐,全场景支持)。
9. JavaScript中的事件代理(事件委托)及应用场景
事件代理的定义:事件代理(又称事件委托)是一种利用DOM事件冒泡机制实现的事件处理模式。核心思想是:不将事件监听器直接绑定到目标元素上,而是绑定到目标元素的父级元素(或祖先元素)上,当目标元素触发事件时,事件会通过冒泡机制传播到父级元素,父级元素的事件监听器捕获到事件后,再根据事件源(event.target)判断是否是需要处理的目标元素,进而执行对应的处理逻辑。
核心原理:DOM事件的冒泡机制——当一个元素触发某个事件(如click、input)时,该事件会从触发元素开始,沿着DOM树向上传播,依次触发其所有父级元素的同名事件,直到传播到document或window对象。事件代理正是利用这一机制,将事件处理逻辑集中在父级元素上,实现对多个子元素的事件管理。
应用场景:
1. 动态生成的元素:当子元素是动态添加到页面中的(如通过AJAX加载、用户操作生成),无法在元素生成前为其绑定事件,此时可将事件代理到父级元素上,无论子元素何时生成,都能触发事件处理逻辑。例如:列表页的列表项是动态加载的,点击列表项的事件可代理到列表容器(如ul)上。
2. 大量相似子元素:当页面中有大量结构相似的子元素(如表格的单元格、导航菜单的菜单项),为每个子元素单独绑定事件会占用大量内存,影响页面性能;通过事件代理,只需在父级元素上绑定一个事件监听器,即可处理所有子元素的事件,减少内存占用,提升性能。
3. 简化事件管理:将多个子元素的事件处理逻辑集中在父级元素上,便于统一维护和管理,减少代码冗余。例如:多个按钮的点击事件,可代理到父级容器上,通过判断按钮的ID或类名执行不同的逻辑。
优点:1. 减少事件监听器数量,降低内存消耗;2. 支持动态元素的事件处理,无需重复绑定;3. 简化代码维护,集中管理事件逻辑。
注意事项:1. 仅支持冒泡事件(如click、input、mouseover等),不支持捕获事件(如focus、blur的原生事件,需通过addEventListener的useCapture参数改为捕获模式,或使用对应的冒泡事件如focusin、focusout);2. 需准确判断事件源(event.target),避免误触发父级元素其他子元素的事件;3. 若父级元素被移除,需及时移除事件监听器,避免内存泄漏。
10. 防抖和节流:定义、区别及实现思路
防抖(Debounce)的定义:防抖是一种事件触发频率控制技术,核心逻辑是:当事件被连续触发时,不立即执行回调函数,而是设置一个延迟时间,若在延迟时间内事件再次被触发,则重新计时(重置延迟时间);只有当事件触发后,延迟时间内没有再次触发,才会执行一次回调函数。简单来说,防抖是“多次触发,最后一次生效”。
节流(Throttle)的定义:节流也是一种事件触发频率控制技术,核心逻辑是:当事件被连续触发时,设置一个时间间隔,在这个时间间隔内,无论事件触发多少次,都只执行一次回调函数;即每隔固定的时间,只允许回调函数执行一次。简单来说,节流是“多次触发,固定间隔生效”。
核心区别:
- 执行时机:防抖是在事件停止触发后,等待延迟时间结束才执行一次;节流是在事件触发过程中,每隔固定时间执行一次,无论事件是否停止触发。
- 触发频率:防抖的执行次数取决于事件停止触发后的延迟时间,连续触发时可能只执行一次;节流的执行次数取决于事件触发的总时长和设置的时间间隔(总时长/时间间隔),连续触发时会定期执行。
- 应用场景:防抖适用于“事件触发后需要等待稳定状态再执行”的场景;节流适用于“事件连续触发但需要定期执行”的场景。
实现思路:
1. 防抖的实现思路:
核心是使用定时器存储延迟执行的回调函数,每次事件触发时,先清除之前的定时器(重置计时),再创建新的定时器,延迟执行回调函数。可分为“非立即执行版”(默认,停止触发后延迟执行)和“立即执行版”(首次触发时立即执行,之后停止触发后再执行一次)。
具体步骤:① 定义一个防抖函数,接收回调函数和延迟时间作为参数;② 在防抖函数内部声明一个定时器变量(用于存储定时器ID);③ 返回一个新的函数(事件处理函数);④ 新函数内部,先清除之前的定时器(clearTimeout(timer));⑤ 若为立即执行版,判断定时器是否为空(首次触发时),若为空则立即执行回调函数;⑥ 创建新的定时器,延迟时间后执行回调函数(非立即执行版),或重置定时器为空(立即执行版)。
2. 节流的实现思路:
核心是记录上一次执行回调函数的时间,每次事件触发时,判断当前时间与上一次执行时间的间隔是否大于等于设置的时间间隔,若大于则执行回调函数并更新上一次执行时间;若小于则不执行。可分为“时间戳版”(首次触发时立即执行)和“定时器版”(首次触发后延迟执行)。
具体步骤:① 定义一个节流函数,接收回调函数和时间间隔作为参数;② 在节流函数内部声明一个变量,用于存储上一次执行回调函数的时间(初始值为0);③ 返回一个新的函数(事件处理函数);④ 新函数内部,获取当前时间(Date.now());⑤ 若为时间戳版,判断当前时间 - 上一次执行时间 ≥ 时间间隔,若满足则执行回调函数,并更新上一次执行时间为当前时间;⑥ 若为定时器版,判断是否存在定时器,若不存在则创建定时器,延迟时间间隔后执行回调函数,执行后清除定时器并更新上一次执行时间。
典型应用场景:
防抖:搜索框输入联想(等待用户输入完成后再发送请求)、窗口大小调整(resize事件,等待窗口调整完成后再计算布局)、按钮点击防重复提交(避免用户快速点击多次触发提交)。
节流:滚动事件(scroll事件,定期获取滚动位置)、鼠标移动事件(mousemove事件,绘制Canvas时定期更新)、高频点击事件(如游戏中的射击按钮,限制点击频率)。