高频篇
CSS
盒模型
Q: 盒模型 A: CSS盒模型是布局的基础概念,每个元素被表示为一个矩形盒子,由content(内容)、padding(内边距)、border(边框)、margin(外边距)组成。标准盒模型(box-sizing: content-box)中,width/height仅作用于content区域,元素实际占用宽度 = width + padding + border。IE盒模型(box-sizing: border-box)中,width/height包含content+padding+border,更符合直觉,也是目前主流推荐。在实际开发中,通常用 * { box-sizing: border-box; } 全局重置,方便尺寸计算,避免padding和border撑大元素。
Q: BFC A: BFC(Block Formatting Context)是块级格式化上下文,是一个独立的渲染区域,内部元素的布局不会影响外部。触发BFC的条件包括:float不为none、overflow不为visible、display为inline-block/flex/grid/table-cell、position为absolute/fixed。BFC的主要应用:清除浮动(父元素触发BFC后可包含浮动子元素)、防止margin折叠(相邻块级元素的垂直margin会合并,但分属不同BFC的不会)、自适应两栏布局(左侧浮动,右侧触发BFC可避免环绕)。
Q: 选择器权重计算方式 A: CSS选择器优先级由四个层级(a,b,c,d)决定,可表示为千位、百位、十位、个位:行内样式(a=1, 1000)、ID选择器(b=1, 0100)、类/属性/伪类选择器(c=1, 0010)、元素/伪元素选择器(d=1, 0001)。!important具有最高优先级,可覆盖任何声明。当权重相同时,后定义的样式生效。注意:通配选择器*权重为0,组合器(如+, >)不增加权重。实际工程中应避免滥用!important,合理使用BEM命名规范可减少权重冲突。
Q: 清除浮动 A: 浮动元素会脱离文档流,导致父元素高度塌陷。清除浮动的经典方法:1)伪元素clearfix:父元素添加::after { content: ""; display: table; clear: both; },这是最推荐的方案,无额外标记,兼容性好。2)触发父元素BFC:设置overflow: hidden/auto,副作用是会裁剪溢出内容。3)额外空标签:在浮动元素末尾添加<div style="clear:both"></div>,但增加了无意义DOM。4)双伪元素法:同时清除前后浮动,防止margin折叠。
Q: 垂直居中的方案 A: 垂直居中方案视场景而定:1)已知宽高:absolute + 负margin(top:50%; left:50%; margin-left:-half-width; margin-top:-half-height)。2)未知宽高:absolute + transform(top:50%; left:50%; transform:translate(-50%,-50%))。3)flex布局:父元素display:flex; align-items:center; justify-content:center,最简洁通用。4)grid布局:父元素display:grid; place-items:center。5)table-cell:父元素display:table-cell; vertical-align:middle; text-align:center。6)inline-block写法:利用vertical-align:middle配合line-height。生产环境优先推荐flex方案,兼容性好且语义清晰。
Q: CSS3的新特性 A: CSS3引入了大量新特性:1)选择器增强:属性选择器、nth-child/nth-of-type、:not()等。2)盒模型增强:box-sizing、box-shadow、border-radius、border-image。3)背景与渐变:background-size、background-origin、linear-gradient/radial-gradient。4)过渡与动画:transition、@keyframes + animation。5)变换:transform(translate/rotate/scale/skew),支持2D和3D变换。6)弹性布局:Flexbox和Grid布局系统。7)媒体查询:@media实现响应式设计。8)自定义字体:@font-face。9)多列布局:column-count等。10)滤镜:filter属性(blur、grayscale等)。
Q: CSS动画和过渡 A: transition(过渡)需要触发条件(如hover),定义起始状态到结束状态的平滑变化,只有两个状态:from和to。animation(动画)则通过@keyframes定义多个关键帧,可自动播放、循环、反向等,控制更精细。transition适合简单交互效果(按钮hover变色),animation适合复杂持续动画(loading旋转、进场动画)。两者都可使用timing-function控制缓动曲线(ease、linear、cubic-bezier)。性能上优先使用transform和opacity做动画,因为它们由GPU合成,不会触发布局重排。
Q: 隐藏页面元素的方式 A: 常用隐藏方式及区别:1)display:none — 元素完全从文档流移除,不占空间,不可点击,会引起重排和重绘。2)visibility:hidden — 元素不可见但仍占空间,不可点击,只会引起重绘。3)opacity:0 — 元素不可见但仍占空间且可点击,触发重绘。4)position + 移出可视区:如 left:-9999px,元素移出屏幕。5)clip-path: polygon(0 0, 0 0, 0 0, 0 0) — 裁剪为零区域。6)transform: scale(0) — 缩放为0,占空间。7)aria-hidden="true" — 语义化隐藏,仅对屏幕阅读器隐藏。根据需求选择,如动画常用opacity,语义隐藏用display。
Q: em/px/rem/vh/vw区别 A: px是绝对单位,1px对应屏幕一个物理像素点,不受父元素影响。em相对父元素font-size,1em等于父元素字体大小,用在margin/padding时常根据自身font-size计算,容易造成复合嵌套时难以追踪。rem相对根元素(html)font-size,通过媒体查询调整根字体大小可实现全局缩放,适合响应式布局。vh和vw分别相对视口高度和宽度的1%,vmin取vw和vh较小值,vmax取较大值,适合全屏布局和字体自适应。移动端适配常用rem或vw方案。
Q: flex布局 A: Flexbox是一维布局模型,通过display:flex或display:inline-flex触发。容器属性:flex-direction(主轴方向)、flex-wrap(换行)、justify-content(主轴对齐)、align-items(交叉轴对齐)、align-content(多行对齐)。项目属性:flex(flex-grow flex-shrink flex-basis缩写)、align-self(单独对齐)、order(排序)。常见的面试考点:flex:1等价于flex-grow:1; flex-shrink:1; flex-basis:0%,项目会均分剩余空间。圣杯布局、垂直居中、两栏/三栏自适应布局都可优雅实现。Flexbox解决了传统布局中float和inline-block难以处理的问题。
Q: CSS性能优化方法 A: CSS性能优化策略:1)减少重排重绘:避免频繁修改布局属性(width/height/margin),改用transform和opacity做动画。2)选择器优化:避免过深嵌套(如Sass嵌套不要超过3层),避免通配选择器和昂贵伪类(:nth-last-child)。3)使用简写属性:如margin:10px替代margin-top/lr等四条声明。4)合理使用will-change:提前告知浏览器哪些属性会变化,但不要滥用。5)压缩CSS文件,移除未使用的CSS(可用PurgeCSS)。6)关键CSS内联,非关键CSS异步加载。7)使用CSS contain属性隔离渲染区域。8)避免CSS表达式和@import。
Q: 画0.5px线 A: 在高DPI(如Retina)屏幕上,1px物理线显示为CSS 0.5px。实现方案:1)transform: scale(0.5) — 给伪元素设置1px边框后scaleY(0.5),最通用。2)使用box-shadow: 0 0.5px 0 #000,利用阴影模拟细线。3)通过meta viewport设置initial-scale=0.5(仅适合移动端整体缩放)。4)SVG方案:使用<line stroke-width="0.5">。5)直接设置border-width: 0.5px(部分浏览器支持)。推荐使用transform方案,兼容性好且不影响布局。
888
- Q: 画三角形 A: 利用CSS边框的斜角特性画三角形:宽高设为0,设置四个边框颜色,需要哪边就保留该方向相邻两边的颜色。例如画朝上的三角形:width:0; height:0; border: 50px solid transparent; border-bottom-color: red; 。可调整边框宽度控制大小,也可用border-width单独控制各边宽来画直角三角形。进阶用法:用伪元素+三角形实现气泡聊天框的尖角。也可用clip-path: polygon() 画任意三角形,更灵活。
- Q: 两栏布局方案 A: 两栏布局(左侧固定,右侧自适应)常用方案:1)flex布局:父display:flex,左侧固定宽度,右侧flex:1。最简单推荐。2)float + BFC:左侧float:left固定宽度,右侧overflow:hidden触发BFC。3)float + margin:左侧float:left,右侧margin-left等于左侧宽度。4)position + margin:左侧position:absolute,右侧margin-left。5)grid布局:父display:grid; grid-template-columns: 200px 1fr。6)table布局:父display:table; 子display:table-cell。生产环境推荐flex方案,代码简洁且无需清除浮动。
JavaScript
Q: typeof类型判断 A: typeof操作符返回一个字符串,表示操作数的类型。返回值有:"undefined"、"boolean"、"number"、"string"、"symbol"、"function"、"object"、"bigint"。注意:typeof null === "object",这是JS的遗留bug(null的二进制前三位为0,被识别为object)。typeof数组和普通对象都返回"object",无法区分。typeof NaN === "number"。判断具体类型应使用Object.prototype.toString.call(),可返回[object Array]/[object Date]等精确类型。typeof适合判断基本类型和函数。
Q: 类型转换 A: JS是弱类型语言,类型转换分为隐式和显式。显式转换:Number()、String()、Boolean()、parseInt()、parseFloat()。隐式转换发生在:1)字符串拼接(+操作符有一方是字符串时)。2)比较运算(==会进行类型转换,如null == undefined为true,0 == ""为true)。3)条件判断(if/while中的值会被转为布尔值)。4)数学运算(-、*、/等会将字符串转为数字)。对象转原始值会先调用valueOf()再调用toString()。建议尽量使用===避免隐式转换的坑。
Q: 闭包 A: 闭包是指函数能够访问其外部函数作用域中变量的能力,即使外部函数已执行完毕。JavaScript中函数创建时会形成闭包,内部函数保存对外部函数变量的引用。经典场景:1)函数柯里化(返回函数逐步接收参数)。2)数据私有化(封装私有变量不暴露。3)回调函数和事件处理(保持上下文)。4)循环中异步操作(使用闭包保存循环变量值)。闭包带来的内存问题:被引用的变量不会被垃圾回收,使用后需手动释放(赋值为null)。闭包是现代JS中模块模式、高阶函数的核心基础。
Q: 原型与原型链 A: 每个函数都有prototype属性(指向原型对象),每个对象都有__proto__属性(指向构造函数的prototype)。当访问对象属性时,如果对象本身没有,会沿着__proto__链向上查找,直到找到或到达Object.prototype.__proto__为null。这就是原型链机制。原型链实现了JS的继承:实例共享原型上的属性和方法。constructor属性指回构造函数本身。原型链的终点是Object.prototype,其上定义了toString、hasOwnProperty等方法。使用instanceof操作符可检查对象在原型链上是否有某构造函数的prototype。
Q: 原型继承和Class继承 A: 原型继承通过设置子构造函数的prototype指向父类的实例实现:Child.prototype = new Parent();但这样做子类实例会共享父类引用属性。Class继承是ES6语法糖,本质仍是原型链,写法更清晰:class Child extends Parent { constructor() { super(); } }。super()必须先调用才能使用this,因为ES6 class的this由父类创建。Class继承解决了原型继承的问题:1)子类可覆盖父类方法。2)支持super调用父类方法。3)实例不会意外共享属性。Class中定义的方法不可枚举,这是与ES5方式的一个区别。
Q: 模块化 A: JS模块化发展历程:IIFE(立即执行函数避免全局污染)→ CommonJS(Node.js使用,require/module.exports,同步加载,值拷贝)→ AMD(RequireJS,浏览器端异步加载,define/require)→ CMD(SeaJS,按需加载)→ ES Module(ES6标准,import/export,静态分析,值引用,支持tree-shaking)。ES Module是官方标准,在浏览器中使用(<script type="module">)
,编译时确定依赖关系,支持异步加载。CommonJS和ES Module的主要区别:前者是运行时加载输出值的拷贝,后者是编译时加载输出值的引用。现代工程中使用ES Module作为标准。
Q: 事件机制 A: DOM事件机制分为三个阶段:捕获阶段(从window向下到目标元素)、目标阶段(到达目标元素)、冒泡阶段(从目标元素向上到window)。addEventListener第三个参数为true时在捕获阶段触发,默认为false在冒泡阶段触发。事件冒泡允许事件代理(event delegation),在父元素上监听子元素事件,减少内存占用。阻止传播:stopPropagation()阻止冒泡,stopImmediatePropagation()同时阻止该元素上其他同类型事件。preventDefault()阻止默认行为(如链接跳转)。现代框架(React/Vue)都封装了事件系统,但理解原生机制有助于调试性能问题。
Q: 箭头函数 A: 箭头函数是ES6引入的简洁函数写法,与普通函数的区别:1)箭头函数没有自己的this,this指向定义时所在外层作用域的this(词法绑定),且无法通过call/apply/bind改变。2)不能用作构造函数,不能new。3)没有arguments对象,可用rest参数代替。4)不能使用yield,不能作为Generator函数。5)没有prototype属性。6)语法上,单参数可省略括号,单行返回值可省略return和花括号。箭头函数适合短回调函数(如数组方法map/filter),但不适合作为对象方法或需要动态this的场景。
Q: JS内存泄露检测 A: JS常见内存泄露场景:1)全局变量意外创建(未声明直接赋值)。2)闭包保持外部变量引用未被释放。3)DOM引用未清除(js仍持有已移除DOM的引用)。4)定时器/事件监听未清理(setInterval、addEventListener未移除)。5)脱离DOM的引用(Map/Set中的DOM引用)。检测工具:Chrome Memory面板可录制堆快照对比,使用Allocation Timeline记录内存分配,Performance面板观察内存变化趋势。实战排查:反复触发可疑操作,手动GC后拍快照,比较对象数量和retaining path找到未释放的引用。
Q: async/await异步总结 A: async/await是ES2017引入的异步编程语法糖,基于Promise实现。async函数返回一个Promise,内部使用await暂停执行直到Promise完成。错误处理使用try/catch捕获reject,比Promise链式.catch更直观。注意:1)await只能在async函数中使用。2)多个无依赖的异步操作应使用Promise.all并发执行而非顺序await(串行慢)。3)for-await-of可遍历异步迭代器。4)async函数内部的return值会包装为resolved Promise。5)async/await并不能完全替代Promise,两者互补使用。在React/Vue项目中组件初始化异步请求时常用async/await。
Q: Promise异步总结 A: Promise是ES6引入的异步编程解决方案,有三种状态:pending(待定)、fulfilled(已兑现)、rejected(已拒绝),状态一旦改变不可逆。核心API:resolve/reject改变状态,then(onFulfilled, onRejected)返回新Promise支持链式调用,catch()处理错误,finally()无论结果都执行。静态方法:Promise.all(全部成功或一个失败)、Promise.race(最快一个)、Promise.allSettled(全部完成不管结果)、Promise.any(第一个成功)。微任务特性:.then/.catch/.finally的回调属于微任务,在宏任务执行完后、下一个宏任务前执行。Promise解决了回调地狱问题。
Q: Event Loop执行机制 A: JavaScript是单线程语言,Event Loop是其异步执行机制。执行顺序:1)执行当前宏任务中的同步代码。2)执行当前微任务队列中的所有任务(按先进先出)。3)执行下一个宏任务(从宏任务队列取出一个)。宏任务包括:script整体代码、setTimeout、setInterval、I/O、UI渲染、setImmediate(Node)。微任务包括:Promise.then/catch/finally、MutationObserver、process.nextTick(Node)、queueMicrotask。在每一轮Event Loop中,微任务可能产生新的微任务,这些新任务会在本轮继续执行直到队列清空。
浏览器
Q: 储存 A: 浏览器存储方案对比:1)Cookie:4KB限制,每次请求自动携带(同源),可设置过期时间,常用于会话管理。2)localStorage:5MB限制(同源),持久化存储,不随请求发送,适合长期存储用户偏好。3)sessionStorage:同localStorage但生命周期为页面会话,关闭标签页即清除。4)IndexedDB:异步操作,结构化数据存储(支持索引),容量大(通常>250MB),适合存储大量离线数据。5)Cache API:配合Service Worker实现离线缓存。选择建议:小数据用localStorage,复杂查询用IndexedDB,离线策略用Cache API。
Q: 浏览器缓存机制 A: 浏览器缓存分为强缓存和协商缓存。强缓存:第一次请求时服务器返回Cache-Control(优先级高)或Expires,浏览器在有效期内直接使用本地缓存不发请求。Cache-Control: max-age=3600表示1秒内有效。协商缓存:强缓存失效后,浏览器携带If-Modified-Since(对应Last-Modified,精度到秒)或If-None-Match(对应ETag,优先级高,内容变化时改变)发请求,服务器判断资源未修改则返回304 Not Modified。ETag比Last-Modified更精确(可感知内容微小变化),但计算开销稍大。推荐两者配合使用。
Q: 从输入URL到网页显示的过程 A: 完整过程:1)DNS解析:浏览器缓存→系统缓存→路由器缓存→根服务器,将域名转为IP。2)TCP连接:三次握手建立连接,HTTPS还会进行TLS握手。3)发送HTTP请求:构建请求行/头/体。4)服务器处理并返回HTTP响应。5)浏览器解析HTML构建DOM树,同时解析CSS构建CSSOM树。6)遇到<script>(无async/defer)会阻塞解析。7)合并DOM树和CSSOM树生成Render Tree(渲染树,只包含可见元素)。8)Layout(回流):计算每个节点的几何位置。9)Paint(绘制):将像素绘制到屏幕。10)Composite(合成):GPU合成图层。关键优化:减少重排重绘,首屏关键CSS内联。
Q: 前端攻击方式 A: 常见前端攻击及防御:1)XSS(跨站脚本攻击):攻击者注入恶意脚本。防御:输入过滤、输出转义(将< >等转为实体字符)、CSP(Content Security Policy)限制可加载的脚本来源。2)CSRF(跨站请求伪造):诱导用户点击链接利用其登录态发送恶意请求。防御:同源检测、CSRF Token、SameSite Cookie属性。3)点击劫持:透明iframe覆盖。防御:X-Frame-Options: DENY。4)中间人攻击:HTTPS和HSTS。5)SQL注入:后端使用参数化查询。6)DDoS前端层面可用CDN和限流。
Q: 跨域方案 A: 跨域指不同协议/域名/端口间的请求受限,由同源策略控制。常见跨域方案:1)CORS(跨域资源共享):后端设置Access-Control-Allow-Origin等响应头,最推荐方案。2)JSONP:利用script标签无跨域限制,仅支持GET请求。3)代理服务器:开发环境用webpack-dev-server proxy,生产环境用Nginx反向代理。4)postMessage + iframe:不同窗口间通信。5)WebSocket:天然支持跨域。6)document.domain + iframe:仅限主域相同。7)window.name + iframe。CORS是标准解决方案,需后端配合配置。
Q: 移动端300ms延迟解决 A: 移动端点击300ms延迟源于浏览器需要判断用户是单击还是双击缩放。解决方案:1)<meta name="viewport" content="width=device-width, initial-scale=1.0"> — 现代浏览器在viewport设为设备宽度后默认取消300ms延迟。2)使用touch-action: manipulation CSS属性,禁止双击缩放。3)FastClick库:通过touch事件模拟click,消除延迟。4)使用touch事件代替click(如touchstart/touchend)。目前主流做法是meta viewport配合touch-action,因为现代浏览器(iOS 9.3+、Android Chrome 32+)已原生修复此问题。
Q: 多标签通讯 A: 浏览器多标签页间通信方案:1)BroadcastChannel API(推荐):通过new BroadcastChannel('channel')创建,postMessage发送消息,onmessage接收,同源标签间通信。2)localStorage + storage事件:一个标签修改localStorage,同源其他标签监听window.onstorage事件。3)SharedWorker:多标签共享后台线程,通过MessageChannel通信。4)Service Worker:通过postMessage与客户端通信。5)postMessage + iframe或window.open。6)WebSocket中转:通过服务器转发。按需选择,简单场景用BroadcastChannel或localStorage。
Q: requestIdleCallback与requestAnimationFrame区别 A: requestAnimationFrame(rAF)在每次浏览器重绘前执行,频率与屏幕刷新率同步(通常60fps),适合执行动画和视觉更新。requestIdleCallback(rIC)在浏览器空闲时执行,不阻塞关键渲染路径,适合执行低优先级任务(如数据上报、预计算)。两者的核心区别:rAF有固定执行周期保证视觉流畅,rIC无固定执行时间可能在几帧后才执行。rIC的callback接收IdleDeadline对象,通过timeRemaining()获取剩余空闲时间。rIC兼容性不如rAF,React的Scheduler机制模拟了类似rIC的功能。
Q: script标签defer与async区别 A: defer和async都是使脚本异步加载(不阻塞HTML解析)的属性。区别:defer是延迟执行,多个defer脚本按出现顺序在HTML解析完成后、DOMContentLoaded事件前执行。async是加载完立即执行,多个async脚本不保证执行顺序,谁先下载完谁先执行。defer适合需要等待DOM的脚本或依赖其他脚本的库引用。async适合独立第三方脚本(如流量统计、广告),不依赖于其他脚本。没有这两个属性时,普通script会阻塞HTML解析,直到下载并执行完。
Vue2
Q: 响应式原理 A: Vue2响应式基于Object.defineProperty实现。核心是Observer类递归遍历data对象的所有属性,通过Object.defineProperty将它们转为getter/setter。每个属性拥有一个Dep(依赖收集器),getter中触发dep.depend()收集当前Watcher,setter中触发dep.notify()通知所有Watcher更新。数组通过重写7个变更方法(push/pop/shift/unshift/splice/sort/reverse)实现响应式。局限:1)无法检测对象属性的新增/删除(需Vue.set/Vue.delete)。2)不能检测数组索引和长度的直接修改。3)对深层对象递归劫持有性能开销。Vue3用Proxy解决了这些问题。
Q: vdom和diff算法 A: 虚拟DOM(vdom)是用JS对象模拟真实DOM结构,通过对比新旧vdom的差异(diff)来最小化DOM操作。Vue2的diff算法基于Snabbdom,采用双端比较策略。过程:1)只做同层比较,不跨层级。2)从新旧子节点数组的两端同时向中间遍历。3)比较策略:头头、尾尾、头尾、尾头,若都不匹配则用key查找。4)找到可复用节点则打补丁(patch),否则新增或删除。key的作用是标识节点身份,帮助diff算法更高效地复用和移动节点。正确使用key可减少不必要的DOM操作,提高渲染性能。
Q: 模板编译 A: Vue2模板编译分为三个阶段:1)解析(parse):将template字符串解析为AST(抽象语法树),通过正则匹配和处理HTML标签、属性、指令(v-if/v-for/v-bind等)、插值表达式()。2)优化(optimize):遍历AST标记静态节点(不会变化的节点),在patch时可以跳过静态节点提高性能。3)生成(generate):将AST转换为render函数字符串,最终通过new Function()生成render函数。render函数执行返回vnode。Vue实例挂载时如果提供了render函数则直接使用,否则编译template。vue-loader可以在构建时预编译模板,减少运行时开销。
Q: Vue组件渲染过程 A: Vue组件渲染过程:1)创建Vue实例,合并选项(options)。2)调用beforeCreate钩子,初始化生命周期/事件。3)调用created钩子,初始化data/props/watch/computed的响应式系统。4)编译模板或使用render函数。5)调用beforeMount钩子。6)创建Watcher实例(渲染Watcher),执行render函数生成vnode。7)调用patch函数将vnode转为真实DOM挂载到页面。8)调用mounted钩子。9)数据变化时,响应式系统通知渲染Watcher重新执行render生成新vnode,patch对比新旧vnode差异后更新DOM。此过程体现了数据驱动视图的核心思想。
Q: 组件通信方式 A: Vue2组件通信方式:1)props/$emit — 父子组件通信,父传子用props,子传父用$emit触发事件。2)$parent/$children — 直接访问组件实例(不推荐,耦合度高)。3)$refs — 通过ref获取子组件实例(直接调用方法或访问属性)。4)provide/inject — 祖先组件通过provide提供数据,后代通过inject注入,适合跨层级(如主题配置)。5)eventBus — 创建全局Vue实例作为事件总线,通过$on/$emit/$off实现任意组件通信(适合小项目)。6)Vuex — 集中式状态管理,适合复杂数据流。7)$attrs/$listeners — 透传属性和事件。
Q: 生命周期方法 A: Vue2生命周期钩子(共8个):beforeCreate(实例创建前,data不可访问)→ created(实例创建完成,可访问data但DOM未挂载)→ beforeMount(挂载前,render首次调用)→ mounted(DOM已挂载,可操作DOM,不承诺子组件也挂载完)→ beforeUpdate(数据变化后重新渲染前)→ updated(渲染完成,避免在此修改数据导致死循环)→ beforeDestroy(实例销毁前,清除定时器/事件监听)→ destroyed(实例销毁完成)。父子组件生命周期顺序:父beforeCreate→父created→父beforeMount→子...mounted→父mounted。销毁:父beforeDestroy→子...destroyed→父destroyed。
Q: 统一监听报错 A: Vue2中统一监听错误的方法:1)全局错误处理器:Vue.config.errorHandler = (err, vm, info) => {}。可捕获组件渲染和观测期间的同步错误,但无法捕获异步错误和原生事件错误。2)window.onerror:捕获未被try/catch的运行时错误(包括异步),但无法捕获Promise异常。3)window.addEventListener('unhandledrejection'):捕获未被catch的Promise错误。4)组件内errorCaptured钩子:捕获后代组件抛出的错误,返回false可阻止向上传播。生产环境建议组合使用Vue.config.errorHandler + window.onerror + unhandledrejection实现全方位错误监控。
Q: Vue优化实践 A: Vue2性能优化策略:1)合理使用v-if和v-show(频繁切换用v-show,运行时不变用v-if)。2)v-for加key并避免与v-if同用(可用计算属性过滤)。3)使用Object.freeze()冻结不响应的大数据对象。4)函数式组件(functional: true)减少渲染开销。5)keep-alive缓存动态组件减少重新渲染。6)异步组件和路由懒加载减少首屏包体积。7)使用computed代替methods(计算属性有缓存)。8)大型列表用虚拟滚动(vue-virtual-scroller)。9)使用事件委托减少事件监听器数量。10)用v-once标记静态内容。
Vue3
Q: vue3对vue2优势 A: Vue3相比Vue2的主要优势:1)性能提升:打包体积减少41%,初次渲染快55%,更新快133%,内存使用减少54%。2)Composition API:更好的逻辑复用和代码组织方式,解决了Options API在复杂组件中逻辑分散的问题。3)Proxy代替Object.defineProperty:可监听新增/删除属性、数组索引变更,无需Vue.set。4)Tree-shaking友好:按需引入API,未使用的API不被打包。5)TypeScript支持更好:使用TS重写,类型推导更完善。6)新增Fragments(多根节点)、Teleport、Suspense等内置组件。7)更快的diff算法(PatchFlag标记静态节点)。8)更好的SSR性能。
Q: 生命周期区别 A: Vue3生命周期与Vue2的对应关系:beforeCreate → setup(),created → setup(),beforeMount → onBeforeMount,mounted → onMounted,beforeUpdate → onBeforeUpdate,updated → onUpdated,beforeDestroy → onBeforeUnmount,destroyed → onUnmounted。主要变化:1)beforeCreate和created被setup替代(它们本就是同步执行,不需要显式钩子)。2)销毁钩子重命名:beforeDestroy→onBeforeUnmount,destroyed→onUnmounted(语义更准确)。3)新增onRenderTracked和onRenderTriggered用于调试跟踪依赖和更新触发。4)所有生命周期钩子需要在setup中调用。
Q: Composition API与Options API理解 A: Options API以选项配置对象组织代码(data/methods/computed/watch),优点是结构清晰易上手,缺点是复杂组件中同一功能的逻辑被拆散在不同选项中(比如一个功能涉及data、methods、watch三处)。Composition API则按功能组织代码,使用setup函数将相关逻辑聚合在一起,通过ref/reactive定义响应式数据,通过computed/watch等创建派生状态和副作用。Composition API的优势:1)逻辑复用更灵活(替代mixins,无命名冲突和来源不明问题)。2)更好的TypeScript类型推断。3)大型组件代码组织更清晰。两者可混用,Vue3完全兼容Options API。
Q: ref使用 A: ref是Vue3中创建响应式数据的API,接收一个值并返回一个响应式对象,该对象有一个.value属性。基本类型通过ref包裹后变为响应式,对象类型ref内部会调用reactive处理。模板中使用ref时自动解包(无需.value),但在setup和computed中需要.value访问。用于DOM访问:<div ref="myRef">,在setup中const myRef = ref(null),onMounted时myRef.value获取真实DOM。也可用于组件引用(获取子组件实例)。ref可接受任意类型的值,包括函数。
Q: toRef/toRefs使用 A: toRef和toRefs用于处理响应式对象的解构问题。toRef(reactiveObj, 'key')基于响应式对象的某个属性创建ref,保持与原属性的响应式连接。toRefs(reactiveObj)将响应式对象的每个属性都转为ref,返回一个普通对象。使用场景:当需要解构reactive对象时,直接解构会失去响应式连接,使用toRefs解构可保持响应式。例如:const state = reactive({a:1, b:2}); const {a, b} = toRefs(state); 此时a.value和b.value的改变会同步到state。toRef适合只想对单个属性创建ref的场景。
Q: 为何需要ref/toRef/toRefs A: 之所以需要ref、toRef和toRefs,根源在于JavaScript的限制:基本类型不是引用传递,无法被直接转为响应式。ref的作用就是给基本类型包裹一层对象,利用对象的引用特性实现响应式。组合式函数中,ref让函数可以返回响应式值并能保持引用关系。toRefs解决reactive对象解构时数据丢失响应式的问题:const {a} = reactive({a:1})解构后a是普通值不会触发更新。toRefs将reactive每个属性转为ref保留连接。toRef则针对单个属性。三者共同确保Vue3响应式系统在变量传递和结构变换中连接不中断。
Q: vue3重要功能升级 A: Vue3的重要功能升级包括:1)Proxy响应式:可监听新增/删除属性、数组索引和长度变化。2)Composition API:setup、ref、reactive、computed、watch等。3)Teleport:将组件渲染到指定DOM节点(如模态框到body)。4)Suspense:等待嵌套异步组件的加载状态。5)Fragments:支持多根节点组件。6)Emits声明:组件通过emits选项声明触发的事件。7)自定义指令API变更:钩子函数名与组件生命周期对齐。8)v-model变化:支持多个v-model绑定,可自定义修饰符。9)异步组件定义变化:defineAsyncComponent。10)全局API tree-shaking:Vue.nextTick等需按需导入。
Q: Composition API逻辑复用 A: Composition API通过自定义组合函数(Composables)实现逻辑复用,替代Vue2的mixins。典型的Composable是一个以use开头的函数,内部使用ref/reactive定义状态、computed派生值、watch/watchEffect副作用,最后返回需要暴露的状态和方法。使用:setup中调用useXxx()并解构出需要的数据。相比mixins的优势:1)无命名冲突(变量明确来源于哪个Composable)。2)逻辑来源清晰(调用处可看到每个变量的来源)。3)可以嵌套组合(Composable内部调用其他Composable)。4)类型推导更好。5)无隐式依赖。
Q: Vue3响应式实现 A: Vue3响应式基于Proxy和Reflect实现。reactive函数使用Proxy代理目标对象,拦截get/set/deleteProperty/has等操作。get中通过track函数收集依赖(存入targetMap→depsMap→dep Set结构),set中通过trigger函数触发更新。ref底层对基本类型创建一个{value: ...}对象并用reactive处理(或使用单独的RefImpl类实现get value/set value)。effect函数是最底层的副作用,watchEffect基于effect实现,computed基于effect实现惰性求值和缓存。相比Vue2的Object.defineProperty,Proxy可监听整个对象而非属性,支持数组和动态属性。
Q: Proxy基本使用 A: Proxy是ES6新增的元编程特性,用于创建对象的代理,可拦截和自定义对象的基本操作。语法:new Proxy(target, handler)。handler对象包含trap函数,常用:get(读取属性)、set(设置属性)、deleteProperty(删除属性)、has(in操作符)、ownKeys(Object.keys/for...in)、apply(函数调用)、construct(new操作)。Proxy与Object.defineProperty的关键区别:Proxy可代理整个对象(包括新增属性、数组索引),而defineProperty只能劫持已有的具体属性。Reflect是与Proxy配套的API,在Proxy trap中调用Reflect对应方法可保持默认行为。
Q: vue3用Proxy实现响应式 A: Vue3使用Proxy实现响应式的核心代码思路:function reactive(target) { return new Proxy(target, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver); track(target, key); // 依赖收集 return isObject(res) ? reactive(res) : res; // 深层代理 }, set(target, key, value, receiver) { const res = Reflect.set(target, key, value, receiver); trigger(target, key); // 触发更新 return res; }, deleteProperty(target, key) { const res = Reflect.deleteProperty(target, key); trigger(target, key); return res; } }); }。track函数将当前effect存入WeakMap结构。trigger从WeakMap中取出所有依赖effect并执行。相比Vue2的优势:无需Vue.set处理新增属性,无需特殊处理数组,且初始化时只需代理表层,在get时才深层代理(惰性)。
Q: v-model参数用法 A: Vue3中v-model的改进:1)组件上的v-model默认展开为modelValue prop和update:modelValue事件。2)支持多个v-model绑定:<Child v-model:title="title" v-model:content="content" />,分别对应title/update:title和content/update:content。3)支持自定义修饰符:v-model.capitalize,子组件接收modelModifiers prop。4)自定义v-model名和修饰符:defineProps({ modelValue: String, modelModifiers: { default: () => ({}) } }),defineEmits(['update:modelValue'])。Vue2的.sync修饰符(<Child :title.sync="title" />)在Vue3中可统一用v-model:title替代。
Q: watch与watchEffect区别 A: watch和watchEffect都用于执行副作用,区别:1)watch是惰性的,需要明确指定监听源(ref/reactive/函数/数组),且只在源变化时才执行回调,可访问旧值和新值。2)watchEffect自动追踪回调中使用的所有响应式依赖,立即执行一次(默认),依赖变化时重新执行。3)watch适合需要比较新旧值的场景(如保存草稿时判断数据是否变化),watchEffect适合简单副作用(如根据数据变化更新localStorage)。4)watch可配置deep/immediate/flush,watchEffect只能配置flush。5)watchEffect执行回调时拿不到旧值。
Q: setup中获取实例 A: Vue3的setup中获取组件实例的方式:1)getCurrentInstance():返回当前组件实例对象,包含ctx(普通上下文,开发环境可用)和proxy(代理实例,生产环境也可用)。但官方不推荐直接使用getCurrentInstance,因为它可能在未来版本改变。2)访问attrs/slots/emit:通过setup(props, { attrs, slots, emit }) 参数解构获取。3)ref访问子组件:<Child ref="childRef" /> 配合 const childRef = ref() 获取子组件实例(setup中需用defineExpose暴露方法)。4)provide/inject通过getCurrentInstance注入。推荐优先使用props/emit/attrs/slots等标准API,而不是直接操作实例。
Q: Vue3更快原因 A: Vue3比Vue2更快的原因:1)Proxy响应式:初始化时无需递归遍历所有属性,只在get时惰性代理深层对象。2)PatchFlag编译优化:编译时标记动态节点类型(文本/属性/事件等),diff时只对比带flag的节点,跳过静态节点。3)HoistStatic(静态提升):将静态节点提升到render函数外,每次渲染复用同一vnode。4)CacheHandler(事件缓存):内联事件处理函数会被缓存,不会因重新渲染而重建。5)Tree-shaking:按需引入API,减少打包体积。6)Fragment支持:无需单一根节点,减少不必要的包裹元素。7)SSR性能提升。综合这些优化,Vue3的初次渲染和更新性能相比Vue2提升50%-130%。
Q: PatchFlag A: PatchFlag是Vue3编译优化中的核心概念。编译器在解析模板时,会标记动态节点的具体类型,生成patchFlag位掩码。可能的flag值:TEXT(1,动态文本节点)、CLASS(2,动态class)、STYLE(4,动态style)、PROPS(8,动态属性)、FULL_PROPS(16,key-value动态属性)、HYDRATE_EVENTS(32,水化事件)、STABLE_FRAGMENT(64)、KEYED_FRAGMENT(128)、UNKEYED_FRAGMENT(256)、NEED_PATCH(512)、DYNAMIC_SLOTS(1024)。diff时通过位运算快速检查节点哪些部分变化,只更新变化部分。例如节点只有class变化,则只需对比class属性,大幅减少diff耗时。
Q: HoistStatic与CacheHandler A: HoistStatic(静态提升)是Vue3编译器优化:将模板中静态节点(无绑定表达式)的创建提升到render函数之外,在模块作用域内创建一次,每次渲染直接复用。例如一个纯文本<span>hello</span>,在Vue2中每次渲染都会创建新vnode,Vue3则只会创建一次。CacheHandler(事件缓存)解决内联事件函数问题:Vue2中@click="count + 1"每次渲染生成新函数,导致子组件不必要的重新渲染。Vue3通过缓存事件处理函数(编译时生成缓存代码),确保同一事件回调的引用不变。两者都减少了不必要的对象创建和对比开销。
Q: SSR与Tree-shaking优化 A: Vue3的SSR优化:编译时将静态内容直接输出为字符串(而非vnode树),避免客户端渲染时的vnode创建开销。对于静态节点采用渐进式水化(只对动态节点进行激活)。Tree-shaking优化:Vue3的核心库使用ES Module编写,未使用的API(如nextTick、transition、keep-alive等)不会打包到最终产物中。例如只使用reactive和ref,则完整打包体积从30KB+降至约10KB。Vite天然支持Tree-shaking,搭配ES Module开发时无需打包整个依赖。Webpack也可通过sideEffects配置启用Tree-shaking。
Q: Vite启动快的原因 A: Vite启动极快的核心原因是利用浏览器原生ES Module支持,摒弃了Webpack的打包构建模式。开发模式下:1)使用esbuild预构建依赖(Go语言编写,比JS打包器快10-100倍)。2)按需编译:浏览器请求哪个模块就编译哪个,无需打包整个项目。3)利用浏览器ES Module的http请求加载模块,源码直接以原始ES Module格式发送。4)使用原生ESM热更新,修改后只让浏览器重新请求变更模块,而非重新打包。5)利用HTTP缓存加速重复请求。生产模式使用Rollup打包,保证兼容性和优化。Vite的按需编译特性使项目规模不影响启动速度。
Q: Composition API与React Hooks对比 A: Composition API和React Hooks都旨在解决逻辑复用问题,但实现机制不同。相似点:都允许在函数组件/hooks函数中使用状态和副作用,都支持自定义组合逻辑。关键区别:1)React Hooks有严格的调用顺序要求(不能放在条件/循环中),依赖数组需手动声明;Composition API无调用顺序限制,依赖自动追踪。2)React Hooks每次渲染时都会重新执行,闭包陷阱常见;Vue3的setup只执行一次,通过响应式系统自动追踪依赖,无闭包问题。3)React Hooks依赖数组易遗漏导致bug;Vue3的watch/watchEffect自动收集依赖。4)Vue3的Composition API学习曲线相对更平缓。
React
Q: JSX本质 A: JSX是JavaScript的语法扩展,本质是React.createElement(type, props, ...children)的语法糖。Babel编译器将JSX转译为React.createElement调用,该函数返回虚拟DOM对象(React Element)。例如:<div className="app"><span>hello</span></div> 转译为 React.createElement('div', {className: 'app'}, React.createElement('span', null, 'hello'))。JSX允许在花括号{}中嵌入JS表达式(变量、函数调用、三元运算等)。由于是语法糖,必须有React在作用域内(React 17+的新JSX转换自动注入)。JSX本身不是模板引擎,而是编译时语法糖。
Q: 合成事件机制 A: React合成事件(SyntheticEvent)是对浏览器原生事件的一层包装,提供跨浏览器兼容的统一API。核心机制:1)事件委托:React不会将事件直接绑定到DOM元素上,而是在根节点(React 17前是document,17+是root容器)通过事件冒泡统一监听。2)事件池:React 16中SyntheticEvent会被复用(事件回调执行完后属性被置null,异步访问需调用e.persist()),React 17已废弃事件池。3)合成事件与原生事件的区别:合成事件通过冒泡机制工作,调用e.stopPropagation()不能阻止原生事件冒泡,反之亦然。4)合成事件可抹平浏览器差异(如stopPropagation、preventDefault的兼容性)。
Q: setState与batchUpdate机制 A: React的setState是异步的(实际上由batchUpdate机制控制同步还是异步)。在React生命周期函数和合成事件中,setState被批量处理:多个setState调用合并为一次更新,减少渲染次数。在setTimeout、原生事件、Promise异步回调中,setState是同步的(因为不在React的batchUpdate上下文中)。React 18引入了自动批处理(Automatic Batching),setTimeout/Promise中的setState也会被批处理。setState有两种传参:对象形式(setState({count: 1}))和函数形式(setState(prev => ({count: prev.count + 1}))),函数形式可避免因批处理导致的覆盖问题。
Q: jsx转vnode与render函数 A: JSX经过Babel编译为React.createElement调用,每次调用返回一个React Element对象(vnode)。React Element的结构:{ type, props, key, ref, $$typeof: Symbol(react.element) }。React 17+引入JSX Transform,可直接使用jsx()函数(需要import {jsx} from 'react/jsx-runtime'),无需显式import React。babel设置runtime: 'automatic'会自动使用新转换。render函数(class组件的render方法,或函数组件本身)执行时:1)检查props和state。2)调用React.createElement生成vnode树。3)通过Reconciler(协调器)比较新旧vnode树。4)提交DOM更新。
Q: 虚拟DOM是否快 A: 虚拟DOM并非绝对比直接操作DOM快。虚拟DOM的核心价值不是速度,而是声明式编程范式:开发者描述UI状态,React负责将状态映射为DOM。虚拟DOM的优势:1)提供跨平台能力(React Native)。2)批量更新和最小化DOM操作(减少重排重绘)。3)diff算法复用节点减少创建。4)抽象了DOM操作细节。虚拟DOM在首次渲染时比原生DOM慢(多了一层JS计算),但在频繁更新的场景中通过批量和差异化更新可减少实际DOM操作。对性能要求极高的场景(如列表动画),直接操作DOM或使用专门优化方案可能更优。虚拟DOM的本质是精确最小化DOM操作的JS算法。
Q: react组件渲染过程 A: React组件渲染过程分为三个阶段:1)触发阶段(Trigger):初始渲染或state/props变化。2)协调阶段(Render):React调用组件函数/class的render方法生成vnode树,从根节点开始递归创建子节点,通过Fiber架构可中断和恢复。Diff算法对比新旧vnode生成effects列表。3)提交阶段(Commit):React将effects列表应用到真实DOM。React 16+的Fiber架构将渲染拆分为可中断的工作单元,使高优先级更新(如用户输入)可打断低优先级渲染。useEffect在commit阶段后异步执行,useLayoutEffect在commit阶段同步执行。整个过程由React调度器控制执行时机。
Q: setState经典面试题 A: setState经典面试题考察异步更新和批处理机制。示例:class组件中 onClick = () => { this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); },结果count只增加1(因为两次合并为一次,后一次覆盖前一次)。改为函数式:this.setState(prev => ({count: prev.count + 1}))两次,结果增加2。在setTimeout中:三次setState不在batchUpdate上下文中,每次立即更新。React 18的自动批处理改变了此行为。另一考点:setState后立即console.log(this.state.count)拿不到最新值(异步),需在setState的回调中或componentDidUpdate中获取。
Q: useEffect闭包陷阱 A: useEffect闭包陷阱指effect回调中引用了旧的state/props值。原因:useEffect的回调函数在其创建时捕获了当时的state/props值,即使后续渲染更新了值,回调中仍然使用旧值。典型场景:useEffect(() => { const timer = setInterval(() => { setCount(count + 1); }, 1000); }, [])中count永远是初始值。解决方案:1)在依赖数组中包含count(但会导致定时器重置)。2)使用函数式更新setCount(c => c + 1)(不依赖外部变量)。3)使用useRef保存最新值(useEffect中通过ref.current访问)。4)使用useReducer将状态更新逻辑集中。5)重新设计组件结构避免闭包陷阱。
Q: Vue与React diff算法区别 A: Vue2的diff基于Snabbdom,采用双端比较策略:新旧子节点数组同时从头和尾向中间遍历(头头、尾尾、头尾、尾头四种比较),根据key移动和复用节点。React的diff采用单端比较(从左到右)加key匹配:先遍历新旧列表从头匹配相同key的节点,遍历完后再处理剩余节点,通过key找到可复用节点后执行移动操作。Vue的diff在列表头尾插入/移动时表现更好(双端遍历可更快发现),React的diff在列表尾部插入时更优。两者都是同层比较,时间复杂度O(n)。Vue3的diff增加了PatchFlag优化,进一步提升了对比效率。两者都要求使用稳定key(不用index)保证diff正确性。
Q: 统一监听React报错 A: React中统一监听错误的方案:1)Error Boundary(错误边界):class组件实现componentDidCatch(error, info)或static getDerivedStateFromError(error),捕获子组件渲染/生命周期/构造函数中的错误,无法捕获异步错误和事件处理中的错误。2)window.onerror:捕获未处理的运行时错误(包括异步)。3)window.addEventListener('unhandledrejection'):捕获未处理的Promise拒绝。4)React 16+的Error Boundary是声明式方案,用static getDerivedStateFromError显示降级UI,componentDidCatch上报错误。生产环境推荐组合使用Error Boundary包裹路由/重要模块,配合window.onerror和unhandledrejection兜底。
Q: React优化实践 A: React性能优化策略:1)React.memo包装组件避免不必要的重新渲染(浅比较props)。2)useMemo缓存计算结果防止重复执行昂贵计算。3)useCallback缓存函数引用防止子组件不必要渲染。4)避免在render中创建新对象/函数(内联对象导致每次props都变化)。5)使用useMemo代替useEffect+useState组合。6)列表使用稳定key,数据分页或虚拟滚动(react-window)。7)懒加载:React.lazy + Suspense拆分代码。8)使用Context按需拆分(避免context变化导致所有消费组件重渲染)。9)合理使用useRef保存不需要触发渲染的值。10)避免使用内联样式(每次创建新对象)。11)React 18的startTransition标记非紧急更新。
Q: React真题 A: React常见面试真题:1)受控组件与非受控组件区别:受控组件由React state控制表单值,非受控由DOM自行管理(ref获取)。2)高阶组件(HOC)vs Render Props vs Hooks逻辑复用对比。3)Fiber架构原理:可中断的异步渲染,requestIdleCallback模拟。4)React 18新特性:Concurrent Mode、Automatic Batching、useTransition、useDeferredValue、Suspense SSR。5)key的作用:识别节点是否变化。6)React状态管理选型:Redux/Mobx/Zustand/Recoil对比。7)SSR(Next.js)vs CSR。8)React事件机制与原生事件互操作。9)Redux中间件原理(applyMiddleware、compose)。10)Immutable在React中的应用。
Q: React与Vue区别 A: React与Vue的核心区别:1)设计哲学:React偏向函数式编程(immutable、单向数据流、纯函数组件);Vue偏向声明式响应式编程(可变数据、自动追踪)。2)数据绑定:React单向数据流(setState显式更新);Vue双向绑定(v-model语法糖)。3)更新机制:React setState触发组件树重新渲染(手动优化);Vue响应式系统精确追踪依赖自动更新。4)模板:React使用JSX(全JS能力);Vue使用模板(指令系统v-if/v-for,编译优化)。5)状态管理:React需额外库(Redux/Zustand);Vue有官方Vuex/Pinia。6)TypeScript支持:React较好(JSX = TSX),Vue3有很大改善。7)学习曲线:React需要理解JS、函数式概念;Vue上手容易但深层原理复杂。
React Hooks
Q: class组件问题 A: class组件存在的问题:1)代码组织困难:相关的逻辑分散在不同生命周期方法中(如数据请求在componentDidMount和componentDidUpdate中重复),无关逻辑耦合在一起。2)this绑定问题:方法需要.bind(this)或使用箭头函数,增加了样板代码。3)逻辑复用难:HOC(高阶组件)导致组件嵌套过深(wrapper hell),render props使代码难以阅读。4)状态逻辑复用难:没有优雅的机制将带有状态的行为提取复用。5)class的this关键字对初学者不友好。6)class在构建工具中体积更大,压缩效果不如函数。7)TypeScript中class的泛型使用较复杂。Hooks完美解决了这些问题。
Q: useState实现state/setState A: useState是React Hooks中管理状态的API,接收初始值(或惰性初始化函数)返回[state, setState]数组。setState参数可以是新值或函数(prev => newValue)。要点:1)每次渲染都会生成新的闭包,state是该次渲染时的快照。2)setState会导致组件重新渲染。3)多个setState在React 18中自动批处理。4)与class的setState不同,useState的setState是替换而非合并(对象类型需手动展开)。5)setState传入相同的值(Object.is比较)不会触发重渲染。6)useState的初始值只在首次渲染时有效。7)惰性初始化:useState(expensiveFn())会每次调用,应改为useState(() => expensiveFn())。
Q: useEffect模拟生命周期 A: useEffect可模拟class组件生命周期:1)componentDidMount:useEffect(() => { ... }, [])。空依赖数组表示只在挂载后执行一次。2)componentDidUpdate:useEffect(() => { ... })(无依赖,每次渲染都执行),或useEffect(() => { ... }, [dep])(特定依赖变化时执行)。3)componentWillUnmount:useEffect(() => { return () => { ... } }, []),返回的清理函数在卸载时执行。注意:useEffect的语义与生命周期有本质不同——它描述的是与状态同步的副作用,而不是特定时间点。依赖决定执行时机,默认每次渲染后都执行。善用依赖数组可精确控制副作用触发。
Q: useEffect模拟WillUnmount注意事项 A: useEffect的清理函数在组件卸载时执行,但有重要注意事项:1)清理函数不仅在卸载时执行,在每次effect重新执行前也会执行(用于清理上一次的effect)。所以如果依赖数组变化,先去清理再重新执行。2)空依赖[]的清理函数确实只在卸载时执行一次。3)清理函数中应清除定时器、取消订阅、取消请求等。4)在清理函数中访问state/props时要注意闭包陷阱——它捕获的是创建时的值。5)使用AbortController取消fetch请求:useEffect(() => { const ac = new AbortController(); fetch(url, {signal: ac.signal}); return () => ac.abort(); }, [url])。6)清理函数中不要调用setState(组件已卸载会报错),但React 18已对此做软处理。
Q: useRef与useContext A: useRef创建可变ref对象,其.current属性可存储任意值,改变不会触发重渲染。用途:1)访问DOM元素(<div ref={ref} />)。2)保存可变数据(如定时器ID、上一次的值、不会过期的回调引用)。3)存储不需要触发渲染的状态(与useState互补)。注意:ref.current在渲染期间读取会得到旧值,因为不触发渲染。useContext接收Context对象(createContext返回值),返回当前context值。当context值变化时,使用useContext的组件会重新渲染。useContext替代了Context.Consumer,使代码更简洁。多个context应合理拆分,避免过大context导致不必要渲染。
Q: useReducer能否代替redux A: useReducer + useContext可在中小型应用中实现类似Redux的状态管理,但不能完全替代Redux。useReducer适合组件内或深层组件树的局部状态管理。在替代场景中:const [state, dispatch] = useReducer(reducer, initialState),配合useContext将dispatch传递到子组件。限制:1)没有Redux DevTools支持。2)没有中间件机制(redux-thunk、redux-saga)。3)没有时间旅行调试。4)跨组件状态共享需手动组织provider的嵌套。5)缺少标准化的状态管理规范(action types规范、reducer组合)。6)性能优化不如Redux(context默认所有消费组件都会重渲染)。大型项目或需要中间件的场景,Redux仍是更好选择。
Q: useMemo性能优化 A: useMemo缓存计算结果,仅在依赖变化时重新计算。语法:const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])。使用场景:1)避免昂贵计算重复执行(如大数据排序、复杂数据处理)。2)保持引用稳定性:缓存对象/数组避免子组件不必要渲染(配合React.memo)。3)避免useEffect因引用变化被触发(依赖对象或函数时)。注意事项:1)不要滥用,简单的计算不需要useMemo(本身开销小,加缓存反而增加内存和比较开销)。2)不要将useMemo用作语义保证(React可能在某些场景下丢弃缓存值)。3)配合React.memo使用效果更好。
Q: useCallback性能优化 A: useCallback缓存函数引用,仅在依赖变化时更新。语法:const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b])。等价于useMemo(() => fn, [deps])。使用场景:1)将函数作为prop传递给优化后的子组件(使用React.memo的子组件),避免因函数引用变化导致子组件重新渲染。2)将函数作为useEffect的依赖,避免effect不必要执行。3)保持回调函数稳定性。注意事项:1)不要对所有函数都用useCallback,它本身有开销。2)只有当子组件使用了React.memo或函数作为effect依赖时才需要。3)配合useMemo使用,确保传入的值也是memoized的。4)空依赖[]返回稳定引用,但可能捕获过期闭包。
Q: 自定义Hook定义 A: 自定义Hook是以use开头的函数,内部调用其他Hook,实现逻辑复用。规则:1)函数名必须以use开头(React通过此规则判断哪些函数包含Hook调用)。2)内部遵循Hooks规则(只在顶层调用,只在函数组件或自定义Hook中调用)。3)可接收参数、返回任意值(通常是数组或对象)。示例:function useWindowSize() { const [size, setSize] = useState({width: window.innerWidth, height: window.innerHeight}); useEffect(() => { const handler = () => setSize({width: window.innerWidth, height: window.innerHeight}); window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, []); return size; }。自定义Hook让组件逻辑被抽取和复用。
Q: Hooks两条规则 A: React Hooks的两条铁律:1)只在顶层调用Hooks(Don't call Hooks inside conditions, loops, or nested functions)。原因:React依靠Hook的调用顺序来维护state和effect。每次渲染时Hook按相同顺序被调用,React用链表(Fiber.memoizedState)顺序存储Hook的值。如果条件渲染导致某次渲染跳过Hook,后续Hook的索引会错乱,state和setter对应错误。2)只在函数组件或自定义Hook中调用Hooks。不应在普通JS函数或class组件中调用。此规则确保Hooks在正确的执行上下文中工作。ESLint插件eslint-plugin-react-hooks可自动检查这两条规则。
Q: Hooks依赖调用顺序原因 A: Hooks依赖调用顺序的根本原因是底层实现机制决定的。React在Fiber节点上用链表存储所有Hook状态(memoizedState链)。首次渲染时按调用顺序依次创建Hook节点并链接起来。后续渲染时按相同顺序读取链表上的Hook状态。如果Hook被放在条件语句中,当条件变化导致Hook被跳过时,链表索引就不一致了——后续Hook读到的是前一个Hook的状态,导致bug。React团队曾考虑过使用key或Map的方式支持条件式Hook,但会增加复杂度和内存开销。调用顺序一致性也意味着React可以高效地处理Hook(O(1)读取),无需遍历查找。
Q: class逻辑复用问题 A: class组件中逻辑复用的主要方式是HOC和Render Props,各有问题。HOC(高阶组件)问题:1)Wrapper Hell:多层HOC嵌套导致组件层层包裹,调试困难。2)命名冲突:HOC注入的prop可能与父组件prop冲突。3)来源不明:传入的prop难以判断来自哪个HOC。4)静态方法丢失:需要手动拷贝静态方法(hoist-non-react-statics)。Render Props问题:1)嵌套地狱:多个Render Props回调形成多层嵌套。2)this上下文问题。3)在render中创建新函数导致浅比较失效。4)回调函数中的闭包陷阱。两者都没有充分利用组合的优势,且增加了组件层的抽象层级。Hooks通过自定义Hook实现了纯粹的逻辑组合,避免了这些嵌套问题。
Q: Hooks逻辑复用好处 A: Hooks逻辑复用的优势:1)无嵌套:自定义Hook是普通函数组合,不会产生组件嵌套层次,避免了Wrapper Hell。2)来源清晰:每个变量和方法明确来自哪个useXxx调用。3)无命名冲突:变量名在解构时可重命名,不同Hook可以返回同名变量。4)逻辑可组合:一个自定义Hook内部可调用其他Hook,形成组合。5)细粒度控制:每个Hook管理自身状态和副作用,独立互不干扰。6)类型友好:TypeScript对Hook函数返回值可直接推断。7)Tree-shaking友好:按需导入Hook不会被额外打包。8)迁移成本低:自定义Hook可逐步替代class中的逻辑。
Q: Hooks使用注意事项 A: Hooks使用注意事项:1)遵循Hooks规则(顶层调用,仅在函数组件/自定义Hook中调用)。2)依赖数组要完整:useEffect/useMemo/useCallback要包含所有依赖(eslint-plugin-react-hooks可辅助)。3)避免过早优化:不要对所有计算都用useMemo,不要对所有函数都用useCallback。4)setState的函数式更新可避免闭包陷阱:setCount(c => c + 1)。5)useEffect中不要用async函数(返回Promise而非清理函数)。6)死循环风险:不要在useEffect的依赖中设置会变化的数据。7)合理拆分:一个useEffect只做一件事,多个副作用拆成多个useEffect。8)State提升:共享状态提升到共同父组件。
Webpack
Q: hash/chunkhash/contenthash区别 A: hash:每次构建生成唯一hash,所有文件共用。只要有一个文件变化,所有文件hash都会变,导致缓存失效。chunkhash:根据不同的入口(entry)生成不同hash,同一chunk的文件共享hash。js文件变化只影响该chunk的hash,不影响其他chunk。contenthash:根据文件内容生成hash,常用于CSS文件(MiniCssExtractPlugin分离CSS后使用)。内容不变hash不变,缓存粒度最细。建议:JS文件用chunkhash,CSS文件用contenthash。注意优化:runtimeChunk需独立提取避免业务代码变化影响vendor hash。
Q: 常用插件总结 A: webpack常用插件:1)HtmlWebpackPlugin:生成HTML并自动注入打包后的JS/CSS。2)MiniCssExtractPlugin:提取CSS为独立文件(替代style-loader)。3)CssMinimizerPlugin:压缩CSS。4)TerserPlugin:压缩JS(webpack5内置)。5)DefinePlugin:定义编译时全局常量。6)CopyWebpackPlugin:复制静态资源。7)CleanWebpackPlugin:构建前清理输出目录(webpack5内置output.clean)。8)HotModuleReplacementPlugin:热更新(webpack5内置)。9)SplitChunksPlugin:代码分割(webpack4+内置)。10)BundleAnalyzerPlugin:分析打包产物体积。11)IgnorePlugin:忽略指定模块(如moment的语言包)。12)ProvidePlugin:自动加载模块,无需import。
Q: 热更新原理 A: HMR(Hot Module Replacement)热更新原理:1)Webpack Dev Server启动时,Webpack监听文件变化并重新编译,生成两个部分:manifest(模块列表)和update(更新内容)。2)通过WebSocket将更新的hash推送给浏览器客户端。3)客户端对比hash,通过JSONP请求获取更新模块的JS代码。4)WebpackDevMiddleware在内存中编译模块,通过HotModuleReplacementPlugin注入的HMR runtime处理模块更新。5)HMR runtime找到变更模块的父模块并执行accept回调(通过module.hot.accept)。6)如果accept回调触发组件重新渲染(React Hot Loader或React Fast Refresh),则更新页面而不刷新整个页面。整个过程无刷新完成模块替换。
Q: webpack原理简述 A: Webpack本质上是一个模块打包器(Module Bundler),核心流程:1)初始化:合并配置参数,注册插件。2)编译阶段:从入口文件开始,递归解析依赖,调用Loader将非JS模块转为JS模块,生成AST,分析依赖关系。3)构建模块图:将每个模块包装为包含模块ID、依赖列表、转换后代码的Module对象,形成依赖图(Dependency Graph)。4)封装(Seal):根据依赖图将模块合并到chunk中,进行代码分割优化。5)输出(Emit):根据chunk生成最终文件到输出目录。核心概念:Entry(入口)、Output(输出)、Loader(转换器)、Plugin(插件)、Chunk(代码块)、Module(模块)。Webpack将所有资源视为模块,允许引入任何文件。
Q: 构建速度优化 A: Webpack构建速度优化策略:1)使用最新版本Webpack和Node。2)Loader优化:用include/exclude缩小处理范围,用缓存(cache-loader、babel-loader的cacheDirectory)。3)多进程构建:thread-loader并行处理Loader,或使用TerserPlugin的parallel选项。4)模块解析优化:resolve.alias映射路径,resolve.extensions限制后缀范围,resolve.modules指定搜索目录。5)webpack5持久化缓存(文件系统缓存)。6)减小文件搜索范围,避免不必要的loader处理node_modules。7)使用webpack-bundle-analyzer分析瓶颈。8)开发环境用source-map cheap-module选项。9)DLLPlugin预编译不常变动的库(webpack5推荐使用Cache)。
Q: 产出代码优化 A: Webpack产出代码优化目标是小、缓存友好、加载快。策略:1)代码分割(SplitChunks):提取公共依赖(vendors)、第三方库单独chunk。2)懒加载:动态import() + webpackChunkName命名拆分。3)Tree-shaking:删除未使用的ES Module导出代码,确保production模式开启(sideEffects配置正确)。4)代码压缩:TerserPlugin压缩JS,CssMinimizerPlugin压缩CSS。5)scope hoisting:ModuleConcatenationPlugin将模块合并到一个函数作用域(production默认)。6)合理使用hash:chunkhash/contenthash实现长效缓存。7)图片压缩:image-webpack-loader。8)分离CSS:MiniCssExtractPlugin。9)按需加载polyfill:@babel/preset-env的useBuiltIns: 'usage'。
HTTP
Q: HTTP基础总结 A: HTTP是超文本传输协议(HyperText Transfer Protocol),基于TCP/IP的应用层协议。默认端口80。特点:1)无状态(每个请求独立,通过Cookie/Token维持会话)。2)请求-响应模式。3)可用非持久连接(HTTP/1.0默认短连接,每次请求创建TCP)和持久连接(HTTP/1.1默认Connection: keep-alive,复用TCP)。请求方法:GET(获取)、POST(提交)、PUT(更新)、DELETE(删除)、PATCH(部分更新)、HEAD(获取响应头)、OPTIONS(预检请求)。状态码:1xx(信息)、2xx(成功200/201/204)、3xx(重定向301/302/304)、4xx(客户端错误400/401/403/404)、5xx(服务器错误500/502/503)。
Q: HTTP缓存 A: HTTP缓存分为强缓存和协商缓存,由响应头控制。强缓存:1)Expires(HTTP/1.0,绝对时间,依赖客户端时间)。2)Cache-Control:max-age=秒(优先级更高),no-cache(协商缓存),no-store(不缓存),private/public(是否可被代理缓存)。强缓存生效时返回200 from disk/memory cache。协商缓存:1)Last-Modified / If-Modified-Since(最后修改时间,精度秒)。2)ETag / If-None-Match(内容hash,更精确)。协商缓存生效返回304 Not Modified。缓存位置优先级:Service Worker → Memory Cache → Disk Cache → Push Cache。合理设置缓存策略可大幅减少请求,提升页面加载速度。
Q: HTTP 1.0/1.1/2.0区别 A: HTTP 1.0:默认短连接,每个请求创建新的TCP连接,不支持Host头,不支持断点续传。HTTP 1.1:默认持久连接(Connection: keep-alive),支持Pipeline(请求管道化,但队头阻塞问题仍存在),支持Host头(虚拟主机),支持分块传输(chunked),支持断点续传(Range头),新增更多缓存控制(Cache-Control、ETag)。HTTP 2.0:二进制分帧(非文本协议),多路复用(一个TCP连接并发处理多个请求,解决了队头阻塞),请求优先级,服务器推送(Server Push),头部压缩(HPACK算法)。HTTP 2.0仍有TCP层面的队头阻塞问题(丢包时整个TCP流阻塞),HTTP 3.0(基于QUIC/UDP)解决了此问题。
Q: WebSocket与HTTP区别 A: WebSocket和HTTP的关键区别:1)协议不同:WebSocket是ws/wss协议,HTTP是http/https。2)通信模式:HTTP是请求-响应模式(客户端主动,服务端无法主动推送),WebSocket是双向全双工通信(服务端可主动推送)。3)连接方式:HTTP可短连接或长连接,WebSocket建立连接后持续保持(通过HTTP协议Upgrade握手)。4)数据格式:HTTP是文本协议(头部+body),WebSocket是二进制帧(轻量级,开销小)。5)应用场景:HTTP适合RESTful API,WebSocket适合实时通信(聊天、游戏、股票行情、实时协同编辑)。6)兼容性:WebSocket需要浏览器和服务端都支持,但现代浏览器已全面支持。
Q: TCP三次握手四次挥手 A: 三次握手建立TCP连接:1)客户端发送SYN(seq=x)给服务端,进入SYN_SENT状态。2)服务端回复SYN+ACK(seq=y, ack=x+1),进入SYN_RCVD状态。3)客户端发送ACK(seq=x+1, ack=y+1),双方进入ESTABLISHED状态。目的:确保双方收发能力正常,同步序列号。四次挥手断开连接:1)主动方发送FIN(seq=u),进入FIN_WAIT_1。2)被动方回复ACK(seq=v, ack=u+1),进入CLOSE_WAIT(主动方收到后进入FIN_WAIT_2)。3)被动方发送FIN(seq=w, ack=u+1),进入LAST_ACK。4)主动方回复ACK(seq=u+1, ack=w+1),进入TIME_WAIT(等待2MSL后关闭)。TIME_WAIT目的是确保被动方收到ACK。
Q: 跨域时发送options请求原因 A: 跨域请求时,浏览器判断是否为简单请求:满足条件(GET/POST/HEAD + 无自定义头 + Content-Type为text/plain, multipart/form-data, application/x-www-form-urlencoded之一)的为简单请求直接发送。非简单请求(如PUT/DELETE、有自定义头、Content-Type为application/json)会在正式请求前发送预检请求(OPTIONS),获取服务器是否允许跨域。OPTIONS响应需包含Access-Control-Allow-Methods(允许方法)、Access-Control-Allow-Headers(允许头)、Access-Control-Max-Age(预检结果缓存时间)。减少OPTIONS请求:1)合并请求头使用,2)设置Max-Age缓存,3)转为简单请求。
Q: token/cookie/session区别 A: Cookie是浏览器存储机制,自动携带到同源请求中,有大小限制(4KB)。Session是服务端存储的会话数据,sessionID通过Cookie传递。Token是服务端生成的令牌(通常为JWT),包含用户信息和签名,无状态,服务端不需要存储会话信息。关系:Cookie常用于传递sessionID或Token。区别:1)存储位置:Session存服务端,Cookie/Token存客户端。2)状态:Session有状态(服务端记录),Token无状态(服务端不保存)。3)扩展性:Token更适合分布式系统(无需共享session存储)。4)安全性:Cookie易受CSRF攻击(同源自动携带),Token需防范XSS(localStorage存储)。5)失效机制:Session可服务端主动失效,Token需等待过期或维护黑名单。
Q: HTTPS中间人攻击及预防 A: HTTPS的中间人攻击(MITM)方式:1)伪造证书:攻击者替换服务端证书为自签证书(用户浏览器未严格验证证书时会弹出安全警告)。2)SSL剥离:将HTTPS降级为HTTP(通过中间代理修改页面链接)。3)ARP欺骗/DNS劫持:将流量重定向到恶意服务器。预防措施:1)严格证书校验:浏览器验证证书链、域名、有效期、CA签名。2)HSTS(HTTP Strict Transport Security):强制浏览器只能通过HTTPS访问,禁止降级。3)证书透明度(Certificate Transparency):公开所有已签发证书的日志,检测异常证书。4)公钥固定(HPKP):但已不推荐使用。5)用户层面:不忽略证书警告,不连接公共WiFi进行敏感操作。
Node
Q: 浏览器与Node事件循环区别 A: 浏览器和Node的事件循环核心机制相同(单线程、宏任务/微任务),但阶段划分不同。浏览器事件循环:宏任务(script/setTimeout/事件回调/UI渲染)→ 清空微任务 → 下一个宏任务。Node事件循环基于libuv库,分为6个阶段:1)timers(执行setTimeout/setInterval回调)。2)pending callbacks(执行延迟到下一个循环的I/O回调)。3)idle/prepare(内部使用)。4)poll(轮询I/O事件,执行I/O回调)。5)check(执行setImmediate回调)。6)close callbacks(执行close事件回调)。每个阶段执行完后清空微任务队列(process.nextTick优先于Promise.then)。Node 11+后行为与浏览器更接近(每个宏任务后清空微任务)。
Q: Node开启多进程及进程通信 A: Node.js是单线程模型,但可通过多进程利用多核CPU。开启方式:1)child_process模块:spawn(流式数据)、exec(缓冲输出)、execFile(执行文件)、fork(衍生Node进程,自带IPC通信)。2)cluster模块:基于child_process.fork,自动在多核CPU上创建worker进程共享端口。进程间通信(IPC):1)通过send()和message事件传递JSON消息。2)共享内存:shared array buffer或mmap文件。3)消息队列:Redis、RabbitMQ等外部中间件。4)Unix socket或命名管道。cluster模式下master管理worker生命周期和负载均衡(默认round-robin)。注意:多进程不共享内存,每个进程有独立的V8实例。
综合题目
Q: 工作流程 A: 前端开发工作流程一般包括:1)需求分析:与产品经理和UI设计师沟通需求,确认交互逻辑。2)技术方案设计:评估可行性、技术选型、组件设计、接口约定。3)开发环境搭建:配置工程化工具(Webpack/Vite、ESLint、Prettier、Git hooks)。4)编码实现:划分组件、开发业务逻辑、接口联调。5)代码Review:确保代码质量和规范一致性。6)自测与冒烟测试:覆盖核心功能。7)提测与Bug修复:配合QA修复缺陷。8)上线部署:CI/CD流水线、灰度发布、监控报警。9)线上问题跟踪:Sentry等监控工具跟踪性能指标和错误。10)迭代复盘:回顾问题总结经验。敏捷开发中每个迭代(1-2周)完成一次完整循环。
Q: 项目难点解决 A: 分享项目难点应遵循STAR原则(Situation、Task、Action、Result)。典型思路:1)描述具体的技术挑战(不是简单罗列,而是讲清楚为什么难)。2)说明你提出的方案和权衡(为什么选择此方案而非其他)。3)展示最终效果(性能提升X%、用户体验改善)。常见痛点举例:海量数据渲染(虚拟滚动方案)、复杂状态管理(状态机/状态机图)、性能优化(性能指标优化数据)、跨端兼容(统一适配方案)、安全漏洞防范(XSS/CSRF修复方案)。注意:难点不是简单功能实现,而是真正有技术挑战的问题,最好有量化的成果支撑。
Q: 前端性能优化 A: 前端性能优化可按加载时和运行时划分。加载优化:1)CDN加速静态资源。2)资源压缩(JS/CSS/Image)。3)代码分割和按需加载(动态import)。4)Tree-shaking删除未使用代码。5)使用HTTP/2多路复用减少连接数。6)preload/prefetch预加载关键资源。7)优化图片格式(WebP/AVIF)和响应式图片。8)关键CSS内联。运行时优化:1)减少DOM操作和重排重绘。2)使用requestAnimationFrame做动画。3)虚拟列表处理大数据。4)Web Worker处理密集型计算。5)事件委托减少事件监听器。6)debounce/throttle限制高频触发。7)使用IntersectionObserver实现懒加载。8)使用GPU加速(transform/opacity动画)。
Q: 设计模式使用场景 A: 前端常用设计模式:1)单例模式:全局状态管理(Redux Store)、Vuex Store、全局弹窗组件。2)观察者模式/发布订阅:Vue的响应式系统、EventBus、跨组件通信。3)工厂模式:创建不同UI组件、不同格式解析器。4)策略模式:表单校验规则组合、不同排序算法切换。5)装饰器模式:高阶组件(HOC)、中间件(Redux applyMiddleware)。6)适配器模式:数据格式转换、跨平台API适配。7)代理模式:图片懒加载(虚拟代理)、请求缓存。8)职责链模式:Axios拦截器链、Express中间件。9)命令模式:撤销/重做操作、宏录制。10)模版方法模式:框架生命周期。设计模式的价值是提供通用解决方案和沟通语言。
Q: H5慢排查 A: H5页面加载慢的排查流程:1)确认网络环境:弱网还是强网。2)使用Chrome DevTools Performance面板录制加载过程,查看白屏时间、FCP、LCP指标。3)Network面板分析:请求数量、传输大小、是否有阻塞请求、DNS解析时间、TTFB。4)查看是否首屏JS/CSS过大导致加载慢 → 代码分割优化。5)检查是否有未压缩大图 → 图片压缩/WebP/懒加载。6)检查是否有同步AJAX请求阻塞渲染。7)检查是否缺少CDN或缓存策略不合理。8)检查服务器响应时间TTFB是否过长(后端慢或网络延迟)。9)使用Lighthouse生成性能报告给出优化建议。10)移动端特别关注:首屏关键路径渲染、减少重定向。
Q: 十万条数据渲染 A: 十万条数据渲染的优化方案:1)虚拟滚动(Virtual Scroll):只渲染可视区域内的数据,使用transform: translateY()模拟滚动位置。常用库:react-window、vue-virtual-scroller、umy-ui。2)时间分片(Time Slicing):将渲染分批进行,每次渲染一小部分,使用requestAnimationFrame控制帧率。3)Web Worker:在worker线程中处理数据计算,主线程只负责渲染。4)Object.freeze:如果数据不需要响应式,用Object.freeze冻结减少Vue/React的观测开销。5)分页加载:如果语义允许,采用分页或滚动加载更多(无限滚动)。6)使用文档片段批量插入DOM:减少浏览器回流次数。7)减少DOM节点层级和数据绑定。大多数场景下虚拟滚动是最佳选择。
Q: H5首屏优化 A: H5首屏加载优化策略:1)关键渲染路径优化:内联关键CSS(<style>标签),非关键CSS异步加载(preload或media="print")。2)JS异步加载:使用defer或async属性,或动态创建script标签。3)首屏数据预请求:在DOMContentLoaded前尽早发起数据请求(prefetch或提前构建请求URL)。4)资源预加载:使用<link rel="preload">预加载首屏关键资源(字体、图片、JS/CSS)。5)骨架屏:在数据加载前展示页面轮廓,提升感知速度。6)使用SSR/SSG:服务端渲染首屏HTML,减少客户端渲染时间。7)图片优化:懒加载非首屏图片,使用WebP/AVIF格式,响应式图片srcset。8)缓存策略:合理使用强缓存和服务端缓存。9)代码分割:路由级别懒加载。
Q: js-bridge原理 A: JS-Bridge是H5与原生App(iOS/Android)间的通信桥梁。实现方式:1)URL Scheme拦截(旧方案):H5通过iframe.src或location.href发起自定义URL请求(如jsbridge://method?params),原生拦截请求解析并调用原生API。问题:URL长度限制、连续调用丢失。2)注入JavaScript(主流方案):原生通过WebView的evaluateJavascript(Android)或evaluateJavaScript(iOS)执行JS回调函数。H5将回调函数注册到全局,原生执行后调用对应回调。3)WKWebView的MessageHandler(iOS):通过window.webkit.messageHandlers.postMessage通信。4)JavascriptInterface(Android):通过@JavascriptInterface注解暴露原生方法。现代方案通常结合使用注入JS API和回调机制,支持异步双向通信。
Q: 搭建开发环境考虑点 A: 搭建前端开发环境需考虑:1)包管理器选择(npm/yarn/pnpm),pnpm因其硬链接机制更节省磁盘空间。2)构建工具(Webpack/Vite/Turbopack),中小项目推荐Vite,大项目需权衡。3)代码规范:ESLint(规则配置)、Prettier(格式化)、EditorConfig、Git hooks(husky + lint-staged)。4)TypeScript配置:tsconfig严格模式、路径别名、类型检查。5)CSS方案:CSS Modules/Tailwind CSS/CSS-in-JS(styled-components)选型。6)状态管理:Redux/Zustand/Pinia选择。7)Mock方案:MSW(Mock Service Worker)或接口代理配置。8)测试框架:Vitest/Jest + Testing Library + Cypress/Playwright。9)CI/CD:GitHub Actions/GitLab CI,代码检查+测试+build+部署流水线。10)环境变量管理:.env文件和多环境配置。
Q: 技术负责人技术选型 A: 技术负责人在技术选型时需综合考虑:1)团队能力:团队熟悉什么技术栈?学习成本多少?是否有相关经验?2)业务需求:是否需要SSR(SEO需求)?是否需要跨端(React Native/Flutter/uni-app)?3)生态成熟度:社区活跃度、文档完整性、issue响应速度、第三方库支持。4)性能和体量:小项目可选轻量方案(Preact/Vue),大项目需稳定生态。5)长期维护:框架API稳定性、版本迁移成本、团队招聘容易度。6)基建支持:CI/CD兼容性、TypeScript支持、测试工具链。7)渐进式采用:部分页面试点新技术而非全量切换。8)学习曲线和晋升空间:对团队成员成长是否有帮助。9)投入产出比:解决实际问题才是目的,不盲目追新。
Q: 字符串前缀匹配 A: 字符串前缀匹配(如实现自动补全/输入提示)的高效方案:1)Trie(前缀树/字典树):将词汇构建成树结构,每个节点代表一个字符,公共前缀共享节点。搜索时沿着前缀路径查找,时间复杂度O(m)(m为前缀长度)。适合静态词库的快速匹配。2)如果数据量小,直接用filter + startsWith: arr.filter(s => s.startsWith(prefix))。3)排序后二分查找:对词库排序,用二分查找定位前缀范围,再扩展获取所有匹配。4)IndexedDB存储索引:浏览器端处理大量离线词库。5)后端API + 防抖:实时查询服务端接口(配合debounce控制请求频率)。Trie是面试常见考点,空间换时间,注意内存占用问题(可用压缩Trie或Radix Tree优化)。
Q: 前端路由原理 A: 前端路由核心原理是监听URL变化并渲染对应组件,不用刷新页面。两种模式:1)Hash模式:监听window.onhashchange事件,URL中#后的部分作为路由标识。hash变化不会触发页面刷新,兼容性好。2)History模式:利用HTML5 History API(pushState/replaceState),监听popstate事件。pushState可改变URL而不刷新页面。需要服务端配合做fallback(所有路由指向同一个index.html),否则直接访问子路径会404。实现一个简易路由:维护路由映射表,URL变化时查找对应组件渲染到指定容器。前端路由的缺点:SEO不友好(需SSR或预渲染补充),History模式需服务端支持。
Q: 首屏渲染优化 A: 首屏渲染优化是一系列前端性能优化集合:1)减少关键资源大小:内联首屏CSS(<style>),压缩JS/CSS/图片,移除未使用代码。2)减少关键请求数量:合并小文件、使用HTTP/2多路复用、CSS Sprites/iconfont。3)优化加载顺序:preload预加载关键资源,prefetch预加载非关键资源。4)减少阻塞渲染的JS:script使用defer/async,或动态插入。5)SSR/SSG:服务端渲染或静态生成减少客户端渲染工作。6)骨架屏和渐进加载:先展示基本轮廓再逐步填充内容。7)优化TTFB:使用CDN、启用压缩、优化服务端响应时间。8)缓存复用:Service Worker缓存应用外壳(App Shell),结合Cache API实现离线访问。9)资源预建联:DNS预解析(dns-prefetch)、预连接(preconnect)。
Q: interface与type区别 A: TypeScript中interface和type(类型别名)的核心区别:1)扩展方式:interface通过extends继承,type通过&交叉类型组合。2)声明合并:interface可多次声明自动合并(同名interface会合并),type不能重复声明。3)适用类型:type更灵活,可定义原始类型(type Name = string)、联合类型(type Status = 'a' | 'b')、元组、工具类型等;interface只能定义对象/函数类型。4)Mapped Types:type可使用映射类型({ [K in keyof T]: ... }),interface不行。5)性能:interface在大型项目中类型检查性能略优于type(因为interface有缓存机制)。建议:对外发布的库API用interface(可扩展、声明合并),应用内部用type或interface均可。
手写题
Q: 防抖 A: 防抖(debounce)在事件触发后等待指定时间再执行回调,若等待期间再次触发则重置等待时间。常用于搜索输入框。
function debounce(fn, delay = 300, immediate = false) { let timer = null; return function(...args) { const context = this; if (immediate && !timer) fn.apply(context, args); clearTimeout(timer); timer = setTimeout(() => { if (!immediate) fn.apply(context, args); timer = null; }, delay); }; }Q: 节流 A: 节流(throttle)在指定时间间隔内只执行一次回调,无论触发频率多高。常用于滚动、resize事件。
function throttle(fn, interval = 300, options = { leading: true, trailing: false }) { let lastTime = 0; let timer = null; return function(...args) { const context = this; const now = Date.now(); if (!lastTime && !options.leading) lastTime = now; const remaining = interval - (now - lastTime); if (remaining <= 0) { if (timer) { clearTimeout(timer); timer = null; } fn.apply(context, args); lastTime = now; } else if (options.trailing && !timer) { timer = setTimeout(() => { fn.apply(context, args); lastTime = options.leading ? Date.now() : 0; timer = null; }, remaining); } }; }Q: New的过程 A: new操作符的核心步骤:
function myNew(Constructor, ...args) { const obj = Object.create(Constructor.prototype); const result = Constructor.apply(obj, args); return (result !== null && typeof result === 'object') || typeof result === 'function' ? result : obj; }Q: instanceOf原理 A: instanceof检查构造函数的prototype是否出现在对象的原型链上。
function myInstanceOf(obj, Constructor) { if (typeof obj !== 'object' || obj === null) return false; let proto = Object.getPrototypeOf(obj); const prototype = Constructor.prototype; while (proto !== null) { if (proto === prototype) return true; proto = Object.getPrototypeOf(proto); } return false; }Q: 实现call、apply、bind方法 A: call/apply/bind的手写实现:
Function.prototype.myCall = function(context = window, ...args) { const key = Symbol('fn'); context[key] = this; const result = context[key](...args); delete context[key]; return result; }; Function.prototype.myApply = function(context = window, args = []) { const key = Symbol('fn'); context[key] = this; const result = context[key](...args); delete context[key]; return result; }; Function.prototype.myBind = function(context = window, ...bindArgs) { const fn = this; return function(...callArgs) { return fn.apply(context, [...bindArgs, ...callArgs]); }; };Q: 发布订阅模式 A: 发布订阅(Pub/Sub)模式实现:
class EventEmitter { constructor() { this.events = {}; } on(event, listener) { if (!this.events[event]) this.events[event] = []; this.events[event].push(listener); return () => this.off(event, listener); } off(event, listener) { if (!this.events[event]) return; this.events[event] = this.events[event].filter(l => l !== listener); } emit(event, ...args) { if (!this.events[event]) return; this.events[event].forEach(l => l(...args)); } once(event, listener) { const wrapper = (...args) => { listener(...args); this.off(event, wrapper); }; this.on(event, wrapper); } }Q: 深拷贝(含数据类型与循环引用) A: 考虑数据类型和循环引用的深拷贝:
function deepClone(obj, map = new WeakMap()) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj); if (map.has(obj)) return map.get(obj); const clone = Array.isArray(obj) ? [] : {}; map.set(obj, clone); Reflect.ownKeys(obj).forEach(key => { clone[key] = deepClone(obj[key], map); }); return clone; }Q: LRU缓存实现 A: LRU(最近最少使用)缓存实现,基于Map保持插入顺序:
class LRUCache { constructor(capacity) { this.capacity = capacity; this.cache = new Map(); } get(key) { if (!this.cache.has(key)) return -1; const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; } put(key, value) { if (this.cache.has(key)) this.cache.delete(key); this.cache.set(key, value); if (this.cache.size > this.capacity) { this.cache.delete(this.cache.keys().next().value); } } }Q: curry函数(柯里化) A: 函数柯里化将多参数函数转为嵌套的一元函数:
function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args); } else { return (...more) => curried.apply(this, [...args, ...more]); } }; }Q: LazyMan实现sleep A: LazyMan实现链式调用和延迟执行:
class LazyMan { constructor(name) { this.tasks = [() => { console.log('Hi! This is ' + name); this.next(); }]; setTimeout(() => this.next(), 0); } next() { const task = this.tasks.shift(); task && task(); } eat(food) { this.tasks.push(() => { console.log('Eat ' + food); this.next(); }); return this; } sleep(delay) { this.tasks.push(() => { setTimeout(() => { console.log('Wake up after ' + delay + 's'); this.next(); }, delay * 1000); }); return this; } sleepFirst(delay) { this.tasks.unshift(() => { setTimeout(() => { console.log('Wake up after ' + delay + 's'); this.next(); }, delay * 1000); }); return this; } } function LazyManFactory(name) { return new LazyMan(name); }Q: getType函数 A: 精确获取任意JS值的类型:
function getType(value) { if (value === null) return 'null'; if (typeof value !== 'object') return typeof value; return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); }Q: 数组扁平化 A: 数组扁平化将嵌套数组展平为指定深度的一维数组:
function flatten(arr, depth = Infinity) { if (depth === 0) return arr.slice(); return arr.reduce((acc, item) => acc.concat(Array.isArray(item) ? flatten(item, depth - 1) : item), []); } function flattenIter(arr) { const stack = [...arr]; const result = []; while (stack.length) { const item = stack.pop(); if (Array.isArray(item)) stack.push(...item); else result.unshift(item); } return result; }Q: 数组转树 A: 将扁平数组(含id和pid字段)转为树形结构:
function arrayToTree(items, rootId = null) { const map = new Map(items.map(item => [item.id, { ...item, children: [] }])); const tree = []; items.forEach(item => { const node = map.get(item.id); if (item.pid === rootId || (rootId === null && !item.pid)) { tree.push(node); } else if (map.has(item.pid)) { map.get(item.pid).children.push(node); } }); return tree; }Q: 获取URL参数 A: 将URL查询字符串解析为键值对象:
function getURLParams(url) { const params = {}; const queryString = url.includes('?') ? url.split('?')[1].split('#')[0] : ''; if (!queryString) return params; queryString.split('&').forEach(pair => { if (!pair) return; const [key, value] = pair.split('=').map(s => decodeURIComponent(s.replace(/\\+/g, ' '))); if (key) { if (params.hasOwnProperty(key)) params[key] = [].concat(params[key], value); else params[key] = value; } }); return params; }Q: Promise加载图片 A: 使用Promise包装图片加载实现异步控制:
function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error('Failed to load image: ' + url)); img.src = url; }); } function loadImages(urls) { return Promise.all(urls.map(url => loadImage(url))); }Q: 数组交集并集 A: 数组的交集、并集、差集实现:
const union = (a, b) => [...new Set([...a, ...b])]; const intersection = (a, b) => a.filter(item => b.includes(item)); const difference = (a, b) => a.filter(item => !b.includes(item)); const symmetricDifference = (a, b) => [...difference(a, b), ...difference(b, a)];Q: 反转字符串 A: 反转字符串的多种实现方式:
function reverseString(str) { return str.split('').reverse().join(''); } function reverseString2(str) { let result = ''; for (let i = str.length - 1; i >= 0; i--) result += str[i]; return result; } function reverseString3(str) { return str === '' ? '' : reverseString3(str.slice(1)) + str[0]; }Q: H5图片懒加载设计 A: 图片懒加载实现(当图片进入可视区时再加载):
function lazyLoadImages(selector = 'img[data-src]') { const images = document.querySelectorAll(selector); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.removeAttribute('data-src'); observer.unobserve(img); } }); }, { rootMargin: '100px' }); images.forEach(img => observer.observe(img)); }Q: Vue3响应式原理实现 A: Vue3响应式核心的极简实现:
const targetMap = new WeakMap(); let activeEffect = null; function effect(fn) { activeEffect = fn; fn(); activeEffect = null; } function track(target, key) { if (!activeEffect) return; let depsMap = targetMap.get(target); if (!depsMap) targetMap.set(target, depsMap = new Map()); let dep = depsMap.get(key); if (!dep) depsMap.set(key, dep = new Set()); dep.add(activeEffect); } function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) return; depsMap.get(key)?.forEach(fn => fn()); } function reactive(target) { return new Proxy(target, { get(target, key, receiver) { track(target, key); const res = Reflect.get(target, key, receiver); return typeof res === 'object' && res !== null ? reactive(res) : res; }, set(target, key, value, receiver) { const res = Reflect.set(target, key, value, receiver); trigger(target, key); return res; } }); } function ref(value) { return reactive({ value }); }Q: 简洁版Promise实现 A: 极简Promise实现(含基本resolve/reject/then链式调用):
class MyPromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.reason = undefined; this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; const resolve = (value) => { if (this.state !== 'pending') return; this.state = 'fulfilled'; this.value = value; this.onFulfilledCallbacks.forEach(fn => fn()); }; const reject = (reason) => { if (this.state !== 'pending') return; this.state = 'rejected'; this.reason = reason; this.onRejectedCallbacks.forEach(fn => fn()); }; try { executor(resolve, reject); } catch (e) { reject(e); } } then(onFulfilled, onRejected) { return new MyPromise((resolve, reject) => { const handleFulfilled = () => { try { const res = onFulfilled(this.value); resolve(res); } catch (e) { reject(e); } }; const handleRejected = () => { try { const res = onRejected ? onRejected(this.reason) : reject(this.reason); resolve(res); } catch (e) { reject(e); } }; if (this.state === 'fulfilled') handleFulfilled(); else if (this.state === 'rejected') handleRejected(); else { this.onFulfilledCallbacks.push(handleFulfilled); this.onRejectedCallbacks.push(handleRejected); } }); } catch(onRejected) { return this.then(null, onRejected); } }
算法题
Q: 时间复杂度与空间复杂度概念 A: 时间复杂度衡量算法执行时间随输入规模n增长的增长率,用大O表示法描述最坏情况。常见复杂度:O(1)常数、O(log n)对数、O(n)线性、O(n log n)线性对数、O(n^2)平方、O(2^n)指数、O(n!)阶乘。空间复杂度衡量算法运行所需额外内存空间。分析时关注循环嵌套(乘法关系)、递归深度、额外数据结构大小。例如:双层循环+O(1)空间 → O(n^2)时间+O(1)空间;递归二分搜索 → O(log n)时间+O(log n)空间(调用栈)。实际应用中,需要在时间和空间之间做权衡(如空间换时间,使用哈希表缓存)。
Q: 数字千分位格式化 A: 将数字转为千分位逗号分隔格式,兼容小数和负数。
function formatNumber(num) { const [int, dec] = String(num).split('.'); const formatted = int.replace(/(\d)(?=(\d{3})+$)/g, '$1,'); return dec ? formatted + '.' + dec : formatted; } // 或直接使用内置API // num.toLocaleString('en-US')时间复杂度O(n),空间复杂度O(n)(n为数字长度)。
Q: 快速排序实现与复杂度分析 A: 快速排序使用分治思想,选基准值,将数组分为小于和大于基准的两个子数组,递归排序。
function quickSort(arr, left = 0, right = arr.length - 1) { if (left >= right) return arr; const pivot = partition(arr, left, right); quickSort(arr, left, pivot - 1); quickSort(arr, pivot + 1, right); return arr; } function partition(arr, left, right) { const pivot = arr[right]; let i = left; for (let j = left; j < right; j++) { if (arr[j] < pivot) { [arr[i], arr[j]] = [arr[j], arr[i]]; i++; } } [arr[i], arr[right]] = [arr[right], arr[i]]; return i; }时间复杂度:平均O(n log n),最坏O(n^2)(已排序数组且固定选最后一个为基准)。空间复杂度:平均O(log n)(递归栈),最坏O(n)。优化:随机选取基准、三数取中法。
Q: 移动数组0到末尾 A: 将数组中所有0移动到末尾,保持非零元素相对顺序,原地修改。
function moveZeroes(nums) { let nonZeroIdx = 0; for (let i = 0; i < nums.length; i++) { if (nums[i] !== 0) { [nums[nonZeroIdx], nums[i]] = [nums[i], nums[nonZeroIdx]]; nonZeroIdx++; } } return nums; }时间复杂度O(n),空间复杂度O(1)。思路:双指针——慢指针指向下一个非零元素位置,快指针遍历数组,遇到非零元素就交换到慢指针位置。
Q: 斐波那契第n值 A: 斐波那契数列的O(n)迭代解法(避免递归指数爆炸):
function fibonacci(n) { if (n <= 1) return n; let prev = 0, curr = 1; for (let i = 2; i <= n; i++) { [prev, curr] = [curr, prev + curr]; } return curr; }递归解法(不可取,需指出问题):时间O(2^n),空间O(n)。迭代法:时间O(n),空间O(1)。还可使用矩阵快速幂实现O(log n)时间。
Q: 两数之和 A: 找出数组中两数之和等于目标值的索引:
function twoSum(nums, target) { const map = new Map(); for (let i = 0; i < nums.length; i++) { const complement = target - nums[i]; if (map.has(complement)) return [map.get(complement), i]; map.set(nums[i], i); } return []; }时间复杂度O(n),空间复杂度O(n)。核心思路:遍历时用哈希表存储已遍历的值和索引,检查当前值的补数是否已在表中,只需一次遍历。
Q: 二分查找实现与分析 A: 在有序数组中查找目标值,返回索引或-1:
function binarySearch(nums, target) { let left = 0, right = nums.length - 1; while (left <= right) { const mid = left + ((right - left) >> 1); if (nums[mid] === target) return mid; if (nums[mid] < target) left = mid + 1; else right = mid - 1; } return -1; }时间复杂度O(log n),空间复杂度O(1)。注意mid计算用 left + (right-left)/2 避免整数溢出。二分查找的变体(查找左右边界、旋转数组)是高频考题。
Q: 队列功能实现 A: 用两个栈实现队列(FIFO):
class QueueWithTwoStacks { constructor() { this.inStack = []; this.outStack = []; } enqueue(x) { this.inStack.push(x); } dequeue() { if (!this.outStack.length) { while (this.inStack.length) this.outStack.push(this.inStack.pop()); } return this.outStack.pop(); } peek() { if (!this.outStack.length) { while (this.inStack.length) this.outStack.push(this.inStack.pop()); } return this.outStack[this.outStack.length - 1]; } isEmpty() { return !this.inStack.length && !this.outStack.length; } }每个元素最多入栈两次出栈两次,均摊时间复杂度O(1)。
Q: 括号匹配判断 A: 判断字符串中括号是否正确匹配:
function isValidParentheses(s) { const stack = []; const map = { ')': '(', ']': '[', '}': '{' }; for (const char of s) { if (!map[char]) { stack.push(char); } else { if (stack.pop() !== map[char]) return false; } } return stack.length === 0; }时间复杂度O(n),空间复杂度O(n)。思路:左括号入栈,右括号检查栈顶是否匹配。最终栈为空则全部匹配。
开放问题
Q: 面试结束想了解什么 A: 面试最后提问环节是展示思考深度和求职动机的机会。建议有层次地提问:1)团队与技术:询问技术栈选型原因、当前面临的技术挑战、团队代码规范和Code Review流程。2)业务与成长:业务方向和发展规划、新人培养体系、技术分享氛围。3)工作与期望:团队成员的构成和分工、对这个岗位的期望(入职后最需要解决的问题)。避免问:薪酬福利(HR面再问)、百度可查的问题。好的提问能体现你对公司的真诚兴趣和主动思考的态度。
Q: 未来发展规划 A: 回答未来发展规划应体现成长性且与岗位需求匹配。建议分阶段表述:短期(1-2年):深耕前端技术栈,成为团队中的业务和技术骨干,掌握性能优化、工程化建设等深度领域,高质量完成项目交付。中期(3-5年):在某一方向(可视化/低代码/性能优化/Node全栈)形成专业深度,具备技术方案设计和跨团队协作能力,开始承担技术负责人职责。长期(5年+):保持技术敏感度同时拓宽广度,向架构师或技术管理者方向发展。关键点:规划要具体且真诚,将个人成长与业务发展结合。
Q: 期望加入的公司 A: 回答应结合自身价值观和公司特点。有价值的表达角度:1)技术成长:看重公司的技术氛围和挑战性业务,希望在优秀团队中提升自己。2)业务认同:认可公司产品方向和业务价值,对领域有兴趣和热情。3)文化匹配:看好公司工程师文化(技术驱动/开源贡献/质量优先)。4)稳定性与前景:公司发展稳健,有良好的技术积累和成长空间。建议提前了解公司技术博客、开源项目,在回答中引用具体信息("你们在XX方面的实践让我印象深刻")。
Q: 除开发外做的事 A: 除编码外关注技术广度和综合素质:1)技术输出:写技术博客、参与开源项目、在团队内做技术分享。2)技术视野:关注前沿技术动态(阅读Spec/TC39提案、关注W3C标准演进),参加技术会议。3)跨领域学习:了解后端(Node/Go)、DevOps(Docker/CI/CD)、设计(用户体验理解)等相关领域知识,帮助从全局视角做技术决策。4)阅读:技术书籍(系统设计、算法、架构)、非技术书籍(沟通、管理、产品思维)。5)运动与爱好:保持工作生活平衡。6)社区参与:在技术社区回答问题。这些展现了自驱学习和工程师文化认同。
Q: 如何看待加班 A: 回答应展现职业素养和灵活性:1)理解业务需要:项目关键期或线上紧急问题时,愿意投入额外时间保证交付质量。2)关注效率而非时长:反对低效的"形式化加班",注重提升个人和团队工作效率,减少不必要的加班。3)做好预期管理:询问加班频次和原因(业务增长期 vs 常态化),评估是否能接受。4)平衡理念:高效工作 = 合理工时 + 充分休息,长期高负荷加班不可持续。关键:展现愿意为结果负责的态度,同时表明你重视效率和健康。避免简单回答"能加班"或"不接受加班"。
Q: 最大缺点 A: 回答缺点要真诚但选择可改进的非核心短板:1)选择真实的缺点(面试官能识别"完美"的假话)。2)选择非核心技能的缺点(不是团队协作、责任心等关键素质)。3)先说缺点再说改进措施(展示自我认知和成长心态)。示例:"我以前做技术方案时容易追求完美导致过度设计,后来我学会了MVP思维,先交付核心功能再迭代优化。"避免说致命缺点或说"没有缺点"。
Q: 不足之处 A: 回答思路类似缺点问题,考察自我认知和成长心态。方向示例:对不熟悉的技术栈初期上手较慢,但通过系统整理笔记和mock项目练习可快速跟上;技术深度和广度平衡问题,近期正在系统学习某一领域;大团队协作经验不足,正在阅读相关书籍并主动向有经验的同事请教。好的回答结构:明确不足 → 为什么意识到 → 正在如何改进 → 目前进展如何。
Q: 优雅谈薪技巧 A: 谈薪资要展现专业和商业思维:1)做足准备:提前了解行业薪资范围和公司职级薪酬体系。2)合理定位:基于个人能力、经验和市场行情给出薪资范围(而非固定值),留有谈判空间。3)展示价值而非谈需求:强调你能为公司带来的价值("以我的经验和能力,期望薪资在XX到XX之间"),而非个人需求。4)综合评估:不只是月薪,关注总包结构(年终、股票期权、福利、晋升机制)。5)时机合适:不要在初面谈薪资,等拿到offer或终面时再沟通。6)态度诚恳:表达加入意愿的同时表明对市场的了解。