前端校招笔试题深度解析:JavaScript核心机制与手写代码实战

前端校招笔试题深度解析:JavaScript核心机制与手写代码实战 1. 从一道校招笔试题聊起前端到底在考什么每年八九月都是校招季最热闹的时候各个大厂的笔试题满天飞。前两天整理电脑里的旧文件翻到一份蘑菇街2019届校招的前端开发工程师笔试题第二套看着上面密密麻麻的批注想起当年自己坐在机房里的那个下午突然想拿出来聊聊。先说结论这套题不算难但很典型。它考察的范围包括 JavaScript 基础、异步编程、作用域与闭包、原型链、手写代码能力、HTTP 与浏览器基础、CSS 布局等覆盖面广但深度适中。这也代表了电商类大厂前端校招笔试的主流风格——不追求偏题怪题而是用一份卷子快速筛出“基础扎实、写得出代码、思路清晰”的人。如果你正在准备前端校招或者刚入行想检验自己的基础功底这份卷子值得认真做一遍。我当年做完的直观感受是每一道题都不陌生但想拿满分每一个考点都得真正吃透而不是背个答案就完事。尤其是手写代码题直接能拉开差距。这篇文章我会把这份笔试题按考点拆开逐个分析每道题背后的出题意图、解题思路、常见错误以及我实际踩过的坑。不会只给答案重点讲清楚“为什么这么考”和“考场上怎么应对”。2. 笔试题全景分析出题人是怎么设计这套卷子的2.1 题量结构与时间压力先说大框架。这份笔试题总共包含选择题、简答题、编程题三种题型题量在 25 道左右给的时间大概 90 分钟。这个题量和时间配比在互联网公司校招笔试里属于标准配置计算下来平均每题只有 3 分多钟所以你不可能在一道题上死磕太久。从实际考试体验来说选择题和简答题是热身真正的分水岭是编程题。很多人前面做得太仔细导致最后编程题没时间写这属于战略性失误。后面我会详细讲如何分配时间。2.2 知识板块占比与出题倾向根据我对这套题以及同期其他大厂笔试题的对比知识板块的占比大概如下知识板块题量占比出题倾向JavaScript 核心机制约 35%闭包、作用域、原型链、this 指向、异步等手写代码约 25%防抖节流、数组去重、深拷贝、事件总线等浏览器与网络约 20%HTTP 状态码、缓存、事件循环、DOM 操作CSS 与布局约 12%居中方式、BFC、盒模型、flex 布局工程化与框架约 8%模块化、打包工具基本概念、框架生命周期这个分布很说明问题JavaScript 是绝对的考察重点尤其是那些“背了就会、不背就懵”的核心机制题。出题人默认你简历上写了熟悉 JavaScript那么笔试就是验证你是否真的熟悉——不是能不能写页面而是对这些语言底层机制的理解程度。电商场景下的前端最核心的诉求就是复杂交互、数据驱动、大量列表渲染、异步请求管理这些都直接对应 JavaScript 语言能力的考察。所以这份卷子里的 JS 题其实是在模拟真实业务中对开发者语言功底的底线要求。2.3 难度梯度设置我观察到一个有趣的细节这套题不是按题型排难度的而是故意把简单的选择题放在前面但中间穿插一两道“陷阱题”然后在简答题里逐层加码最后编程题直接上难度。这种设计是有讲究的。它不是为了考倒你而是为了把你“分层”基础扎实的人前面拿分快后面编程题能写出来基础薄弱的人前面就卡住了后面时间更不够。所以如果你做这套题时感觉时间不够用先别急着怪自己手慢更可能是前面的陷阱题耽误了太久。3. JavaScript 核心机制逐题拆解3.1 变量提升与暂时性死区这套题里有一道很经典的题考察 var、let、const 的区别还有一个变种考到函数声明与变量声明的提升优先级。console.log(a); // ? var a 1; console.log(b); // ? let b 2;第一问输出undefined因为 var 声明的变量会被提升到作用域顶部但赋值操作不会提升所以此时a的值是undefined。第二问直接报ReferenceError因为 let 存在暂时性死区在声明之前访问变量会报错。这个考点本身不复杂但很多人在第二问上栽过跟头。原因在于平时写代码时很少在声明之前去访问变量所以对“提升”这个概念只停留在“知道”的层面没有真正理解。出题人考这道题的目的是想看你是否理解 JavaScript 代码在执行前的“预编译”阶段发生了什么。我建议换个角度理解JavaScript 代码运行过程分两步第一步是创建阶段此时 var 变量被初始化成 undefined函数声明被完整创建第二步是执行阶段逐行执行代码。let 和 const 在创建阶段会被识别但不会被初始化任何对它们的访问都会掉进暂时性死区。3.2 闭包的应用场景与内存问题闭包题在这份卷子里出现了两次。一次是选择题问以下代码的输出结果for (var i 0; i 5; i) { setTimeout(() { console.log(i); }, 1000); }这个题是经典的“循环里放异步回调”陷阱。输出是 5 个 5而不是 0 到 4。原因是 var 声明的 i 属于全局作用域循环结束后 i 的值已经变成 5而 setTimeout 的回调函数在 1 秒后才执行读取的是最终的 i 值。考场上写出正确答案只是第一步出题人真正想听的还在后面的简答题里——怎样让输出变成 0, 1, 2, 3, 4这就要用到闭包来保存每次循环的 i 值for (var i 0; i 5; i) { (function(j) { setTimeout(() { console.log(j); }, 1000); })(i); }用 IIFE 创建独立作用域把 i 作为参数传入每次迭代都保留一个独立的 j。这是最经典的解法。或者用 let 声明 i利用 let 的块级作用域特性直接解决。闭包的另一面是内存问题。简答题里有一道“闭包会导致内存泄漏吗怎么避免”这是个好问题因为它考察的不只是你会不会写闭包而是对闭包底层原理和内存管理机制有没有完整认知。闭包本身不会必然导致内存泄漏但如果不当使用闭包会让外部函数的局部变量长期被引用无法被垃圾回收机制释放。最常见的场景是某个闭包被长生命周期的对象引用而闭包内部又引用了大量外部变量这些变量就永远得不到释放。实际业务中我在写事件监听时遇到过这个问题。组件销毁时忘记移除事件监听器导致闭包一直存在页面切换到其他路由后旧组件的状态数据还在内存里。排查的时候用 Chrome DevTools 的 Memory 面板做堆快照对比能看到明显的“Detached DOM tree”和闭包引用链。3.3 原型链与继承的深入理解原型链考察在这套卷子里是重头戏。选择题部分有一道function Parent() { this.name parent; } Parent.prototype.sayHello function() { console.log(hello from parent); }; function Child() {} Child.prototype new Parent(); var child new Child(); child.sayHello(); // ?答案是能正常输出hello from parent。这里涉及两个知识点一是通过原型链Child 的实例可以沿着child.__proto__也就是 Child.prototype往上找到 Parent.prototype 上的方法二是Child.prototype new Parent()这种继承方式会让 Child.prototype 拥有 Parent 实例上的属性name和原型链上的方法。简答题里进一步考察了几种继承方式的优缺点对比。这道题非常值得展开因为每次面试我都会被问到而且我后来发现它和实际代码设计紧密结合。原型链继承Child.prototype new Parent()优点是实现简单子类能访问父类原型上的所有属性和方法缺点是所有子类实例共享父类实例上的属性如果这些属性是引用类型数组、对象一个实例修改后会影响其他实例。构造函数继承Parent.call(this)优点是可以传参每个子类实例拥有独立的父类属性副本避免了共享问题缺点是无法继承父类原型上的方法。组合继承两者结合理论上最优但父类构造函数会被调用两次多了一次性能开销。ES6 class 继承使用 extends 关键字本质上是组合继承的语法糖但底层通过Object.create()更高效地处理了原型链。实际开发中我现在基本都用 ES6 class 继承语法清晰不容易出问题。但如果你在维护老项目ES5 时代组合继承还是主流方案这个演进路线有必要搞清楚。3.4 this 指向与箭头函数this 指向是前端笔试中的“老演员”了这套题也不例外。有一道选择题const obj { name: obj, getName: function() { return this.name; }, getNameArrow: () { return this.name; } }; console.log(obj.getName()); // ? console.log(obj.getNameArrow()); // ?第一问输出obj普通函数的 this 指向调用它的对象。第二问是坑箭头函数没有自己的 this它会捕获定义时的外层 this这里定义时的外层是全局作用域在浏览器环境下 this 指向 window所以输出undefined因为 window.name 没有定义。出题人用同一个对象里两个不同定义方式的函数做对比这种设计很巧妙一眼就看出你是否真正理解箭头函数和普通函数 this 机制的核心差异。这个知识点我在实际开发中踩过坑。用 React 写类组件时在 JSX 中绑定事件处理函数如果直接用箭头函数语法handleClick () { console.log(this.state.count); }这里用箭头函数是为了让 this 绑定到组件实例。如果用普通方法语法配合 JSX 事件绑定this 会丢失指向必须在构造函数里 bind 或者用箭头函数语法。这些都属于“笔试考烂了、但实际项目照样犯”的经典场景。4. 异步编程与事件循环笔试中的“分水岭”4.1 事件循环的经典题这道题但凡做过前端笔试题的人都见过console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); Promise.resolve().then(() { console.log(promise1); }).then(() { console.log(promise2); }); console.log(script end);输出顺序是script start → script end → promise1 → promise2 → setTimeout。这道题考察的是 JavaScript 事件循环中宏任务和微任务的执行顺序。同步代码先执行然后本轮事件循环的微任务队列Promise.then 里的回调会先于下一轮宏任务setTimeout执行。让我手动走一遍这个流程代码执行到 console.log先打出 script start然后遇到 setTimeout把它加入宏任务队列遇到 Promise.resolve().then把回调加入微任务队列其实是 then 链上两个微任务执行最后一个 console.log打出 script end同步代码跑完开始清空微任务队列先执行 promise1 的回调打出 promise1然后这个回调内部触发了第二个 .then所以 promise2 被追加到微任务队列末尾本轮回调执行完后继续处理打出 promise2微任务队列清空后下一轮事件循环才开始取出宏任务队列里的 setTimeout 回调打出 setTimeout。很多人做错这道题是因为把 setTimeout 等同于“延迟 0 秒后立刻执行”。实际上即使延迟是 0它也要等当前调用栈清空、微任务队列处理完后才会执行。4.2 async/await 与 Promise 的组合题比上面的题更难一些的是 async/await 版本的async function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } console.log(main start); async1(); console.log(main end);输出顺序是 main start → async1 start → async2 → main end → async1 end。关键点在于 await 的行为await async2()会先执行 async2 内部的同步代码输出 async2然后 async1 函数暂停执行把 await 后面的代码包装成一个微任务让出执行权。外层同步代码继续输出 main end。之后微任务队列被清空async1 恢复执行输出 async1 end。我在学这个知识点时老师给过一个贴切的类比可以把 await 理解为“我先叫个号轮到我的时候再回来”。async1 函数在 await 处挂起相当于去排队了先把线程让给后面的同步代码等同步代码跑完、微任务轮到它了才继续。这套题里还考到 Promise 状态的传递和 catch 的捕获范围。有一道题是这样的Promise.reject(error) .then(() { console.log(then1); }) .catch(err { console.log(catch1, err); }) .then(() { console.log(then2); });输出是 catch1 error紧接着是 then2。这里的重点是 catch 返回的也是一个 promise而且状态是 fulfilled所以后面的 .then 会继续执行。这个知识点在实际业务中很容易踩坑。你写了一个 Promise 链中间某一步错了被 catch 兜住了然后你以为整个链条已经结束但实际上 catch 后面如果还挂接着 .then它照样会跑。在某些场景下面可能触发非预期的逻辑。4.3 事件循环题型的备考思路做这些题时我的经验是不要死背输出顺序。你要自己在纸上画一个循环图同步任务先执行调用栈压栈出栈遇到宏任务setTimeout、setInterval、I/O注册回调并加入宏任务队列遇到微任务Promise.then、MutationObserver、queueMicrotask加入微任务队列调用栈清空后先清空微任务队列微任务队列清空后取一个宏任务执行每个宏任务执行完回到上面第 4 步。把这张图刻在脑子里所有事件循环的题都能推出来。碰到 async/await记住一个原则await 后面的代码等同于.then(() { ... })的回调会被推入微任务队列。5. 手写编程题从思路到落地5.1 手写防抖与节流这套卷子的编程题第一道是手写防抖debounce第二道是节流throttle第三道是实现一个深拷贝第四道是数组去重。这四道题没有任何一道是“偏题”全是日常开发中高频使用的基础能力。先看防抖。防抖的核心思想是事件触发后等待一段时间再执行回调如果在这段时间内事件再次触发就重新计时。function debounce(fn, delay) { let timer null; return function(...args) { if (timer) { clearTimeout(timer); } timer setTimeout(() { fn.apply(this, args); }, delay); }; }这里有两个细节必须注意。第一要用fn.apply(this, args)保留原函数执行时的 this 指向否则在组件方法中调用时 this 会丢失。第二要用闭包存储 timer防止每次触发都重新创建一个新的 timer 变量导致防抖失效。我当时在这个细节上栽过跟头。写的时候只用了fn(...args)结果在 Vue 组件里调用防抖函数时函数内部的 this 变成了 undefined。所以不要小看这个 apply 的细节在浏览器环境下它可能不报错严格模式下报 undefined 错误但在严格模式下必炸。节流的核心思想是事件触发后在单位时间内只执行一次回调即使期间有多次触发也忽略。function throttle(fn, delay) { let last 0; return function(...args) { const now Date.now(); if (now - last delay) { last now; fn.apply(this, args); } }; }这是最基础的时间戳版本。它有一个特点第一次触发会立即执行最后一次触发的回调不会执行。如果需要“最后一次也执行”可以用定时器版节流或者使用时间戳和定时器结合的方式。这里插一句如果你想在项目中直接用现成的工具函数LoDash 里的 debounce 和 throttle 是最靠谱的选择它会处理很多的边界情况比如按频率自动选择在 requestAnimationFrame 阶段执行。但笔试考手写是考察你有没有理解底层原理。5.2 手写深拷贝边界情况是拉分项深拷贝这道题特别能拉开差距。基础版谁都能写个 JSON.parse(JSON.stringify())但如果只是这样这道题拿不到满分。出题人期望看到的是你能处理函数、Date、RegExp、Map、Set 等特殊类型以及循环引用问题。我的实现思路分四层function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) { return target; } if (target instanceof Date) { return new Date(target); } if (target instanceof RegExp) { return new RegExp(target.source, target.flags); } if (map.get(target)) { return map.get(target); } const cloneTarget Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); if (target instanceof Map) { target.forEach((value, key) { cloneTarget.set(key, deepClone(value, map)); }); } if (target instanceof Set) { target.forEach(value { cloneTarget.add(deepClone(value, map)); }); } Reflect.ownKeys(target).forEach(key { cloneTarget[key] deepClone(target[key], map); }); return cloneTarget; }几个核心点说一下。第一循环引用必须处理否则递归会无限循环导致栈溢出。我这里的思路是用 WeakMap 记录已经克隆过的对象遇到同一个引用时直接返回之前的结果。第二Date 和 RegExp 不能直接赋值要重新构造实例否则还是共享引用。第三用 Reflect.ownKeys 而不是 Object.keys是为了能拷贝不可枚举属性和 Symbol 键名。这个方案在笔试里完全够用。实际生产环境中更稳妥的选择是用 Lodash 的 cloneDeep因为它的边界情况比我手写的更完善。5.3 数组去重的多种思路数组去重这道题看起来简单但考察的是你能否多维度思考问题。基础版本function unique(arr) { return Array.from(new Set(arr)); }这个写法简洁高效能处理基础类型和 NaN。但如果你想展示更全面的能力还可以提供另外两个版本// filter indexOf function unique(arr) { return arr.filter((item, index) arr.indexOf(item) index); } // 利用对象键值对 function unique(arr) { const obj {}; return arr.filter(item { if (obj[item]) { return false; } obj[item] true; return true; }); }每种方案都有场景限制。Set 方案最简单但无法去重对象类型因为对象按引用比较。indexOf 方案性能较差需要 O(n²) 的时间复杂度。对象键值对方案会把数字和字符串混淆比如 1 和 1 会被当成同一个键。笔试做题时我会把 Set 方案作为主答然后在注释里补充两种方案的优缺点对比出题人一看就知道你对数组去重有过完整思考。5.4 手写题的答题策略编程题在笔试中的评分通常不是“全对或全错的二分法”而是根据你的思路、代码正确性和边界情况处理来综合评分。所以我的经验是先写核心逻辑保证主流程正确再考虑边界情况代码结构要清晰变量命名要有意义不要写一坨看不懂的代码关键步骤加中文注释方便阅卷人理解你的意图即使答案不完美也可能给步骤分如果时间不够写伪代码也比空着强至少展示出你的思路。6. 浏览器与网络基础必拿分的“性价比区”6.1 HTTP 状态码与缓存这套题里有一个板块我觉得是性价比最高的就是浏览器与网络相关的题目。它不像 JS 那样需要深度推理只要你背过就能得分属于“必拿分”区域。有一道选择题问强制缓存和协商缓存的区别。正确答案是强制缓存由 Cache-Control 和 Expires 控制命中后直接从本地读取不发请求协商缓存由 Last-Modified/If-Modified-Since 和 ETag/If-None-Match 控制命中后服务器返回 304浏览器重新定向到本地缓存。这里有一个易错点很多人以为 304 就是协商缓存的响应码但实际上协商缓存命中时服务器返回的是 304 Not Modified浏览器收到这个状态码后会从本地缓存中读取资源。强制缓存命中时浏览器根本不会发请求状态码是 200 (from disk cache)。我在实际项目里排查过一个问题页面更新后用户看到的还是旧资源强制缓存和协商缓存配得不对。后来发现是响应头里的 Cache-Control 被设为了max-age31536000这意味着浏览器在一年内都不会向服务器请求新版本。解决办法是改成协商缓存或者给静态资源文件名加哈希值新的文件名可以绕过旧缓存。6.2 浏览器渲染流程与 DOM 操作还有一道简答题问从输入 URL 到页面渲染完成浏览器做了什么。这类题是面试官的好朋友几乎每个公司都会问区别只是问法不同。完整的回答链条是DNS 解析把域名转换成 IP 地址建立 TCP 连接三次握手发送 HTTP 请求服务器返回响应浏览器解析 HTML构建 DOM 树解析 CSS构建 CSSOM 树两者合并构建 Render 树进行布局计算确定每个节点的位置绘制到屏幕Painting。如果想拿高分可以补充现代浏览器的进阶细节CSS 和 JavaScript 会阻塞渲染JavaScript 还可能修改 DOM 和 CSSOM所以 script 标签建议放在 body 底部或者使用 defer、async 属性浏览器有预加载扫描器preload scanner会提前发现资源并下载。这套卷子里还有一道多选问哪些操作会导致重排Reflow。选项有修改一个元素的宽度、移除一个 DOM 节点、修改字体大小、修改背景颜色。正确答案是前三个修改背景颜色只会触发重绘Repaint不会触发重排。我当时就把这个题做错了因为没注意到“修改背景颜色”这个选项想当然地以为任何样式变化都会重排。后来在实际项目里做性能优化时才真正理解了两者的区别重排一定会重绘但重绘不一定需要重排。重排的代价远高于重绘所以写代码时要避免频繁地修改 DOM 几何属性。6.3 跨域与常见解决方案跨域的问题在这套卷子里占了简答题的一部分。考点是什么是跨域为什么会有跨域怎么解决。跨域是由浏览器的同源策略引起的。协议、域名、端口三者任何一者不同就会形成跨域。浏览器默认阻止跨域请求读取响应内容这是安全机制防止恶意网站通过脚本获取你在其他网站的敏感数据。解决方案我通常会分类来说JSONP利用 script 标签不受同源限制的特性但只支持 GET 请求且需要后端配合返回特定格式的 JSON。CORS后端在响应头里配置 Access-Control-Allow-Origin支持各种请求方式是现在的主流方案。反向代理开发环境下用 Webpack Dev Server 的 proxy 配置转发请求绕开浏览器的同源限制生产环境则通过 Nginx 反向代理。postMessage适合页面和 iframe 之间的跨域通信。关于 CORS 我补充一点跨域请求如果是非简单请求比如带了自定义 Header或者 Content-Type 是 application/json浏览器会先发起 OPTIONS 预检请求服务器需要正确响应预检才会发送真正的跨域请求。很多时候后端只配置了 Access-Control-Allow-Origin没有配置 Access-Control-Allow-Headers 或 Access-Control-Allow-Methods预检请求直接失败浏览器报跨域错误但后端日志里看不到任何异常。这个问题排查起来非常浪费时间解决办法是在前端 Network 面板看发起的是不是 OPTIONS 请求如果是大概率是预检没过。7. CSS 布局与盒模型送分题还是送命题7.1 水平垂直居中的全部方案CSS 题目在这套卷子里数量不多但很稳定。有一道简答题要求写出实现元素水平垂直居中的各种方案。这种题属于“锦上添花”型答得越全越证明有实战经验。我从常用到冷门列一下flex 方案父容器display: flex; justify-content: center; align-items: center;这是目前最常用的方案。grid 方案父容器display: grid; place-items: center;代码更简洁但不兼容 IE。绝对定位 transform子元素position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);适合父容器有定位属性的情况。绝对定位 margin: auto子元素position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto;适用于子元素有固定宽高的情况。table-cell 方案父容器display: table-cell; text-align: center; vertical-align: middle;兼容老浏览器的时候用。inline-block 配合 line-height父容器固定高度并设置相等 line-height子元素用 inline-block。这些方案没有绝对的优劣取决于场景和兼容性要求。笔试时建议把前两种方案写出来剩下的用注释的方式说明即可。7.2 BFC 是什么能解决什么问题BFC块级格式化上下文是 CSS 高频考点。简单来说BFC 是一个独立渲染区域内部元素的布局不会受外部影响也不会影响外部。触发 BFC 的条件包括根元素 htmlfloat 不为 noneposition 为 absolute 或 fixeddisplay 为 inline-block、flow-root、table-cell 等overflow 不为 visible。BFC 最常见的应用有三个清除浮动、防止元素被浮动元素覆盖、防止 margin 塌陷。我记得做这道题时我写了 overflow: hidden 触发 BFC 来清除浮动但出题人追问了一句为什么 overflow: hidden 可以清除浮动我是这样理解的当子元素浮动后父元素的高度不会被子元素撑开导致父元素高度塌陷。此时如果给父元素设置 overflow: hidden它会形成一个 BFCBFC 计算高度时会把浮动的子元素也考虑进去从而把父元素的高度撑起来。7.3 盒模型标准的和怪异的盒模型这道题出现在选择题里问的是box-sizing的两个属性值有什么区别。标准盒模型box-sizing: content-box中元素设置的 width 只包含 content 的宽度padding 和 border 会往外撑怪异盒模型box-sizing: border-box中width 包含 content padding border 的总宽度。举个具体的例子一个宽度为 100px、padding 为 10px、border 为 1px 的元素content-box 下元素实际占据宽度是 100 10 * 2 1 * 2 122pxborder-box 下元素实际占据宽度就是 100pxcontent 会被压缩到 100 - 20 - 2 78px。实际开发中我习惯在全局设置box-sizing: border-box因为使用百分比布局时不用担心 padding 撑破容器宽度。这就是为什么很多人会在 CSS 开头写* { box-sizing: border-box; }的原因。8. 容易翻车的细节题汇总8.1 隐式类型转换这套卷子有一道让人“欲哭无泪”的选择题考察 JavaScript 中的隐式类型转换console.log([] []); console.log([] {}); console.log({} []);答案是第一个输出空字符串 因为两个空数组转成字符串后各自是空字符串拼接后还是空字符串。第二个输出 [object Object]数组转成空字符串对象转成 [object Object]拼接得到 [object Object]。第三个就有意思了在浏览器里{} []会把{}当作代码块而不是对象所以整个表达式变成 []结果是 0。这类题考的不是“这个语法有什么用”而是看你是否真正理解 JavaScript 引擎的解析过程。虽然日常开发中没人会写这种代码但如果你不理解这些细节一旦调试时遇到诡异的输出就会无从下手。8.2 关于 parseInt 的经典坑还有一道题考 parseIntconsole.log([1, 2, 3].map(parseInt));输出不是 [1, 2, 3]而是 [1, NaN, NaN]。原因是 map 会把三个参数传给回调当前值、当前索引、原数组。而 parseInt 接收两个参数字符串和进制。所以实际执行的是 parseInt(1, 0)、parseInt(2, 1)、parseInt(3, 2)。parseInt 的第二个参数接收 0 到 36 之间的数字0 表示按默认规则解析遇到 1 到 36 之外的基数时直接返回 NaN。这道题我当年做对了但后来在面试中被问到变形版本又卡住了。解决这类题的关键是记住map 传给回调的参数不止一个而 parseInt 的第二个参数是进制而不是“可选项”。如果想正确实现字符串转数字的映射应该写[1, 2, 3].map(item parseInt(item))或者[1, 2, 3].map(Number)。8.3 字符串与数组的剪不断理还乱这部分的最后一道细节题是关于字符串反转的var str hello; var reversed str.split().reverse().join(); console.log(reversed); // olleh看似简单但出题人常追加一问为什么不能用str.reverse()因为 String 类型没有 reverse 方法它是 Array 的方法。要先把字符串转成数组操作完再拼接回字符串。类似的还有字符串不是可变的任何字符串操作方法都会返回新字符串原字符串不变。这些细节题的价值在于它们考察的是语言底层的“坑”平时写代码可能永远不会遇到但遇到一次就会浪费很多时间排查。提前做过这些题至少心里有个概念。9. 考场上如何分配时间9.1 时间分配策略回到开头说的那个问题为什么很多人会时间不够我的经验是笔试的时间分配应该遵循“先易后难、分块计时”的原则。以这份 90 分钟的卷子为例我的建议是题型建议用时策略选择题20 分钟一眼看出的直接选拿不准的先跳过不要在单题上超过 2 分钟简答题25 分钟每道题控制在 5 分钟内按点作答写清楚要点即可编程题40 分钟先选最有把握的题核心逻辑写出来后再考虑边界情况检查5 分钟检查有没有漏题选择题是否有明显笔误这个时间分配的核心逻辑是编程题是拉开差距的地方必须保证充分的思考和编码时间。选择题和简答题即使有陷阱你跳过去之后回来再做也不迟。9.2 遇到不会的题怎么办笔试时最忌讳的是“死磕一道题”。如果一道题卡了 5 分钟还没思路直接跳过先做后面的。因为笔试评分通常按照题目的正确个数或者综合得分来算你花 10 分钟做对一道选择题的收益远不如用这 10 分钟写出一道编程题核心逻辑的收益。编程题如果遇到完全没有思路的也不要留白。把题目用自己的话复述一遍写下可能用到的 API然后至少写一个能跑通核心场景的简化版本。即便不能通过全部测试用例也能让阅卷人看到你的思路。9.3 编程环境的习惯问题很多校招笔试平台支持多种语言但都是在网页上写代码没有本地 IDE 的自动补全和报错提示。平时刷题最好就用笔试平台自带的编辑器练习提前适应手写代码的感觉。我见过不少同学在本地 IDE 里写得很顺一换到网页编辑器就各种语法错误这就是平时练少了。另外笔试平台一般需要选择语言版本前端岗位通常选 JavaScriptNode.js环境注意你写的代码是运行在 Node.js 环境中而不是浏览器环境中某些浏览器专属 API如 document、window是拿不到的。10. 从笔试到真实业务这些考点到底有什么用写过整套题之后你可能会想这些题真的能代表实际工作能力吗我的回答是能而且比大多数人想象得更有代表性。闭包和 this 指向在写公共组件、自定义 Hook、事件绑定的时候天天遇到事件循环和异步机制在做接口请求竞态、复杂交互流程时是绕不开的底层逻辑手写防抖节流在搜索框输入建议、滚动懒加载、窗口 resize 场景里就是直接使用的代码CSS 居中与盒模型做页面布局时每一行代码都在体现。这些考点不是知识点的堆砌而是一个前端工程师日常工作的最小知识集。笔试筛掉的是“简历上写了熟悉 JavaScript但拿到三道异步题就发懵”的人留下的是能真正用语言功底解决业务问题的人。我后来参与过几次校招简历筛选和笔试出题工作发现出题人在设计题目时核心思路就是考察“候选人能否在没有 IDE 辅助、没有搜索引擎的情况下依靠记忆加深层理解写出可靠代码”。这与日常开发的常态完全不同所以才需要提前准备和刻意训练。如果你想系统地准备这类笔试我的个人建议是把本文提到的每类知识点都整理成自己的“一张纸”正面写核心结论背面写常见坑和面试官的追问方向。考前过一遍这张纸比刷十套题都高效。因为笔试题的考察核心永远是重复的变化的只是出题的外衣。最后分享一个小技巧在线笔试时把题目里的代码先在草稿纸上手写一遍推演结果再往编辑器里敲。这个习惯能帮你发现很多“看起来正确实际一跑就错”的问题。我自己整理这套题的笔记时每一道题都保持了这个习惯最后笔试分数也确实比裸考的时候高了一大截。