前端工程师面试题纲(含答案)
React部分
基础概念
-
什么是React?它有哪些特点和优势?
- React是用于构建用户界面的JavaScript库。它采用组件化开发方式,代码可维护和可复用性高;使用虚拟DOM提高性能,支持服务器端渲染和移动端开发。其特点包括声明范式、可与已知库或框架配合等。优势有提高应用性能、方便在客户端和服务器端使用、代码可读性好、易与其他框架集成、便于编写UI测试用例等。
-
解释JSX的概念,为什么浏览器无法直接读取JSX,如何处理?
- JSX是React中特有的语法扩展,允许在JavaScript代码中编写类似HTML的标记,实际会被转换为JavaScript函数调用,最终创建React元素,能方便地将界面和逻辑紧密结合。浏览器只能处理JavaScript对象,不能读取常规JavaScript对象中的JSX,为使浏览器能读取JSX,需用像Babel这样的JSX转换器将JSX文件转换为JavaScript对象,再传给浏览器。
-
详细阐述虚拟DOM的工作原理和优势,以及它与真实DOM的区别。
- 工作原理:虚拟DOM是一个轻量级的JavaScript对象,是真实DOM的内存表示。当底层数据发生改变时,整个UI会在虚拟DOM描述中重新渲染,然后计算之前DOM表示与新表示的差异,最后只用实际更改的内容更新真实DOM。优势包括提高性能(通过diff算法找出最小差异进行更新,减少对真实DOM的操作)、跨平台(方便在不同环境中使用)等。与真实DOM的区别:真实DOM更新缓慢、可直接更新HTML、元素更新时创建新DOM、DOM操作代价高、消耗内存多;虚拟DOM更新更快、无法直接更新HTML、元素更新时更新JSX、DOM操作简单、内存消耗少。
-
React的ES6语法与ES5相比有哪些不同?
-
语法区别如下:
require与import:ES5用var React = require('react'),ES6用import React from 'react'。export与exports:ES5用module.exports = Component,ES6用export default Component。component和function:ES5用var MyComponent = React.createClass({ render: function() { ... }}),ES6用class MyComponent extends React.Component { render() { ... }}。props:ES5用var App = React.createClass({ propTypes: { name: React.PropTypes.string }, render: function() { ... }}),ES6用class App extends React.Component { render() { ... }}。
-
组件开发
-
类组件和函数组件的区别是什么,各有什么应用场景?
- 语法上,类组件需要继承
React.Component并定义render方法,函数组件是纯函数接收props并返回React元素。状态管理方面,类组件可用setState管理内部状态,函数组件在React 16.8之前无状态,之后可用useState等Hooks管理状态。生命周期方面,类组件有完整的生命周期方法,函数组件通过useEffect模拟生命周期。类组件适用于有复杂状态管理和生命周期方法的场景,函数组件适用于简单展示、无状态或状态管理简单的场景。
- 语法上,类组件需要继承
-
如何理解React中的Props和State,它们之间有什么区别,如何更新组件的状态?
Props是传递给组件的属性,是不可变的,用于组件间数据传递;State是组件内部的状态,可变,用于存储组件自身的数据。区别在于Props由父组件传递,State由组件自身管理。更新组件状态在类组件中用this.setState方法,在函数组件中用useState返回的更新函数。若新状态依赖旧状态,建议使用函数式更新。
-
简述React组件的生命周期,在函数组件中如何模拟生命周期方法?
- React类组件的生命周期分为挂载、更新、卸载三个阶段。挂载阶段:
constructor->static getDerivedStateFromProps->render->componentDidMount;更新阶段:static getDerivedStateFromProps->shouldComponentUpdate->render->getSnapshotBeforeUpdate->componentDidUpdate;卸载阶段:componentWillUnmount。在函数组件中,用useEffect模拟生命周期,不传依赖数组时,每次渲染后执行,类似componentDidMount和componentDidUpdate;传递空数组时,仅在挂载和卸载时执行,清理函数模拟componentWillUnmount。
- React类组件的生命周期分为挂载、更新、卸载三个阶段。挂载阶段:
-
如何实现组件之间的通信,有哪些常见的方式?
-
常见方式有:
- 父子组件:通过
Props传递数据,父组件向子组件传递数据;子组件通过回调函数向父组件传递数据。 - 兄弟组件:可通过状态提升将共享状态提升到共同的父组件,或使用Context上下文。
- 复杂场景:使用Redux、Zustand等状态管理库结合新Hook
useSyncExternalStore。
- 父子组件:通过
-
Hooks相关
-
介绍React Hooks的优势和限制,使用时需要注意什么?
- 优势:可以在不编写
class的情况下使用state和其他React特性,复用状态逻辑,让代码更简洁。限制:不能在条件语句、循环或嵌套函数中调用Hooks,必须在React函数的顶层调用。使用时需遵循Hooks规则,确保每次渲染时Hooks的调用顺序一致。
- 优势:可以在不编写
-
实现一个自定义Hook,并说明其用途。
- 示例:
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((response) => response.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [url]);
return { data, loading };
}
- 用途:复用数据获取逻辑,在不同组件中使用相同的逻辑获取数据。
- useMemo和useCallback的区别是什么,如何使用它们进行性能优化? -
useMemo用于缓存变量,返回一个计算结果;useCallback用于缓存函数。使用useMemo缓存计算开销大且依赖特定props或state的计算结果,避免重复计算;使用useCallback避免函数重建,当函数作为props传递时,防止因函数引用变化导致子组件不必要的重新渲染。
性能优化
-
React性能优化的常用方法有哪些,举例说明。
-
常用方法包括:
- 使用
React.memo:用于避免组件不必要的重新渲染,如const MemoizedComponent = React.memo(MyComponent)。 - 使用
useMemo和useCallback:useMemo缓存计算结果,useCallback缓存函数。 - 代码分割:使用
React.lazy和Suspense动态加载组件,如const LazyComponent = React.lazy(() => import('./Component')); <Suspense fallback={<div>Loading...</div>}><LazyComponent /></Suspense>。 - 避免父组件频繁更新:减少父组件的状态更新。
- 列表渲染优化:为列表中的每一项设置稳定的
key。 - 使用生产环境构建:移除开发模式的一些额外检查。
- 使用
-
-
如何避免组件的不必要重新渲染,有哪些工具和技巧?
- 可使用
React.memo包裹函数组件,进行浅比较,仅在props变化时重新渲染;在类组件中使用shouldComponentUpdate生命周期方法,根据props和state的变化决定是否更新组件;使用useMemo和useCallback缓存值和函数,避免因引用变化导致的重新渲染。
- 可使用
-
解释React 18的新特性,如并发模式、Suspense、自动批处理等。
- 并发模式:允许React在渲染时根据任务优先级中断和恢复工作,优先处理高优先级任务(如用户输入),提升用户体验,通过时间切片和优先级调度优化渲染性能。
Suspense:增强了对服务器端渲染和数据获取的支持,可用于延迟加载组件、数据获取和服务端渲染,配合React.lazy使用。- 自动批处理:默认对所有状态更新进行批处理(包括异步操作),减少不必要的重新渲染,将多个状态更新合并为一次重新渲染。
原理深挖
-
React的Diff算法是如何工作的,有哪些优化策略?
- 工作原理:采用同层比较,假设同一层级的节点可以高效比较,使用唯一
key提高比较性能,相同类型的组件仅更新属性,不重新创建。优化策略包括使用唯一key、避免跨层级移动节点、相同类型组件尽量只更新属性等。
- 工作原理:采用同层比较,假设同一层级的节点可以高效比较,使用唯一
-
React Fiber架构的核心思想是什么,解决了什么问题?
- 核心思想是将任务拆分为多个小任务,支持可中断的渲染,采用链表结构和双缓冲树实现可中断更新,使用
requestIdleCallback调度任务优先级。解决了旧架构递归不可中断导致的卡顿问题,优先处理高优先级任务,提升用户体验。
- 核心思想是将任务拆分为多个小任务,支持可中断的渲染,采用链表结构和双缓冲树实现可中断更新,使用
-
JSX编译后的代码结构是怎样的,Babel插件对JSX做了哪些转换?
- JSX编译后会转换为
React.createElement调用,例如<Button type="primary">Click</Button>编译后为React.createElement(Button, {type: "primary"}, "Click")。Babel插件将JSX语法转换为JavaScript代码,使其能在浏览器中运行。
- JSX编译后会转换为
JavaScript部分
基础语法
-
列举JavaScript的基本数据类型和引用数据类型,如何判断数据类型?
- 基本数据类型包括
number、string、boolean、null、undefined、symbol、bigint;引用数据类型包括object(如普通对象、数组、函数等)。判断数据类型的方法有:typeof运算符可判断基本数据类型,但对null会返回object,对引用数据类型只能判断出object或function;instanceof运算符用于判断一个对象是否是某个构造函数的实例;Object.prototype.toString.call()方法可以准确判断各种数据类型。
- 基本数据类型包括
-
解释变量提升和作用域链的概念,结合代码说明。
- 变量提升是指在JavaScript代码执行之前,JavaScript引擎会将所有的变量和函数声明“提升”到其所在作用域的顶部,但只有声明会被提升,赋值操作不会。例如:
console.log(a); // 输出undefined
var a = 10;
- 作用域链是从内到外的查找顺序,函数内先找自己的作用域,再找外层,最后是全局。例如:
var a = 10;
function fn() {
var b = 20;
console.log(b); // 20
console.log(a); // 10
}
fn();
-
var、let和const的区别是什么,使用场景有哪些?
var声明的变量具有函数作用域,存在变量提升,可重复声明和赋值;let和const具有块级作用域,不存在变量提升,let可重新赋值,const声明时必须赋值且不能重新赋值。var适用于旧代码或需要函数作用域的场景;let和const适用于现代JavaScript开发,优先使用const,当值需要改变时用let。
-
什么是闭包,在实际开发中有哪些应用场景?
- 闭包是函数和声明该函数的词法环境的组合。在JavaScript中,闭包常用于封装私有变量或状态,或实现回调函数和高阶函数。应用场景包括实现私有变量、函数防抖/节流、模块化开发中封装内部逻辑等。例如:
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
const add = outer();
console.log(add()); // 1
console.log(add()); // 2
异步编程
-
解释JavaScript中的异步编程,列举几种实现异步编程的方法。
- 异步编程允许JavaScript在等待某个操作(如网络请求或定时器)完成时,继续执行其他代码,避免阻塞UI或浪费CPU资源。实现异步编程的方法有回调函数、Promises、async/await等。
-
如何使用Promise处理异步操作,链式调用和错误捕获的机制是怎样的?
- 创建
Promise实例时,传入一个执行器函数,该函数接收resolve和reject两个参数,分别用于标记操作成功和失败。例如:
- 创建
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
const data = '成功获取数据';
resolve(data);
}, 1000);
});
- 链式调用通过`then`方法实现,可连续处理多个异步步骤,如`fetchData.then(result => { return result.split(' '); }).then(arr => { console.log(arr); });`。错误捕获使用`catch`方法,当`Promise`被`reject`时会执行`catch`中的回调函数。
- 简述async/await的实现原理,与Promise相比有什么优势? -
async/await是基于Promise实现的语法糖,async函数返回一个Promise对象,await只能在async函数中使用,用于等待一个Promise的解决,并返回其结果。优势在于代码更简洁、更像同步代码,避免了回调地狱,提高了代码的可读性和可维护性。
事件机制
-
解释JavaScript中的事件委托,它的原理和优势是什么?
- 事件委托是指将事件监听器添加到父元素上,利用事件冒泡的原理,当子元素上的事件触发时,事件会冒泡到父元素上,由父元素的事件监听器处理。原理基于事件冒泡,即事件从触发的元素开始,依次向上传播到父元素。优势包括减少事件监听器的数量,提高性能;动态添加子元素时无需为新元素单独添加事件监听器。
-
如何处理跨域问题,有哪些常见的解决方案?
-
常见解决方案有:
- CORS(跨域资源共享):服务器端设置响应头,允许特定域名的请求访问资源。
- JSONP(JSON with Padding):利用
<script>标签的src属性不受同源策略限制的特点,通过动态创建<script>标签实现跨域数据请求。 PostMessage:用于在不同窗口或iframe之间传递消息。
-
-
简述Cookie、LocalStorage和SessionStorage的区别和使用场景。
-
区别:
- Cookie:数据会随HTTP请求发送到服务器,有大小限制(一般不超过4KB),可设置过期时间,存在安全风险(如CSRF攻击)。
LocalStorage:数据会永久存储在浏览器中,除非手动删除,大小限制一般为5MB,不随HTTP请求发送到服务器。SessionStorage:数据在会话期间有效,关闭窗口或标签页后数据会被清除,大小限制一般为5MB,不随HTTP请求发送到服务器。
-
使用场景:Cookie用于在客户端和服务器之间传递数据,如用户登录状态;
LocalStorage用于长期保存数据,如用户的偏好设置;SessionStorage用于临时保存会话期间的数据,如购物车信息。
-
性能优化
-
如何优化JavaScript代码的性能,有哪些常见的技巧?
- 常见技巧包括:减少全局变量的使用,避免变量污染和查找时间过长;使用事件委托减少事件监听器的数量;避免使用
with语句和eval函数;合理使用缓存,避免重复计算;使用requestAnimationFrame代替setTimeout和setInterval进行动画处理;压缩和合并代码,减少HTTP请求。
- 常见技巧包括:减少全局变量的使用,避免变量污染和查找时间过长;使用事件委托减少事件监听器的数量;避免使用
-
解释事件循环机制,宏任务和微任务的区别是什么?
- 事件循环是JavaScript处理异步任务的机制,JavaScript是单线程的,同一时间只能做一件事。事件循环先执行同步任务(主线程排队),再处理异步任务。异步任务分为宏任务和微任务,微任务优先级更高,会在宏任务之前执行。宏任务如
setTimeout、setInterval等,微任务如Promise.then、MutationObserver等。
- 事件循环是JavaScript处理异步任务的机制,JavaScript是单线程的,同一时间只能做一件事。事件循环先执行同步任务(主线程排队),再处理异步任务。异步任务分为宏任务和微任务,微任务优先级更高,会在宏任务之前执行。宏任务如
-
如何避免内存泄漏,有哪些常见的原因和解决方法?
- 常见原因包括:意外的全局变量、未清除的定时器或回调函数、闭包、DOM引用未释放等。解决方法有:避免意外的全局变量,使用严格模式;在组件销毁时清除定时器和回调函数;合理使用闭包,避免不必要的闭包;及时释放不再使用的DOM引用。
工程化与工具
-
介绍常见的前端构建工具,如Webpack、Vite等,它们的原理和区别是什么?
- Webpack是一个模块打包工具,通过loader处理不同类型的模块,用plugin扩展功能。原理是从入口文件开始,递归分析所有依赖的模块,将这些模块打包成一个或多个文件。Vite是基于ES模块的开发服务器和构建工具,利用浏览器原生ES模块的支持,实现快速冷启动。区别在于Webpack功能强大,适用于大型复杂项目,配置复杂;Vite冷启动快,开发体验好,适用于小型项目和快速迭代开发。
-
如何进行代码规范和质量检查,有哪些工具可以使用?
- 可使用ESLint进行代码规范检查,通过配置规则检查代码是否符合规范;使用Prettier进行代码格式化,使代码风格统一。还可使用Jest、Mocha等工具进行单元测试,确保代码质量。
-
简述前端自动化测试的重要性和常见的测试框架。
- 重要性:提高代码质量、减少人为错误、便于代码重构、增强团队协作。常见测试框架有Jest(功能强大、易于配置和使用)、Mocha(灵活、可搭配其他断言库)、Jasmine(自带断言和模拟功能)等。
鸿蒙ArkTS部分
基础概念
-
什么是ArkTS,它与TypeScript有什么关系和区别?
- ArkTS是HarmonyOS优选的主力应用开发语言,保持了TypeScript的基本风格,同时通过规范定义强化开发期静态检查和分析,提升程序执行稳定性和性能。它基于TypeScript扩展,强化静态类型检查,新增鸿蒙专属语法,如装饰器、状态管理等。与TypeScript的区别在于扩展了UI(定义声明式UI描述、自定义组件等),强化了检查(不支持部分TypeScript特性)。
-
简述ArkTS的核心优势和特点,如声明式UI、状态管理等。
- 核心优势和特点包括:静态类型检查,在编译时进行类型检查,有助于在代码运行前发现和修复错误;声明式UI,允许开发者以更简洁、更自然的方式开发跨端应用;状态管理,提供多维度的状态管理机制,支持单向和双向数据流;渲染控制,支持条件渲染、循环渲染和数据懒加载;兼容性,兼容TS/JavaScript生态,可使用TS/JS进行开发或复用已有代码;并发机制,支持轻量化的并发机制,提高应用的性能和响应速度。
语法特性
-
介绍ArkTS中的装饰器,如@Entry、@Component、@State等,它们的作用是什么?
@Entry:标记应用入口组件。@Component:定义可复用的UI组件。@State:管理组件内部状态,状态变化会触发UI更新。@Prop:父组件向子组件传递单向数据。@Link:实现父子组件双向数据绑定。@Provide和@Consume:用于与后代组件的双向数据同步,适用于状态数据在多个层级之间传递的场景。
-
解释ArkTS中的状态管理机制,如何实现组件之间的状态同步?
- ArkTS通过装饰器实现状态管理,如
@State管理组件内部状态,@Prop实现单向数据传递,@Link实现双向数据绑定,@Provide和@Consume用于跨层级数据同步。实现组件之间的状态同步可通过以下方式:父子组件使用@Prop和@Link;跨组件使用@Provide和@Consume;对于多层嵌套的情况,使用@Observed和@ObjectLink。
- ArkTS通过装饰器实现状态管理,如
开发实践
-
如何进行ArkTS应用的开发,包括环境搭建、项目结构等。
- 环境搭建:需安装DevEco Studio,获取OpenHarmony SDK,搭建烧录环境(如使用RK3568开发板),完成开发环境配置,使用工程向导创建工程(模板选择“Empty Ability”),选择真机进行调测。项目结构一般包括代码区(如
entry/src/main/ets)和资源文件区(如entry/src/main/resources)。
- 环境搭建:需安装DevEco Studio,获取OpenHarmony SDK,搭建烧录环境(如使用RK3568开发板),完成开发环境配置,使用工程向导创建工程(模板选择“Empty Ability”),选择真机进行调测。项目结构一般包括代码区(如
-
简述ArkTS应用的生命周期管理,以及如何处理组件的创建和销毁。
- 页面生命周期(被
@Entry装饰的组件):onPageShow(页面每次显示时触发一次)、onPageHide(页面每次隐藏时触发一次)、onBackPress(用户点击返回按钮时触发)。组件生命周期(一般用@Component装饰的自定义组件):aboutToAppear(组件即将出现时回调)、onDidBuild(组件build()函数执行完成之后回调)、aboutToDisappear(自定义组件析构销毁之前执行)。在aboutToDisappear中可进行资源释放等操作,但不允许改变状态变量。
- 页面生命周期(被
框架与组件
-
介绍ArkUI框架的主要组件和布局容器,如何实现自定义组件?
- 主要组件有
Text(显示文本)、Button(按钮)、Image(图片)等;布局容器有Column(纵向排列)、Row(横向排列)、Stack(层叠)等。实现自定义组件步骤:用@Component定义结构体,build()返回UI描述。例如:
- 主要组件有
@Component
struct CustomButton {
build() {
Button('Custom Button').onClick(() => {
console.log('Button clicked');
})
}
}
-
如何提高ArkTS应用的性能,有哪些优化策略?
- 优化策略包括:避免频繁更新
@State,使用@ObjectLink处理复杂对象;列表渲染时通过ForEach的keyGenerator提升复用效率;及时释放资源,在aboutToDisappear中取消定时器、关闭文件句柄;减少全局变量,优先使用@State替代AppStorage。
- 优化策略包括:避免频繁更新
典型面试题示例
-
如何实现父子组件间的双向绑定?
- 子组件使用
@Link修饰变量,父组件通过$符号传递(如$variable)。例如:
- 子组件使用
// 父组件
@Component
struct Parent {
@State count: number = 0
build() {
Child({ count: $count })
}
}
// 子组件
@Component
struct Child {
@Link count: number
build() {
Button('Increment').onClick(() => {
this.count++
})
}
}
- 指出以下代码的问题并给出解决方案:
@Component
struct Demo {
@State count: number = 0
build() {
Button('Click').onClick(() => {
this.count += 1
})
}
}
- 问题:缺少`@Entry`装饰器,无法作为入口组件;`Button`未添加到布局容器(如`Column`)中。解决方案:添加`@Entry`装饰器,将`Button`添加到布局容器中。
@Entry
@Component
struct Demo {
@State count: number = 0
build() {
Column() {
Button('Click').onClick(() => {
this.count += 1
})
}
}
}
-
如何实现一个跨设备同步的待办清单应用?
- 使用
DistributedDataManager同步数据,通过@Observed和@ObjectLink监听列表变化。例如,定义待办事项数据模型,使用DistributedDataManager进行数据存储和同步,在组件中使用@Observed和@ObjectLink监听数据变化并更新UI。
- 使用
面试题
-
你在前端工作中有什么是你引以为傲的,有什么优势
- 性能优化
- 效率提升