2016前端笔试题复盘:JavaScript核心考点与面试陷阱解析
2026/8/29 21:11:30 网站建设 项目流程

2016年春天,我坐在学校的机房里,对着阿里的在线笔试页面,系统里一道接一道弹出的全是前端相关的题。时间过去快十年,很多当时报错和纠结的细节已经模糊,但那些题目本身,至今还经常出现在各家公司的前端面试题里。当时的前端面试题和现在最大的区别在于,没有脚手架、没有工程化框架的辅助,大部分概念题就是让你在白纸上写代码、推输出结果。这套笔试(二)里JavaScript部分格外多,因为第一卷已经考过了HTML/CSS和基础网络。今天翻出来聊聊这套题,不光是怀旧,更重要的是把藏在题后面的知识链路重新梳理一遍,对现在想补前端基础、准备面试的人来说,依然很有参考价值。

1. 回到2016:当时的前端环境决定了你会遇到什么题

1.1 技术生态与笔试命题逻辑

2016年的前端圈正处在一个微妙的分水岭上。jQuery还是绝大多数网站的标配,AngularJS 1.x在部分公司里跑着,React开始被讨论,Vue刚发布没多久,Webpack正在逐步替代Grunt/Gulp成为新的打包工具。ES6的let/const、箭头函数、Promise已经出现在规范里,但生产环境里大家习惯用Babel把代码转成ES5再上线。换句话说,笔试题目设计者必须兼顾“规范考新”和“写法考稳”两个维度。

当时的阿里笔试,基本不考某个具体框架的API,因为框架更新太快,考了也没多大意义。更常见的是从JavaScript语言本身抽题,再加上浏览器原理、CSS布局这些相对稳定的知识。这给后来者的启示是:2016年的前端面试题看起来老,但背后全是地基。盒子模型、作用域、闭包、事件循环、原型链,这些内容到了2026年依然会在面试题里高频出现。如果你现在刷题只盯框架和工具链,反而容易漏掉这些真正决定水平的知识点。

1.2 在线笔试的形式与答题氛围

在线笔试一般分成单选题、多选题、简答题和编程题,时间通常是90分钟到120分钟。这套(二)明显偏JavaScript:选择题里有大量的代码输出题,简答题会要求描述某个机制,编程题则是手写函数完成指定功能。机考环境不能切出去搜索,浏览器禁用了开发者工具,只能在给定的编辑器里写,非常接近实战裸考。这个环境下,平时能靠搜索引擎解决的问题全藏不住了,对基本功的要求格外高。

我印象最深的是选择题里经常出现“下面代码输出什么”的题型,一列代码抛出来,四个选项,看着简单,但稍微漏掉一个变量提升或者this绑定规则,就会选错。这也说明,笔试考核的不是你会不会用某个库,而是你能不能准确预测代码行为。这种能力直到今天都很重要,因为写代码时如果连自己手下的作用域和this都判断不准,出现线上bug时排查成本会非常高。

2. HTML/CSS基础题:盒模型、BFC与居中的那些坑

2.1 盒模型:标准盒与怪异盒的差距

2016年笔试里,盒模型几乎算是送分题,但也是丢分题。经典问法:“一个div宽度设置为220px,padding为20px,border为10px,在标准W3C盒模型下,内容区宽度是多少?元素实际渲染宽度是多少?”

标准盒模型下,width只代表内容区宽度,元素实际渲染宽度等于width加上左右padding和左右border,也就是220 + 202 + 102 = 280px。而IE怪异盒模型下,width包含content、padding和border,所以内容区宽度要反推:220 - 202 - 102 = 160px,实际渲染宽度就是220px。把这两个概念混在一起,后面做题基本都会错。

这个知识点到今天仍然重要,尤其在移动端适配和组件库开发中。建议在全局样式里统一设置:

*, *::before, *::after { box-sizing: border-box; }

这样所有元素都按“给定宽度就是最终宽度”的方式来处理,后续布局心智负担会小很多。当年如果能在答案里额外写出这句全局设置,通常算加分项,因为它体现了工程意识,而不只是背概念。

2.2 BFC:触发条件与使用场景

BFC(块级格式化上下文)在2016年笔试题里出现频率出奇地高。常见问法包括:“怎么清除浮动?”“两个相邻元素margin重叠怎么解决?”“父元素高度塌陷怎么处理?”这些问题本质上都指向BFC。

