
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宽度设置为220pxpadding为20pxborder为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不为noneoverflow不为visiblehidden、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又可以指向任意对象。当时考得再细一点会结合setTimeoutsetTimeout(obj.getName, 1000);一秒后回调执行时this同样丢失了。解决办法是bind绑定或者在外面包一层箭头函数。2016年笔试题里箭头函数还是新东西但如果你能说出“箭头函数没有自己的this它会继承外层作用域的this”这题就能加分。理解this的关键是看调用时的点语法谁调用this就是谁单独调用函数this指向全局严格模式下是undefinednew调用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年几乎所有前端岗位的笔试里都会出现现在也还在考。我第一次答得特别散后来梳理出一个稳定的回答框架输入URL后浏览器先查缓存包括DNS缓存和HTTP缓存。DNS解析把域名换成IP地址涉及本地hosts、DNS服务器递归查询。建立TCP连接经过三次握手如果是HTTPS还需要TLS握手。浏览器发送HTTP请求服务端返回HTML和各类静态资源。HTML解析成DOM树CSS解析成CSSOM树两者合并成渲染树。浏览器进行布局和绘制。遇到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的Setfunction 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的人并不是背的题目多而是对底层机制有清晰、准确的模型。把老题翻出来重新理解一遍正是建立这种模型最便宜、最直接的方式。