面灵AI→

放生面经-禾赛科技前端一面

时间
2026-10
来源
牛客网

《面试题目》

  1. 请说明两个等号与三个等号的区别。
  2. 在两个等号的比较场景中,若左右两边均为空对象,返回值是什么?
  3. 基本类型与引用类型在内存中的存储方式如何判断?基本类型存放在栈中有何优点?为何不能存放在堆中?栈与堆的主要区别是什么?
  4. 前端开发中常见的跨域问题有哪些?
  5. OPTIONS 预检请求在什么情况下出现、什么情况下不出现?如何设置?通过哪个请求头进行预检请求?
  6. 请从实习经历或项目经历中选取一个相对复杂的功能、需求或问题,说明其解决过程,并阐述最能体现个人技术实力与亮点的部分。
  7. 该组件的渲染逻辑是否了解?
  8. 给定一个数组,元素为数字或字符串,请完成去重操作,并说明思路。(追问:请分析该算法的时间复杂度和空间复杂度。该算法是否有优化方案?)
  9. 反问。

《参考解析》

双等号与全等号。=== 是严格相等,先比类型,类型不同直接返回 false,类型相同再按值比较(对象比的是引用地址)。== 会先做类型转换再比较:两边类型相同走 ===;null == undefined 为 true(这是规范里单独规定的,null 和 undefined 不与其他任何值宽松相等);数字与字符串比较时字符串会被转成数字;布尔值先转成数字;对象与原始值比较时调用对象的 ToPrimitive(先 valueOf 后 toString),所以 [] == false 为 true、[] == 0 为 true、[1] == 1 为 true。NaN 与任何值(包括自己)都不相等,判断要用 Number.isNaN。工程上的结论是:日常一律用 ===,只在两个场景里用 ==——判断「值是 null 或 undefined」以及明确知道自己在做有意的宽松比较。

两个空对象比较为什么是 false。对象比较比的是引用,不是内容。{} == {} 两边是两个不同的对象字面量,各自在堆上有一份独立存储,引用地址不同,所以 == 和 === 都返回 false。即使内容完全一致,{a:1} == {a:1} 也是 false。只有当两个变量指向同一个对象时(const a = {}; const b = a;)才为 true。换句话说:== 对对象不会做「深度比较」,它走的还是引用比较;要做内容比较必须手写递归、用 JSON.stringify 或 structuredClone 之类的工具(注意 JSON.stringify 会丢函数、undefined,属性顺序不同也会得出不同结果)。另外要小心 {} == {} 之外的一个坑:{} 在某些上下文被解析成空代码块而非对象字面量,所以别把它孤零零写在语句开头。

基本类型与引用类型的内存。JS 里原始值(number、string、boolean、null、undefined、symbol、bigint)是不可变的,变量里装的是值本身;对象、数组、函数是引用类型,变量里装的是指向堆上实体的地址。要看引擎实现:V8 里局部变量一般直接分配在栈上的栈帧中,属于对象的原始类型成员会内联在堆对象里(不一定单独开辟),所以「原始值一定在栈、对象一定在堆」是简化说法,但作为面试口径没问题。原始值放栈上的好处是分配释放极快(只需移动栈指针)、CPU 缓存友好、没有 GC 负担;如果把它们都放堆,每个小数字都要一次分配、一次指针解引用、一次垃圾回收,代价与收益完全不成比例。栈与堆的区别可以列成几条:栈由系统自动管理、空间小但快、按后进先出释放;堆由运行时/GC 管理、空间大但分配与回收慢、还存在内存碎片,堆上的对象需要靠可达性分析来判定是否回收。理解这套差异,才能解释为什么「频繁创建大对象」和「大量闭包」对性能与内存有不同影响。

前端常见的跨域场景。跨域的根因是同源策略:协议、域名、端口三者任一不同就是跨源,浏览器会拦截跨源请求的读取(注意请求本身常常已经发出去了,被拦的是响应)。常见场景有前后端分离部署在不同域名或端口、调用第三方开放接口、页面嵌 iframe 做通信、加载跨域图片或字体、以及本地开发时 localhost:5173 调 localhost:8080。解决办法:生产上最正规的是服务端配置 CORS 响应头(Access-Control-Allow-Origin 等),需要带 Cookie 时还要 Access-Control-Allow-Credentials: true 且源不能写成 *;同源部署或反向代理(Nginx 把 /api 转发到后端)是零前端改动的方案;开发期用构建工具的 devServer proxy;跨窗口通信用 postMessage;历史上还有 JSONP(只能 GET、需要服务端配合,现在基本淘汰)和 document.domain(已废弃)。WebSocket、<img>、<script> 这类标签请求不受同源策略限制,但这是「能发出去」不等于「能读到响应」。

OPTIONS 预检的触发条件。预检只在「非简单请求」时发生。简单请求要同时满足:方法为 GET、HEAD、POST 之一;请求头只有安全头(Accept、Accept-Language、Content-Language、Content-Type 以及少数不触发预检的头);且 Content-Type 只能是 application/x-www-form-urlencoded、multipart/form-data、text/plain 三种之一。任一条不满足——用了 PUT/DELETE/PATCH、带了自定义头(比如 Authorization、X-Token)、或者 Content-Type: application/json——浏览器就会先发一个 OPTIONS 预检。预检请求会带上两个头告诉服务端真实意图:Access-Control-Request-Method(真实方法)和 Access-Control-Request-Headers(真实的非简单头列表);服务端要用 Access-Control-Allow-Methods、Access-Control-Allow-Headers 回应对应项,并可用 Access-Control-Max-Age 缓存预检结果,减少往返。注意预检不带凭证(凭据靠 Access-Control-Allow-Credentials 控制真实请求),也别把 Authorization 配成通配符之外却忘了加进 Allow-Headers——这是最常见的 401/CORS 报错来源。

数组去重与复杂度。最简写法是 [...new Set(arr)] 或 Array.from(new Set(arr)):Set 用 SameValueZero 判等,时间复杂度 O(n)、空间 O(n),而且它天然区分 1 和 '1'(这两个会被当成不同元素保留),正好符合「元素为数字或字符串」的要求。不用 Set 的话有两条路:一是 indexOf/includes 边遍历边查,代码短但是 O(n²);二是排序后相邻比较,时间 O(n log n),但会改变原数组顺序、且数字与字符串混合排序时结果依赖默认的字典序,容易出错。若要在去重时保留原始顺序又不想引入 Set,可以用对象或 Map 做哈希表:时间复杂度 O(n),空间 O(n),但用对象做 key 时所有 key 都会被转成字符串,1 和 '1' 会被误判为同一个,要加类型前缀(比如 typeof v + ':' + v)修正;用 Map 则不存在这个问题。优化方向取决于约束:只要求时间最优就用 Set;要求原数组顺序且内存敏感可以考虑原地压缩(双指针把保留元素往前挪,空间降到 O(k));数据量大到单机扛不住,就按哈希分片再并行去重。

讲项目亮点的答法。选一个能把「难点—方案—取舍—结果」讲完整的例子,别选最大的项目。顺序是:先说清业务背景与验收标准,再说别人为什么做不了(难点在哪),然后给出你比较过的两三个方案和放弃它们的理由(这一步最体现技术判断),最后用可验证的数字收尾(性能从多少到多少、口径是什么、怎么测的)。追问「该组件的渲染逻辑是否了解」,就顺着数据流讲:数据从哪来、在哪一层被加工、更新时哪部分会重渲染、用 key 做 diff 的依据是什么、有无不必要的重复计算与状态复制。能讲清「为什么这样渲染而不是那样」比能背框架 API 有价值。