BFC的触发条件包括:float不为none,overflow不为visible(hidden、auto、scroll都可以),display为inline-block、table-cell、flex等,position为absolute或fixed。创建BFC之后,容器内部布局不会影响外部,浮动元素也会被计算进父容器高度。所以“给父元素加overflow: hidden”能清除浮动,原理不是隐藏了什么,而是让父元素形成独立的BFC,计算高度时把浮动子元素算进去。

如果这是一道简答题,我会建议别只写代码,而是把原理补一句:BFC是一块独立的渲染区域,内部元素的定位和浮动不会影响外部,外部也不会影响内部。当年我答“清除浮动”时写了两种方式:父元素加overflow: hidden,或者用伪元素clearfix。批改的人更希望看到的不是死记代码,而是对这套独立上下文模型的理解。现代项目里浮动布局已经很少用了,但BFC这套思路在理解flex、grid、margin重叠等问题时依然很关键。

2.3 垂直水平居中的多种方案

“如何让一个元素水平垂直居中”是前端笔试里的钉子户题目。2016年那个阶段,最稳妥的答案是绝对定位加负margin,因为对老IE兼容性最好:

.parent { position: relative; } .child { position: absolute; left: 50%; top: 50%; margin-left: -50px; margin-top: -25px; width: 100px; height: 50px; }

这个方案缺点很明显:必须提前知道元素宽高,不然负margin的值没法计算。后来很多候选人和面试官更习惯用绝对定位配合transform:

.child { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }

好处是不需要知道宽高,但当时需要考虑transform在低版本浏览器的兼容性。到2016年的时候flex已经能在很多现代浏览器里正常使用了,居中代码可以缩短成:

.parent { display: flex; justify-content: center; align-items: center; }

笔试里最好的答法是分场景说明:已知宽高用负margin,未知宽高用transform,父容器是flex布局时优先flex,兼容老IE时再用table-cell或inline-block方案。这样回答在评分时通常能拿满分,因为阅卷人知道你不是背了一个答案,而是真正理解每种方案的限制条件。

2.4 CSS权重计算的考点

这套卷子里有一道选择题是问样式优先级:“div p.special”的权重是多少?计算逻辑通常是:内联样式权重最高,然后是ID选择器,接着是类、属性和伪类选择器,最后是元素和伪元素。严格来说,CSS规范里并不是简单十进制相加,而是按位比较:先比较ID数量,再比较类数量,最后比较元素数量。

用通俗计数法理解,“div p.special”包含两个元素和一个类,权重对应关系是0个ID、1个类、2个元素。如果另一条规则是“#content .box”,那就是1个ID、1个类、0个元素。比较优先级时先看ID数量,后者多一个ID,所以后者胜出。如果ID数相同,再比较类数量;再相同,就比较元素数量。选择器权重完全一致时,后定义的样式覆盖前面定义的。

这里有一个容易踩的坑,就是普通项目里为了覆盖某个第三方组件样式,随手加!important。结果后面想再覆盖时只能靠另一个!important,层叠结构越来越乱。当年笔试中会有题问!important的影响,我当时写了“优先但破坏可维护性”,这个答案在阅卷时是能拿分的。真正合理的做法是提高选择器权重,或者调整源码顺序,而不是频繁动用!important。

3. JavaScript核心题:this、闭包与原型链的高频套路

3.1 作用域与变量提升

2016年的JavaScript选择题特别爱考一个奇怪现象:

var a = 1; function test() { console.log(a); var a = 2; } test();

很多人的第一反应是输出1,因为函数内部找不到a,应该去外层找。但实际答案不是1,也不是报错,而是undefined。原因是var声明会提升到函数作用域顶部,但赋值不会跟着提升,所以函数内部等价于:

function test() { var a; console.log(a); a = 2; }

只要函数作用域内声明了var a,这个变量就属于函数作用域,访问时不会再向外层查找。这个题目在当年算入门级,但错误率非常高。也正因为var的这类表现容易造成混乱,ES6才推出了let和const,加上块级作用域和暂时性死区,变量必须先声明后使用,很多隐性问题在编码阶段就能暴露。

