2015前端笔试题复盘:闭包、原型链与性能优化核心考点
2. 这套卷子究竟在考核什么
最近整理旧笔记本里的资料,翻出来一份当年保存的人人网2015研发笔试卷E,前端方向。看着上面密密麻麻的批注,忽然觉得这套题放到现在复盘一下,依然挺有嚼头。虽然已经是好几年前的东西,但里面涉及的核心知识点——JavaScript 闭包、原型链、浏览器兼容、页面性能优化——恰好是每一代前端开发者都必须跨过去的坎。如果你正在准备面试,或者想检验一下自己的基础功底,这套卷子值得认真做一遍。
先说清楚这张卷子的背景。2015年的人人网正处于移动端浪潮的转型期,Web 端依然承担着大量用户交互场景,比如状态发布、相册上传、好友动态。笔试试卷分成通用技术基础和方向专项两块,既考验在校学生的知识面,也筛选扎实的前端基础能力。当时没有现在这么多框架轮子,笔试题大多围绕原生 JavaScript、浏览器机制、网络协议来出,反而更能看出一个人的内功深浅。
1. 试卷的整体结构与考察逻辑
1.1 2015年前端笔试为什么难
现在回想起来,2015年是一个很微妙的时间点。jQuery 还是绝大多数项目的标配,AngularJS 1.x 在企业里开始流行,React 刚刚进入国内技术圈的视野,ES6 虽然已经定稿但还没有大规模普及。这种技术交替期有一个特点:面试官知道新东西的出现是趋势,但团队里线上跑的还是老代码,所以笔试题目会同时覆盖“传统基本功”和“新方向敏感度”两条线。
传统基本功的考察集中在三块:JavaScript 的语言特性、浏览器兼容性处理、页面性能优化。而新方向敏感度,通常只会作为附加题出现,比如“是否了解 ES6 的新特性”“对组件化开发有什么理解”,占分不多但能拉开差距。人人网这套卷子也遵循了这个规律,整张卷面大约三分之二是基础题,三分之一是开放性设计题。
1.2 卷面模块分布与分值逻辑
从题型分布来看,常见的结构是这样的:
| 模块 | 类型 | 占比 | 考察目标 |
|---|---|---|---|
| HTML/CSS | 填空 + 简答 | 20% | 布局基本功、语义化理解 |
| JavaScript 基础 | 写结果 + 代码补全 | 40% | 语言特性掌握程度 |
| 浏览器/网络基础 | 简答 | 15% | 页面加载全链路理解 |
| 性能优化 | 简答 + 方案设计 | 15% | 实战经验 |
| 算法逻辑 | 编程题 | 10% | 基础算法能力 |
这里要特别提一下 JavaScript 部分为什么占这么大比重。2015年还没有 TypeScript,也没有现在这么完善的组件生态,前端交互全靠手写 JS 撑着。一个候选人如果连闭包、原型、this 指向都说不清楚,后续培养成本会非常高。所以笔试卷里 JS 的占比高,本质上是在筛选“能直接上手写交互的人”。
2. JavaScript 闭包与作用域:这张卷子的核心难点
2.1 经典闭包题目:循环里绑定事件
当年的笔试题里,出现频率最高的一道题长这样:
function bindClick() { var list = document.querySelectorAll('li'); for (var i = 0; i < list.length; i++) { list[i].onclick = function() { console.log(i); }; } }问:点击第 3 个 li 时,控制台输出什么?答案是 3(如果列表长度足够),而不是 0、1、2。很多人当时第一眼会以为输出的是当前索引,但实际运行结果会让不少人懵一下。
原因在于var i声明在函数作用域内,循环结束后i已经变成了最终的列表长度值。所有点击回调函数引用的是同一个i变量,当事件真正触发时,循环早就跑完了,取到的自然是最终值。要修复这个问题,当时的常规方案是利用 IIFE 创建独立作用域:
for (var i = 0; i < list.length; i++) { (function(index) { list[index].onclick = function() { console.log(index); }; })(i); }如果放在 2015 年,能写出这个 IIFE 版本已经算合格。如果还能补一句“用 ES6 的 let 声明也是可以的”,面试官会高看一眼。因为let在每个循环迭代中都会创建一个块级作用域绑定,这个问题从语言层面被直接解决了。
2.2 this 指向与作用域链的综合题
除了闭包,这张卷子还喜欢考察this的指向。有一个经典题目几乎每个版本都会出现:
var name = 'window'; var obj = { name: 'obj', getName: function() { return function() { return this.name; }; } }; console.log(obj.getName()());答案是'window'。因为obj.getName()执行后返回了一个普通函数,这个函数在执行时并没有被 obj 对象调用,而是孤立的调用,所以this指向全局对象。在浏览器环境里,全局对象的name属性恰好就是'window'字符串。
这道题其实是在考一个关键概念:函数的this取决于调用方式,而不是定义位置。很多人把this和作用域链搞混,以为函数定义在 obj 内部,this就应该指向 obj,这是典型的误解。作用域链是词法层面的,而this是运行时绑定的,两者完全是两码事。
如果题目再加一个变体,问如何让返回值里的this指向 obj,解法就有三种:在外层函数里缓存var that = this,用bind绑定,或者用箭头函数。这三种写法在 2015 年的卷子上都能拿到分,但得分点不一样——能说出箭头函数的,说明对 ES6 有主动了解。
2.3 原型链:手写一个继承
当时还有一个高频考点是原型链继承。一个典型题目是:
function Animal(name) { this.name = name; } Animal.prototype.sayName = function() { console.log(this.name); };要求写一个 Cat 类继承 Animal,并且 Cat 的实例能调用sayName方法。如果只知道默认的原型继承写法,这道题只能得一半分,因为很多人会写出这样的代码:
function Cat(name) { Animal.call(this, name); } Cat.prototype = new Animal();这写法有个问题:new Animal()会生成一个带name属性(此时是 undefined)的实例,然后把它当作 Cat 的原型。如果 Animal 的构造函数内部有副作用或者需要参数初始化,这个调用会造成不必要的执行。更规范的做法是Cat.prototype = Object.create(Animal.prototype),然后修正Cat.prototype.constructor = Cat。
为什么要把constructor指回来?这是一个细节考点。因为经过原型重写后,Cat.prototype变成了一个全新的对象,它的constructor默认指向Object而不是Cat,如果不手动修正,instanceof的结果不会受影响,但cat.constructor === Cat会变成 false,这在某些依赖constructor做类型判断的代码里会出问题。
3. 浏览器兼容与 DOM 操作:那年的基本功
3.1 事件处理的兼容差异
2015 年的笔试是不敢绕开浏览器兼容的,因为当时 IE6/7/8 虽然已经呈下降趋势,但 IE8 在部分企业环境里依然活跃。人人网的用户群体里,IE 用户占比远高于现在,所以前端面试题里必然会出现事件兼容的题目。
一个典型题目是:如何兼容 IE 和标准浏览器绑定事件?
function addEvent(elem, type, handler) { if (elem.addEventListener) { elem.addEventListener(type, handler, false); } else if (elem.attachEvent) { elem.attachEvent('on' + type, handler); } else { elem['on' + type] = handler; } }关键考点有四个:一是attachEvent的on前缀不能漏;二是addEventListener的第三个参数false表示冒泡阶段触发,当时的主流实践是挂冒泡而不是捕获;三是attachEvent里this指向window而不是当前元素,这差异很隐蔽;四是addEventListener可以给同一元素重复绑定多个相同事件,而onclick赋值会覆盖前一个。
顺带还要提一个比较高频的题目:如何阻止冒泡和取消默认行为。标准写法是e.stopPropagation()和e.preventDefault(),IE 下则是e.cancelBubble = true和e.returnValue = false。一道小简答题,能答全细节的人不多,因为大部分人只会记标准写法,对 IE 的写法不熟悉。
3.2 封装一个兼容的 XMLHttpRequest
除了事件,DOM 操作里还有一个常考的点是 Ajax 封装。当时已经开始用 jQuery 的$.ajax,但笔试想要的是让你不依赖库实现一个兼容的请求对象。
function getXHR() { if (window.XMLHttpRequest) { return new XMLHttpRequest(); } else { return new ActiveXObject('Microsoft.XMLHTTP'); } } function ajaxGet(url, callback) { var xhr = getXHR(); xhr.open('GET', url, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(xhr.responseText); } } }; xhr.send(null); }这里还藏着一个考点:readyState === 4表示请求完成,但 HTTP 状态码是 200 才代表成功。有些人会把 304(走缓存)也当成成功处理,如果题目要求考虑缓存场景,通常还要判断xhr.status === 200 || xhr.status === 304。不过从实际产品角度,304 后浏览器往往已经帮你取到了缓存内容,responseText 里可能拿到的是空字符串,所以不能简单把它等同于请求成功。
3.3 获取元素位置的兼容处理
还有一道让我印象很深的题,要求写出获取元素在页面上绝对位置的函数。核心是offsetTop和offsetParent的循环累加,但要处理边界情况:
function getAbsoluteTop(element) { var top = 0; while (element) { top += element.offsetTop; element = element.offsetParent; } return top; }考点在于:offsetParent可能为 null,所以循环条件要用element而不是element.offsetParent;offsetTop是相对offsetParent的位置,不是相对 document;还要注意position: fixed的元素offsetParent行为在不同浏览器下不一致。这题不算难,但能把边界条件考虑完整的人并不多。
4. 性能优化考点:笔试里的送分题与陷阱区
4.1 页面加载性能的经典考察
人人网这类内容型产品,页面首屏加载性能直接影响用户留存。笔试卷里有一道几乎必考的题:请列出你能想到的网页性能优化方案。
这道题看着是送分题,其实分水岭很大。基础差的同学会写出“压缩 JS/CSS”“合并图片”“用 CDN”这三板斧,但这只能拿基础分。高分答案通常还要覆盖以下几个方面:
- 减少 HTTP 请求次数:合并脚本、CSS Sprites、内联小图
- 资源加载策略:脚本放底部、样式放头部、按需加载非关键资源
- 缓存策略:静态资源带版本号,利用浏览器缓存
- 渲染层面:减少 DOM 操作、避免强制同步重排
- 服务端优化:开启 gzip、设置合理的响应头
如果能在答案里写一句“对首屏非关键图片做懒加载”,面试官会觉得你有实际项目经验,因为这已经是 2015 年比较新的实践了。
4.2 重排与重绘:区分关键概念
有一道追问很常见:什么是重排(reflow)和重绘(repaint),哪些操作会引起重排?
这题考察的是对浏览器渲染机制的理解。把 JS 代码风格融入答案会更有竞争力:读offsetWidth、scrollTop、clientTop时,如果前面修改过样式但还没触发渲染,浏览器会强制同步重新计算布局,这是最容易引发性能问题的操作之一。解决思路是把读操作一次性收集起来,或者用requestAnimationFrame把写入延迟到下一次渲染帧。
当时的笔试卷上,能准确说出“读取布局属性会导致强制同步布局”的人非常少,因为大家只停留在“重排就是重新计算位置,重绘就是重新画”这种表面理解。如果有余力,可以在答题时补充一个例子:
var width = el.offsetWidth; // 读 el.style.width = width + 10 + 'px'; // 写这种连续读写会造成多次布局抖动,高绩效答案会把读写分离。这部分虽然偏实战,但在笔试里简单提到就能加分。
4.3 懒加载的实现思路
卷子最后的信息题里,有一道问:如何实现图片懒加载?
这里不需要写出完整代码,但需要给出一套可落地的思路。常规做法是监听scroll事件,判断图片是否进入视口,一旦进入就把>function check() { var img = document.querySelector('img[data-src]'); if (!img) return; var rect = img.getBoundingClientRect(); if (rect.top < window.innerHeight && rect.bottom > 0) { img.src = img.getAttribute('data-src'); img.removeAttribute('data-src'); } } window.addEventListener('scroll', check);
这里有一个容易被忽视的点:getBoundingClientRect返回的是相对视口的位置,所以判断条件只需要比较rect.top是否小于视口高度即可,不用手动加scrollTop。很多人会搞混这个坐标系,写出来的判断条件复杂而且容易错。
5. 网络与缓存基础:一道简答题背后的知识体系
5.1 HTTP 状态码的掌握程度
网络基础在当年的笔试卷里不算大头,但一定会出现。常见的题目是:请列举 200、301、302、304、403、404、500、503 分别代表什么。
这道题的难点在于 301 和 302 的区别,以及 304 的缓存机制。301 是永久重定向,浏览器会缓存重定向结果,后续请求直接跳到新地址;302 是临时重定向,每次请求还是先访问原地址。真实项目中,301 常用于域名迁移,302 常用于未登录跳转。能答出这个场景区别,说明不只是背状态码,而是理解过实际业务。
304 这个状态码也很容易答偏。304 表示的是“服务端资源未修改,可以用缓存副本”,不是一种错误。整个交互过程是:客户端请求资源时带上If-Modified-Since或If-None-Match,服务端检查后发现资源没有变更,就返回 304 和空响应体,客户端接着用本地缓存。这可以减少不必要的传输。很多人把 304 和“请求失败”画等号,这在缓存面试题里是明显扣分点。
5.2 浏览器缓存的几个响应头
关于浏览器缓存,还有一道简答题:说说Expires和Cache-Control和ETag的区别。
Expires是 HTTP/1.0 时代的产物,指定一个绝对的过期时间,但客户端时间和服务器时间不一致时会出问题。Cache-Control是 HTTP/1.1 的标准,max-age指定相对存活时间,更可靠。ETag是实体标签,比较内容是否变化,配合If-None-Match使用。强缓存和协商缓存的先后顺序应该是:先看Cache-Control/Expires,没命中再看ETag/Last-Modified。
为了讲清楚,我发现一个生活类比很好用:强缓存就像是冰箱里的酸奶,上面写着保质期,保质期内你直接喝就行,不用问商家;协商缓存像是你拿着旧照片去问商家“这个商品是不是还是这样”,商家说没变你再继续用。
这道题能拿高分的同学,通常还会主动提一嘴:Last-Modified的精度是秒级,如果资源在 1 秒内被修改两次,就检测不到变化,所以ETag更可靠。这种小细节,不是背题能背出来的。
6. 算法逻辑与开放设计题:从笔试卷看思维格局
6.1 字符串处理类的简单算法题
2015年的前端笔试题对算法要求不算高,通常是一两道数组/字符串处理的基础题。最常见的题目是:写一个函数判断字符串是否为回文。
function isPalindrome(str) { str = str.replace(/[^0-9a-zA-Z]/g, '').toLowerCase(); return str === str.split('').reverse().join(''); }考察点是字符串的常用方法组合:正则去除非字母数字、转为小写、分割反转再拼接。还有一种写法是用双指针从两端向中间比较,复杂度是 O(n),但笔试实际考察的主要是基本功是否熟练,而不是复杂度优化。
还有一道字符串去重的题目也出现过:
function unique(str) { var result = ''; for (var i = 0; i < str.length; i++) { if (result.indexOf(str[i]) === -1) { result += str[i]; } } return result; }如果能写出indexOf已经可以,如果写一个Object作为哈希表去重,那就更靠近高级答案。这说明你对 JavaScript 数据结构有实际应用能力,而不仅是会用 API。
6.2 组件设计题:怎么设计一个轮播图
开放设计题在整个卷子里最有区分度。常见的一道题:请设计一个图片轮播组件的 API 和核心逻辑,要求支持自动播放、循环播放、点击切换。
这道题不要求写完整代码,但要求描述清楚结构。我当时给出的思路是:对外暴露init、goTo、prev、next、play、stop方法,内部用索引currentIndex维护当前状态,用setInterval实现自动播放,切换时计算偏移量并设置容器的transform或left值。
更高质量的答案会补充两个设计细节:一是循环播放时的无缝衔接怎么做(第一张前面克隆最后一张,或者到边界时直接跳转但带过渡动画);二是自动播放时鼠标 hover 要暂停,离开后恢复;三是如果用户快速点击多次,要防止动画堆积,最好做节流或重置定时器。能主动想到这些边界情况的人,在项目里大概率是一个靠谱的开发者。
7. 复盘:这份卷子给今天的前端开发者留下了什么
7.1 2015年与现在的考点变化
现在回头看这份卷子,有些知识点已经成了历史,比如attachEvent,没有人在新项目里会用到;但更多内容只是换了一副面孔出现。闭包依然是面试高频题,只是提问方式变成了“这段 React 代码为什么拿不到最新 state”或“这个闭包内存泄漏怎么排查”。性能优化还是必考,只是从“合并静态资源”升级成了“分析 LCP、FCP、CLS”。
我最大的感受是:通识知识的折旧速度比想象中慢。当年花了很久搞懂的this指向问题,在 React 类组件里依然让人踩坑;当年在笔试题里画的原型链,在封装高阶组件时照样用得上。反过来说,那些只背 API 而不理解原理的候选人,到了今天这个框架生态里依然很难适应。
7.2 准备笔试的三个建议
结合当年备考和带新人的经验,我给准备研发笔试的同学三个建议。
第一,把 JS 的语言机制吃透,不要只刷 API。闭包、作用域链、原型链、事件的运行机制,这些是前端面试的“母题”,任何框架题背后都逃不开这些。第二,做笔试题时不要把思路写得太跳跃,面试官看的是你的推导过程,代码写不出来,哪怕用文字把解决思路写清楚也有分。第三,考前了解一下目标公司的主流技术栈,在开放题里体现出你对该技术方向的了解,而不是空谈概念。
8. 关于这套笔试卷的一点点后记
我记得当时做完这套卷子之后,还特意去找了几家同学对答案。有人觉得这些题目太“基础”,和实际项目脱节,但后来工作几年再回头看,这些基础其实是所有业务的底层地基。直到现在,我面试候选人时依然会不自觉地用闭包、this、事件代理这几个问题来试探对方的 JS 功底,无论对方简历上写了多少个框架。
如果你正打算投递前端岗位的研发校招,不妨把这套卷子的知识点过一遍。不用背答案,而是要把每个问题背后的原理搞清楚,尤其是“为什么”这个层面理解了,面试官问什么变体你都不慌。
