尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

2015腾讯前端笔试题解析:从闭包到原型链的前端面试核心

2015腾讯前端笔试题解析:从闭包到原型链的前端面试核心 2015年腾讯春招的这套web前端开发练习卷现在回头看几乎就是当年前端面试的“知识地图”。我当初刷这套题的时候还在用jQuery写页面对MVVM只有模糊的概念对原型链更是似懂非懂。没想到几年后再翻出来看发现里面考的很多东西放到今天依然是面试高频题只是问法换了一层皮。这篇文章我想借着这套练习卷把前端面试笔试背后的考察逻辑拆开聊一聊。不仅把题目类型和参考答案梳理清楚更想聊聊为什么面试官当年要这么考以及这些知识点在今天的前端面试里还在怎么变形出现。无论你是准备校招的应届生还是准备跳槽的初级前端这套题都值得认真过一遍——不是为了背答案而是为了搞懂前端面试到底在筛选什么能力。1. 先聊这套题2015年前端面试到底在考什么1.1 2015年的前端技术栈和现在完全是两个世界2015年这个时间点很特殊。ES6规范刚在年中定稿但浏览器兼容还远没跟上生产环境里写的大多还是ES5。jQuery还是绝对的主流AngularJS 1.x正在国内技术社区收割热度React刚发布不久很多人还在观望Vue才刚刚起步。移动端H5开始爆发但大家还在用手写rem和媒体查询来做适配CSS预处理器是加分项而不是必备项。在这种技术背景下笔试面试的考点高度集中在JavaScript语言本身而不是框架。你会发现这套练习卷几乎没有考任何框架API考的全是底层能力变量类型、作用域、闭包、原型、DOM操作、事件机制、浏览器兼容。为什么因为当年技术选型百花齐放招人最怕的是只会用某个框架、但不懂浏览器和语言底层的人。今天React/Vue二分天下面试虽然会深挖框架原理但底层语言能力的考察权重依然很高只是从直接出题变成了“在场景中考察”。1.2 这套卷子背后的人才筛选逻辑腾讯当年的校招前端笔试题整体筛选逻辑可以概括成一个三层漏斗。第一层是筛掉基本功不扎实的人。题型以选择题和填空题为主覆盖面非常广包括数据类型判断、字符串方法、数组方法、运算符优先级、作用域、事件冒泡等。这一层考察的是“有没有系统学过JavaScript”而不是“会不会写页面”。很多用jQuery写页面写得很熟练的人在这个环节就挂了因为压根不关注语言层面的细节。第二层是筛掉只会背API、不理解机制的人。典型的就是闭包、this指向、原型链这类题。这些题光背概念没用必须在实际场景里能推断出运行结果。这层考察的是“有没有理解JavaScript的核心运行机制”。第三层是筛掉动手能力差的人。编程题一般有数组去重、事件封装、简易计算器这类题目不难但要求写出健壮、可用的代码考察编程习惯——边界处理、变量命名、代码结构。很多人一看题目会觉得简单但真动手写各种问题就冒出来了。这三层筛选逻辑放到今天依然成立。所以这套题的价值不在于题目本身有多难而在于它清晰展示了一套“从语言基础到运行机制再到动手能力”的考察链路。2. 核心题型拆解JavaScript基础与闭包2.1 面试官为什么会对闭包情有独钟闭包是2015年校招笔试题里出现频率极高的话题因为它一题能同时考察三个能力变量作用域是否清晰、函数表达式是否熟练、是否理解函数创建与执行的环境关系。闭包的定义其实很简单函数内部引用了外部函数的变量并且这个内部函数在外部函数执行完毕后仍然可被访问。更直白地说函数记住了自己出生时的环境。当年有一道很经典的题问下面的代码输出什么for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }正确答案是输出五个5。原因在于var声明的i是函数作用域循环结束后i已经变成5五个定时器回调执行时访问的都是同一个i。这题考的不只是异步更是作用域共享的本质。加分回答是用三种方式修复// 方式一IIFE 传参 for (var i 0; i 5; i) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); } // 方式二bind 绑定 for (var i 0; i 5; i) { setTimeout(console.log.bind(console, i), 100); } // 方式三let 块级作用域 for (let i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }这里有个特别容易踩坑的点let方式在2015年的环境里兼容性不行IE全挂所以当时面试官期待的主流答案是IIFE。今天再答这题let已经是首选了因为ES6的块级作用域让这个老坑从设计层面消失了。但从面试答题角度能把三种方案都讲出来并说明各自的兼容性和适用场景这才是满分回答。2.2 闭包的实际用途不只是面试考点闭包被问得多也因为它在真实业务里确实很有用。当年我在项目里用闭包最多的地方是封装私有变量。比如做个计数器不希望外部直接改内部状态function createCounter() { var count 0; return { increment: function() { count; return count; }, decrement: function() { count--; return count; }, getCount: function() { return count; } }; } var counter createCounter(); counter.increment(); // 1 counter.increment(); // 2 console.log(counter.getCount()); // 2 console.log(counter.count); // undefined这其实就是模块模式的原型。后来我理解ES Module和CommonJS发现它们的核心思想跟这个是一样的只不过把闭包封装提升到了文件级别。理解了闭包再去看打包工具编译出来的代码很多东西就通了。面试时如果被问到“闭包有什么实际应用”不要只答“读变量、保存变量”最好能现场手写出这样一个封装私有状态的例子再提到防抖节流函数也是用闭包保存状态这样会显得你是真的会写代码不是背概念。注意闭包会保留外部函数的活动对象导致这些变量无法被垃圾回收。如果用完闭包后不再需要最好把引用置为null。在IE时代还要小心循环引用造成的内存泄漏现代浏览器基本没有这个问题但长生命周期页面里大量使用闭包该释放的引用还是要主动处理。2.3 this指向问题一题能辨别“真懂”和“背答案”this指向是2015年笔试最爱出的另一类题。原因是它没法靠背必须理解调用方式对this的影响。有个典型题目是判断下面的输出var name window; var obj { name: obj, fn: function() { console.log(this.name); } }; obj.fn(); // obj方法调用 var f obj.fn; f(); // window普通函数调用 obj.fn.call({name: call}); // callcall改变this var f2 new obj.fn(); // undefined构造函数里的this指向新对象答好这道题关键是把this的绑定规则梳理清楚。我在当时的笔记里记了四句话作为对象方法调用this指向该对象作为普通函数调用this指向全局对象浏览器里是window用call、apply、bind调用this指向指定的对象用new调用this指向新建的实例对象。这套规则今天依然适用只是多了一个箭头函数的特殊情况——箭头函数本身没有this它沿用了外部词法作用域的this。所以现代前端面试里这道题会升级成箭头函数版本问你在对象方法里用箭头函数和普通函数的区别本质上考的还是同一套规则。我最推荐的答题方式是把规则说完之后补一个场景题事件回调里怎么保存this这是当年的高频变形题。// 错误示范回调里this指向按钮元素 button.addEventListener(click, function() { var self this; // 常见做法先存this setTimeout(function() { self.disabled true; }, 1000); }); // 箭头函数方案 button.addEventListener(click, function() { setTimeout(() { this.disabled true; }, 1000); });能把这个场景题讲明白比干背十条规则都有说服力。3. 编程实操数组去重、事件委托与ajax封装3.1 数组去重的五个版本你能写到第几个几乎每个前端求职者在2015年都遇到过这道题“写一个数组去重的方法。”它考的不只是能不能实现还有你对JavaScript数据结构和API的掌握深度。我当时在做这道题时一口气对比了五种实现方式每种都有不同的性能特征和兼容性要求方案核心思路优点缺点双重循环逐个比较重复则跳过兼容性最好任何环境可用O(n^2)复杂度性能差indexOf遍历时检查是否已存在代码简洁同样O(n^2)不支持IE8-排序后相邻去重先排序再比对相邻项可分步完成改变了原数组顺序对象键值对利用对象键唯一性O(n)性能好无法区分数字1和字符串1filterindexOf函数式写法代码优雅性能同上且只保留第一个最值得深入讲的是对象键值对方案因为当年很多人在这里翻车。你能区分1和1吗用obj[item]做键会把数字1和字符串1当成同一个键。解决办法是键名加上类型信息function unique(arr) { var obj {}; var result []; for (var i 0; i arr.length; i) { var key typeof arr[i] _ arr[i]; if (!obj[key]) { obj[key] true; result.push(arr[i]); } } return result; }这样1会被转成number_11会被转成string_1两者互不干扰。这个思路后来在ES6里可以用Set一行搞定var unique arr [...new Set(arr)];但老实说如果你面试时直接写这行面试官要么觉得你懂ES6要么觉得你没经历过底层问题。最好两种都能写先说思路再说ES6方案顺便聊聊为什么Set能去重——它内部用了类似SameValueZero的比较算法。经验之谈数组去重这道题真正拉开差距的不是能不能写出方案而是能不能把方案的边界条件和历史演变讲清楚。面试官通过这题看的是你对语言本身的了解程度而不是你会不会背一个函数。3.2 事件委托从手写实现到性能优势2015年的笔试和面试里事件委托也是高频题。它考的是DOM事件机制的核心理解事件冒泡。事件委托的原理很直白不把事件监听器逐个加到子元素上而是加到父元素上利用事件冒泡机制在父元素上统一处理。这样有两个明显好处一是动态添加的子元素不需要重新绑定事件二是减少监听器数量节省内存。当年的一道典型题是给一个ul下面动态生成的li绑定点击事件。很多人第一反应是循环绑定但如果li是异步加载出来的循环绑定的时机就不对。事件委托轻松解决var list document.getElementById(list); list.addEventListener(click, function(e) { var target e.target; if (target.tagName.toLowerCase() li) { console.log(target.textContent); } });这里有一个必须注意的细节如果用e.target判断点击li内部的span或a标签时target会落在子元素上。所以健壮的写法是用closest或者循环上溯找最近的lilist.addEventListener(click, function(e) { var li e.target.closest(li); if (li) { console.log(li.textContent); } });closest这个API在2015年还不太普及IE不支持当时更通用的做法是function getParentByTagName(el, tag) { while (el el.parentNode) { el el.parentNode; if (el.tagName el.tagName.toLowerCase() tag) { return el; } } return null; }这题如果你能顺着往下讲为什么要用事件委托、它解决了什么问题、如何处理target不是目标元素的情况、怎么兼容老浏览器基本就是高分行为了。今天React的合成事件系统底层原理也是事件委托——把所有事件挂在根容器上统一管理。把这个关联说出来面试官会觉得你有知识迁移能力。3.3 手写一个ajax封装兼容性处理是重头戏2015年前端面试几乎必考手写ajax因为当时fetch还没有普及axios也刚出来不久大家日常开发用的都是jQuery的$.ajax。面试考手写ajax是在考察你知不知道jQuery底层做了什么。基础版本要能写出这样的代码function ajax(options) { var xhr new XMLHttpRequest(); var method (options.type || GET).toUpperCase(); var url options.url; var async options.async ! false; var data options.data || null; if (method GET data) { url (url.indexOf(?) -1 ? ? : ) serialize(data); data null; } xhr.open(method, url, async); xhr.onreadystatechange function() { if (xhr.readyState 4) { if ((xhr.status 200 xhr.status 300) || xhr.status 304) { options.success options.success(parseData(xhr.responseText)); } else { options.error options.error(xhr.status, xhr.statusText); } } }; if (method POST) { xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded); xhr.send(serialize(data)); } else { xhr.send(); } } function serialize(data) { var result []; for (var key in data) { if (data.hasOwnProperty(key)) { result.push(encodeURIComponent(key) encodeURIComponent(data[key])); } } return result.join(); } function parseData(text) { try { return JSON.parse(text); } catch (e) { return text; } }这个封装里最有考察价值的细节有这么几个。第一是encodeURIComponent对参数值做编码是很多人会漏掉的。如果参数里带中文或特殊字符不编码会直接导致请求出错。第二是状态判断xhr.status的校验不能只判断 200因为浏览器对304缓存的策略在各浏览器间并不完全一致老IE尤其容易出现怪异行为。把304放进来考虑是一个经验细节。第三是readyState 4这个只是表示请求完成不代表成功。真正的成功状态要看HTTP状态码。这两个概念初学者极容易混淆。第四是GET和POST的参数处理方式不同GET拼URLPOST放body而且POST要手动设置Content-Type。这个也是当年常见的丢分点。我在实际笔试时通常还会把onreadystatechange换成onload事件再做一版因为onload只需要判断status逻辑更清晰。但在2015年onload的兼容性不如onreadystatechange好所以笔试里稳妥答案还是前者。4. 那些年必考、这些年依然没变的硬核知识4.1 原型链与继承理解JavaScript面向对象的钥匙2015年的题里原型链相关题目几乎是必出的。比如给一个对象让你顺着原型链找它的构造函数和顶层原型或者写几种继承方式并比较优劣。我当时整理笔记时把原型链的核心画成了一张概念图实例的__proto__指向构造函数的prototype函数的prototype又有一个constructor指回函数自己最终所有对象沿着原型链一路向上找到Object.prototype再往上就是null。这张图必须烂熟于心因为几乎所有原型链题目都能靠它推出来。关键是理解new一个对象时发生了什么。这是一个非常经典的面试手写题function create(Constructor, ...args) { // 1. 创建空对象原型指向构造函数的prototype var obj Object.create(Constructor.prototype); // 2. 把构造函数里的this指向这个新对象并执行 var result Constructor.apply(obj, args); // 3. 如果构造函数返回了对象则返回它否则返回新对象 return (typeof result object result ! null) ? result : obj; }原型的实际价值在哪里最典型的应用是方法共享。把方法挂到prototype上所有实例共享同一个方法引用而不是每个实例都拷贝一份。这在内存使用上优势非常明显。JS里很多内置方法就是这么设计的比如Array.prototype.map、String.prototype.trim所有数组和字符串实例都能调用但内存里只有一份实现。继承方面当年最流行的是组合继承和寄生组合继承。我笔试时最喜欢用的是寄生组合继承因为它在实现继承的同时避免了调用两次父构造函数的问题。后来ES6的class语法本质上就是寄生组合继承的语法糖。面试时如果能把class继承和原型继承的对应关系讲清楚会显得你理解得很透。提醒理解原型链不需要死记硬背最有效的方式是用console.dir打印一个实例对象展开__proto__一层层看看它从哪里来、能访问到哪些属性。自己动手看几次比背十遍图都管用。4.2 浏览器渲染原理从输入URL到页面显示这套题里还有一类占比不小的题目是浏览器相关的。2015年常考的是输入一个URL之后发生了什么这个题到今天依然是面霸级的开放式问题。我当时回答这个问题的标准框架是DNS解析域名拿到IP地址建立TCP连接完成三次握手发送HTTP请求服务器返回HTML浏览器解析HTML构建DOM树同时解析CSS构建CSSOM遇到相关资源会触发浏览器对新资源的请求CSS会阻塞渲染JS会阻塞DOM解析所以有些脚本标签会放在body末尾或者使用defer合并DOM树和CSSOM生成渲染树布局计算每个节点的几何位置绘制把节点绘制到屏幕上。这个框架今天依然可以用但可以加深一层把后面几步和性能优化结合。比如布局和绘制对性能的影响如果频繁修改DOM会导致强制同步布局这是页面卡顿的常见原因。现代前端面试问浏览器渲染更倾向于考“为什么React需要虚拟DOM”以及“如何避免布局抖动”本质还是这套基础框架的延伸。4.3 移动端适配与性能优化2015年的热点方向2015年移动端H5爆发所以腾讯的校招题里也出现了移动端适配相关的内容。主要考点集中在viewport设置、rem适配方案、图片优化、首屏加载速度这几个方面。viewport这块当时的标准答案是meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalablenorem适配的核心是动态设置根字号。当时的经典实现是以750px设计稿为基准把根字号设置为视口宽度的十分之一然后按设计稿标注除以100得出rem值。我还记得当时的写法(function(doc, win) { var docEl doc.documentElement; var resizeEvt orientationchange in window ? orientationchange : resize; var recalc function() { var clientWidth docEl.clientWidth; if (!clientWidth) return; docEl.style.fontSize clientWidth / 10 px; }; recalc(); win.addEventListener(resizeEvt, recalc, false); })(document, window);这个方案现在看是粗糙的但在2015年它确实解决了大屏小屏字体自适应的问题。后来出现了更精细的vw方案以及flexible方案的逐步退场这个演进过程本身就是面试的好谈资。性能优化方面2015年常考的点有合并压缩请求、CSS雪碧图、图片懒加载、CDN加速、减少DOM操作、事件委托。这些都是当年页面优化的常规操作。现在再问性能优化答案会变成Tree Shaking、代码分割、HTTP缓存策略、资源预加载、Web Worker这些但底层思想是一脉相承的——减少传输、减少计算、减少阻塞。面试时能把新旧优化手段的对应关系梳理出来会让你的回答有纵深感。5. 常见问题与避坑经历5.1 笔试现场最容易踩的坑从我自己做题和批改经验来看2015年这类前端笔试题的失分点其实非常有规律。提前知道这些坑刷题效率会高很多。第一个坑是运算符优先级。比如console.log(1 2 3)和console.log(1 2 3)的输出完全不同前者是字符串拼接后者因为有单目运算符把字符串转成了数字。这类题考的是表达式的评估顺序很多人基础不牢就在这里丢分。第二个坑是隐式类型转换。[] ![]这个表达式在JavaScript里结果是true第一次看到的人基本都懵。原因是![]先被转成了false然后两边都转成数字比较[]转数字是0false转数字也是0所以相等。这类题看着奇葩但它确实反映你对JavaScript类型转换体系的把握程度。第三个坑是与浏览器兼容相关的题目。比如IE下获取事件对象要用window.event标准浏览器用事件回调里的参数addEventListener在IE8及以下不支持要用attachEventevent.target在老IE里是event.srcElement。这类题在今天已经不太考了因为IE已经不是主流但2015年的卷子里基本都会有一两道。第四个坑是闭包与循环的组合题。前面已经说过了就是那个输出五个5的setTimeout题。这个坑太经典了很多人笔试栽在这里面试也栽在这里。第五个坑是数组和字符串方法的混淆。比如[1, 2, 3].map(parseInt)的输出竟然是[1, NaN, NaN]。因为parseInt接收两个参数map传入的三个参数值被parseInt硬生生取走了两个第二项下标被当作进制数解析了。这种题就是考察你对API参数细节的理解是否深入。5.2 面试答题技巧从“能做对”到“能讲好”做对一套笔试题只是第一步能把做题思路清晰地讲出来才是校招面试翻盘的关键。我后来参与过一些面试评审发现很多候选人代码写得没问题但讲题的时候逻辑混乱白白丢失了印象分。我的建议是准备笔试答案时就要按“三步法”来梳理思路第一步先说结论是什么运行结果或者设计思路是什么。不要上来就分析过程先给出一个锚点让面试官知道你目标明确。第二步说为什么是这个结论。这里要拆解原理比如涉及作用域就说作用域链涉及事件就说事件流向。简洁有力不要绕弯子。第三步说可以怎么改。给出优化方案或替代方案顺便提一下不同方案的优缺点和适用场景。以this指向的题为例先说这题的输出是obj、window、call、undefined然后解释四种调用方式分别触发了哪种this绑定规则最后补充在实际开发中如何通过箭头函数或bind来规避this指向问题。这样一套回答下来既显示了知识的广度又显示了应用的深度。另外特别想提醒的一点是写代码的时候即便时间紧张也要保持变量命名清晰、代码缩进规范。很多面试官会因为代码风格给加分或减分。校招笔试的编程题本来就简单代码规范程度反而成了区分度来源。我在刷这套题的时候有一个习惯每道编程题写完之后自己以“面试官视角”再读一遍看有没有多余的变量、有没有可读性不高的地方。这个习惯对我后来在项目里写代码也有很大帮助。5.3 多年后回头看这套题带给了我什么说实话这套2015年的练习题里有几道题在今天已经失去了“时效性”。比如IE兼容相关的内容现在基本没有产品需要兼容IE8了比如$.ajax的手写封装现在大家直接用fetch或者axios。但核心的那部分——闭包、this、原型链、事件机制、浏览器渲染——今天依然是前端面试的重中之重只是题目包装方式变了环境从浏览器扩展到了Node.js、小程序甚至服务端。我个人的体会是这套题真正有价值的地方不在于题目本身而在于它逼着你把JavaScript语言的核心机制从头到尾梳理了一遍。当年我花了一个多星期逐题分析、逐题写总结做完之后再看框架源码很多原本看不懂的地方突然就通了。后来我面试别人的时候也喜欢用类似的原题去摸候选人的功底因为语言基础扎实的人学任何框架都只是时间问题语言基础不扎实的人写再多业务代码也只是在堆页面而已。如果你现在准备前端面试我的建议是把这套经典题找出来做一遍即使你用的是React/Vue即使你已经会用各种构建工具——做题的时候不要查资料坚持把每一道选择题的解释写下来把每一道编程题至少想出三种解法。做完之后你会发现那些在框架层反复出现的设计思路底层逻辑全都来源于这些基础机制。最后再分享一个小技巧刷题的时候不要只看正确答案一定要把错误选项为什么错也搞清楚。我当年准备这套题时最受益的一个习惯就是把每个错误选项都单独写一段说明讲清楚它错在哪里、正确的思路应该是什么。这个习惯让我在面试中遇到变体题时能快速识别出出题人设置的陷阱而不是只机械地死记答案。希望这个方法也能帮到你。
返回列表