笔试里还会进一步追问:函数声明和变量声明同时提升,谁优先?“console.log(foo); function foo() {} var foo = 1;”会输出什么?答案是函数本身,而不是undefined。因为函数声明的提升优先级高于变量声明。这类边角题虽然有些抠细节,但它提醒我们一件事:如果代码里依赖这类提升机制,可读性通常很差。正确方式是变量统一在作用域顶部声明,函数用有名函数或模块方式组织。

3.2 闭包的经典循环陷阱

闭包是2016年阿里前端笔试的常客。最经典的一道题是:“页面有5个li标签,用循环给每个li绑定click事件,点击第0个li时弹出0,点击第1个li时弹出1,以此类推。请写出代码。”

很多人的第一版代码是这样:

for (var i = 0; i < 5; i++) { document.getElementsByTagName('li')[i].onclick = function() { alert(i); }; }

结果点击任何li,弹出来的都是5。原因在于,click回调执行时循环已经走完,i已经变成5。函数内部引用的i和循环里的i是同一个外部变量,而不是每次循环复制的一份值。解决方式有好几种,当年标准答案是IIFE传入参数:

for (var i = 0; i < 5; i++) { (function(i) { document.getElementsByTagName('li')[i].onclick = function() { alert(i); }; })(i); }

也可以把事件绑定逻辑封装成函数工厂,或者把var改成let,因为let在每次循环里都会创建独立的变量绑定。这道题表面考闭包,实际考的是词法作用域和“回调执行时的变量状态”。闭包不是高深魔法,它只是函数记住并访问自身词法作用域的能力。只要想明白“执行时函数里引用的i是谁”,绝大多数闭包题都能解开。

3.3 this指向的四种场景

this指向是前端面试里的权重担当。当年常考的输出题如下:

var obj = { name: 'ali', getName: function() { console.log(this.name); } }; var fn = obj.getName; fn();

这里输出的是undefined,因为把方法从对象上拆下来调用后,函数内部this指向了全局对象,而全局对象上没有name属性。如果把调用改成obj.getName(),this就是obj,输出ali。如果再用call或者apply显式指定this,又可以指向任意对象。当时考得再细一点,会结合setTimeout:

setTimeout(obj.getName, 1000);

一秒后回调执行时,this同样丢失了。解决办法是bind绑定,或者在外面包一层箭头函数。2016年笔试题里箭头函数还是新东西,但如果你能说出“箭头函数没有自己的this,它会继承外层作用域的this”,这题就能加分。

理解this的关键是看调用时的点语法:谁调用,this就是谁;单独调用函数,this指向全局(严格模式下是undefined);new调用,this指向新创建的对象;箭头函数不绑定this,而是继承定义时外层的this。这里要注意,闭包和this不是一回事。闭包是靠词法作用域保存变量,this是靠调用方式确定,两者别混在一起。

3.4 原型链与继承

“实现一个继承”在2016年前端开发工程师笔试里出现概率极高。考法分两层:概念层和手写层。概念题会问new一个构造函数时发生了什么。答案是:创建一个新对象,把新对象的原型链指向构造函数的prototype,把构造函数里的this绑定到新对象上,如果构造函数没有显式返回其他对象,则返回这个新对象。

手写题往往要求用ES5实现继承。当时很多人的第一反应是:

Child.prototype = new Parent();

这确实能继承,但会产生一个副作用:父构造函数里的实例属性会出现在Child.prototype上,导致所有子类实例共享同一份数据。更稳妥的是寄生组合式写法:

Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child;

这种写法避免了调用父构造函数生成实例作为原型的问题,是当年面试评分里偏优的答案。到了2026年,class已经普及,但instanceof背后的原型链查找逻辑没有变化。考原型链时还常搭配一道题——手写instanceof,我把它放到后面编程题里详细展开。

4. 异步与浏览器机制:setTimeout、Promise和页面渲染

4.1 经典输出题:定时器与循环

异步部分的笔试题,我最怕那种完全靠背答案的题,因为真正考的是对事件循环的理解。2016年出现频率最高的题目之一是:

for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 0); }

答案是多少?不是0、1、2、3、4,而是输出了5次5。第一层考点和闭包那题类似:var没有块级作用域,setTimeout回调执行时,循环里的i已经变成5。第二层考点是setTimeout(fn, 0)并不会立即执行fn。事件循环机制决定了:当前调用栈里的所有同步代码全部跑完后,才会从任务队列里取出回调执行。所以整个循环先跑完,把5个回调函数依次排进任务队列,之后才开始执行这些回调,这时候去访问i,自然拿到的是循环结束后的5。

把var换成let后,每次循环会创建独立的变量绑定,输出0、1、2、3、4。这也是let在2016年面试题里的典型应用场景。如果你在笔试答案里补一句“let的块级绑定相当于在每次迭代中创建一个新的i”,阅卷人会认为你不只是换了个关键字,而是真懂背后的语义差异。

4.2 宏任务、微任务与Promise的执行顺序

2016年的时候Promise已经进入了笔试范围,但很多前端开发者对微任务概念还没有形成共识,所以当年考Promise顺序题特别容易跌跟头。典型的题目是:

setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); console.log('sync');

输出顺序是sync、promise、timeout。原因是同步代码永远先执行;Promise的then回调属于微任务,会在当前宏任务结束之后、下一个宏任务开始之前立即执行;setTimeout的回调属于宏任务,需要等事件循环从任务队列里取。也就是说,微任务队列比宏任务队列具有更高的优先级,每次执行完一个宏任务,都会先清空当前所有的微任务,再取下一个宏任务。

2016年笔试时不一定要求写出“微任务”这个专业术语,只要描述出“Promise回调比setTimeout先执行”就能拿大部分分。放到2026年,Promise、async/await已经全面普及,但这道题的底层逻辑没有改变,只是题目里可能换成await、MutationObserver、requestAnimationFrame等更复杂的组合。把宏任务、微任务的顺序吃透,面试里遇到异步题就不容易慌。

4.3 从URL输入到页面渲染

简答题里有一道高频题:“浏览器从输入URL到页面展示这个过程中发生了什么?”这道题在2016年几乎所有前端岗位的笔试里都会出现,现在也还在考。我第一次答得特别散,后来梳理出一个稳定的回答框架:

  1. 输入URL后,浏览器先查缓存,包括DNS缓存和HTTP缓存。
  2. DNS解析把域名换成IP地址,涉及本地hosts、DNS服务器递归查询。
  3. 建立TCP连接,经过三次握手;如果是HTTPS,还需要TLS握手。
  4. 浏览器发送HTTP请求,服务端返回HTML和各类静态资源。
  5. HTML解析成DOM树,CSS解析成CSSOM树,两者合并成渲染树。
  6. 浏览器进行布局和绘制。遇到script标签会阻塞解析,所以外部JS通常放到body底部,或者加async、defer。

这道题之所以高频,是因为它把网络、缓存、渲染、性能优化全串了起来。答题时最好按网络层、渲染层、优化层分层展开,让阅卷人一眼看出你的知识结构是完整的。如果只答到“DNS解析加下载页面”就停了,分数会少一截。后面如果再补一句“放在head里的同步script会阻塞首屏渲染,所以需要优化加载顺序”,就能体现出工程经验。

5. 编程题实战:数组去重、扁平化与事件委托的评分细节

5.1 数组去重的多种解法与性能

编程题一般不会太难,但会留足够多的坑。数组去重就是典型。题面通常是:“实现一个函数unique(arr),返回去重后的新数组,要求不修改原数组。”最基础的写法:

function unique(arr) { var result = []; for (var i = 0; i < arr.length; i++) { if (result.indexOf(arr[i]) === -1) { result.push(arr[i]); } } return result; }

逻辑没问题,但是对大数据量不友好,因为indexOf本身是O(n)的查找,整体复杂度到了O(n^2)。改进思路是用对象缓存已经出现过的值:

function unique(arr) { var result = []; var seen = {}; for (var i = 0; i < arr.length; i++) { var item = arr[i]; if (!seen[item]) { seen[item] = true; result.push(item); } } return result; }

这个版本也有坑:对象的键会被转成字符串,所以数字1和字符串'1'会被当成同一个值,数组里同时存在两个不同对象时也会被转成"[object Object]"而误判。更健壮的做法是用Map记录类型和值,或者直接用ES6的Set:

function unique(arr) { return Array.from(new Set(arr)); }

笔试时如果写出这个版本,要同时补充一句:它基于SameValueZero比较,所以NaN也能正确去重,但要注意兼容性,2016年时Set还比较新,需要确认运行环境是否支持。阅卷人看这类题,重点考察的是你有没有意识到边界条件和性能问题,而不是只看结果是否跑通。

5.2 数组扁平化:递归与栈

另一个高频手写题是数组扁平化。要求把[1, [2, [3, [4]], 5]]变成[1, 2, 3, 4, 5]。最简单的递归版本:

function flatten(arr) { var result = []; for (var i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])); } else { result.push(arr[i]); } } return result; }

这个版本用到了递归和Array.isArray。如果题目要求不能递归,可以用栈,下面这版可以保持原顺序:

function flatten(arr) { var stack = arr.slice(); var result = []; while (stack.length) { var item = stack.shift(); if (Array.isArray(item)) { stack = item.concat(stack); } else { result.push(item); } } return result; }

笔试里遇到这个题,我建议先问清楚输入是什么结构、是否支持嵌套任意层数、是否需要考虑稀疏数组。这些在真实开发里会影响实现方式。如果面试官没给条件,主动写出“如果要处理稀疏数组,可以用[].concat.apply([], arr)这类技巧”会更显专业。当然,ES2019已经提供了Array.prototype.flat,但在2016年这是不能直接用的,所以手写能力在当时确实是硬门槛。

5.3 事件委托与手写实现

事件委托在2016年前端开发里是优化性能的重要手段。笔试简答题会问“如何给未来动态添加的元素绑定事件?”标准答案是把监听器挂到父容器上,利用事件冒泡,在回调里判断target。手写版本大概是这样:

document.getElementById('list').addEventListener('click', function(e) { var target = e.target; if (target && target.nodeName.toLowerCase() === 'li') { console.log(target.textContent); } });

这里有一个隐藏考点:addEventListener的第三个参数。默认值是false,代表在冒泡阶段触发;设置为true则是在捕获阶段触发。事件流分为捕获阶段、目标阶段和冒泡阶段,大多数业务场景都用冒泡阶段,所以默认false够用。当年很多项目还要兼容IE8,需要提到attachEvent,因为IE8不支持addEventListener。如果能补一句“attachEvent只支持冒泡,不支持捕获,而且this指向window”,这就能证明你踩过兼容性的坑。

判断target时,除了检查nodeName,最好再判断target是否等于currentTarget,避免点击到容器本身时误触发,或者用closest方法查找匹配元素。这些细节虽然不影响基本功能,但能反映你的鲁棒性意识。笔试阅卷者看到这类边界处理,往往比看到正确输出更满意。

6. 老题复盘:哪些考点在2026年前端面试里还能用

6.1 考点里的“变”与“不变”

仔细对比2016年的前端笔试和现在前端面试题,差异主要体现在工具链和框架上。当年要手写jQuery风格的Ajax封装,现在会问fetch和axios拦截器;当年讨论如何用gulp加版本号,现在聊的是Vite和Webpack的优化策略。但那些关于语言本质、浏览器底层机制的部分几乎没变:闭包仍然在考,this仍然在考,事件循环仍然在考,原型链虽然被class掩盖了一部分,但面向对象思路的题目还在以其他形式出现。

这背后的原因很简单:框架可以换,语言的核心执行模型不会随便变,浏览器的工作方式也比框架稳定得多。所以这两年的题目里,经常会把ES6的class对象和原型链结合起来问,比如“class extends和原型链有什么关系”,如果不理解原型链,很难答到位。

6.2 刷老题的正确姿势与个人体会

我帮人看简历、做模拟面试这几年,发现很多候选人把2016年这套笔试当成题库直接背答案。我不建议这样做。更有效的做法是每道题先按自己的理解写一遍,不管对错,再查规范或者看代码执行细节,问清楚“为什么答案是那样”,最后把同类题横向归纳。比如做完this指向的题目后,顺手把call、apply、bind的差异、箭头函数里的this、事件回调里的this全部总结一遍,比刷30道孤立的选择题有用得多。

如果准备时间比较紧,优先把事件循环、作用域闭包、原型链和CSS布局这四块吃透,因为它们在各类前端面试题里占了七成以上。2016年的笔试题也好,2026年的面试题也罢,本质上都是在考察一个开发者对这几块底子掌握得够不够稳。我自己的体会是,那些能在项目里快速定位线上bug的人,并不是背的题目多,而是对底层机制有清晰、准确的模型。把老题翻出来重新理解一遍,正是建立这种模型最便宜、最直接的方式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询