ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

2016阿里前端笔试题复盘:JS基础与手写实现核心考点解析

2026/8/29 21:13:04 拓冰建站 浏览量
2016阿里前端笔试题复盘:JS基础与手写实现核心考点解析 1. 从2016年阿里面试题看前端面试风向2016年的前端圈和现在完全是两个世界。那时Vue刚出2.0不久AngularJS还在不少老项目里撑着React的生态还没像今天这样一统江湖webpack刚从1.x往2.x爬坡jQuery依然稳坐中小网站的第一把交椅。这个背景下阿里巴巴的前端笔试题目透露出的信号其实很有意思它不考框架API不考工具链配置翻来覆去就是在基础JavaScript、浏览器原理、编码能力和工程理解上做文章。我当年刷这份卷子的时候感触特别深的一点是阿里的笔试题不会让你“背答案”。它考的是你在写代码时脑子里究竟装了多少层东西——比如一个看似简单的闭包题背后可以延伸到内存回收、变量生命周期、模块化设计一道事件循环的输出顺序题能牵出宏任务微任务在浏览器里的整套调度机制。这种考察方式放到今天不仅不过时反而成了大厂前端面试的主流风格。这篇文章我会把这份卷子涉及的核心考点逐一拆开结合当时的出题背景和现在的面试趋势给出能直接用的解题思路、代码实现和避坑经验。不管你是准备校招的应届生还是想要检验自己基础功底的社招选手这套复盘应该都能帮你理出一条相对清晰的复习路径。1.1 这份卷子考了哪些核心方向先说结论2016年阿里前端笔试题的构成大致是四个模块JS语言基础、浏览器与HTTP、代码输出与手写实现、开放设计题。语言基础部分集中围绕变量的作用域、闭包、this指向、原型链展开浏览器部分重点关注事件机制、DOM操作性能和兼容性处理编码题则普遍是防抖节流、数组去重、深拷贝、函数柯里化这类能考察基本功底的题目最后还会有一道综合设计题让候选人去描述一个大体量前端项目的架构方案或一个复杂交互的实现思路。这套结构其实非常聪明。它没法靠临考突击蒙混过关因为每一个知识点都可以往深处无限追问。举个例子同样是考“数组去重”如果只答一个Array.from(new Set(arr))在2016年面试官脸上是不会看到任何波澜的——他可能紧接着就问Set的底层数据结构是什么、NaN能不能被去重、对象类型去重会有什么坑、数据量一上去性能怎么退化。一层问一层基础扎不扎实立刻就显现出来了。1.2 为什么今天还要看六年前的真题有人可能会觉得2016年的技术题放到2025年早就过时了。这个想法我理解但实际情况恰恰相反。前端框架迭代再快JavaScript这门语言的核心模型——原型链、闭包、事件循环、异步机制——几乎没有变过。这些底层机制才是面试里真正拉开差距的地方框架API考的是“你会不会用”底层原理考的是“你写出来的代码能不能撑住复杂度”。而且从这几年各大厂的笔试出题走向来看基础能力的考察权重一直都在。Vue、React这些框架本身就在不断往“更少的框架味、更多的原生JS能力”方向演化比如Vue3的Composition API本质上就是函数式组合能力的体现React Hooks也是一样。说白了框架吃的是JS基本功这碗饭基本功好的人学任何框架都快这也是为什么阿里这种体量的公司在笔试环节宁可多考基础也不急着考框架。2. 核心题型拆解语言基础与代码输出笔试中占比最大、也最能拉开分数差距的就是JS语言基础题和代码输出题。这两种题型看起来都在考“会不会写代码”但本质上考的是“脑子里对这门语言有没有正确的运行模型”。很多同学刷题时靠记忆答案遇到稍微变形一点的题就翻车根源就在于对底层运行机制的理解还停留在表面。2.1 作用域与提升机制模拟运行比记住结论更重要关于变量提升和函数提升2016年的笔试题里几乎必考。我当时遇到的一道典型题是这样var a 1; function foo() { console.log(a); var a 2; console.log(a); } foo();第一反应是什么我第一次做的时候脱口而出“1和2”但正确结果是undefined和2。原因在于函数作用域内部的var a声明会被提升到函数顶部但是赋值语句留在原地。所以函数实际执行时是这样function foo() { var a; console.log(a); // undefined a 2; console.log(a); // 2 }这里有个很重要的理解方式不要背“提升了”这个结论而是要在脑子里建立一种模拟运行的直觉。当我看到一段代码时我会先在脑内把声明语句和赋值语句拆开把声明“拎”到作用域顶部再按顺序执行剩下的逻辑。这套模拟运行的能力一旦养成遇到复杂嵌套作用域的题目就会从容很多因为本质上你已经在扮演JavaScript引擎的角色了。另一个高频考点是var、let、const在循环中的差异。经典题目是for (var i 0; i 3; i) { setTimeout(function() { console.log(i); }, 0); }这个输出是3, 3, 3相信很多人已经烂熟于心。但2016年的题目出得更刁钻一点它会把var i改成let i然后问输出是什么——答案是0, 1, 2。解释起来就是let在每次迭代中都会创建一个新的词法环境闭包捕获的是当次迭代的绑定而不是共享同一个全局变量。这个知识点到今天依然是面试高频因为var和let的差异直接关系到写异步代码时的心智模型。2.2 this指向的四种规则与常见陷阱this指向问题是2016年阿里笔试的“老朋友”也是让无数考生栽跟头的重灾区。四个基本规则先记牢默认绑定指向全局对象或undefined、隐式绑定指向调用对象、显式绑定通过call/apply/bind修改指向、new绑定创建新对象。但让我印象最深的一道题是把隐式绑定和赋值操作揉在一起var name window; var obj { name: obj, getName: function() { return function() { return this.name; }; } }; console.log(obj.getName()());这个输出是window。原因是obj.getName()执行后返回了一个普通函数这个函数在调用时没有任何前缀对象所以this回退到默认绑定。解法也很直白在外层方法中用一个变量缓存this或者在返回函数处使用箭头函数。这道题背后想考察的是你对“调用点决定this”这一本质的理解而不是记住哪条规则对应哪道题。其实this相关题目的核心就一句话函数在哪儿被调用决定了this指向谁而不是在哪儿被定义。当年面试官如果看到面试者在这个问题上能主动提到调用点的概念面试印象分会明显不一样。2.3 闭包除了能输出结果更要能说出用途闭包的考察不会只停留在“输出什么”一定会延伸到“能干什么”。我记得笔试里有一道手写题就是要求用闭包实现一个计数器function createCounter() { var count 0; return { increment: function() { count; return count; }, decrement: function() { count--; return count; }, get: function() { return count; } }; }这个实现有两个关键点。第一count变量只能通过返回的三个方法访问外部无论怎么折腾都碰不到它这就实现了信息的隐藏和封装。第二count的生命周期并没有随着createCounter执行结束而销毁而是被内部函数持续引用形成了一块“不被打扰的私有区域”。为什么闭包在2016年的面试中会被反复考因为当时前端模块化开发正处在从全局函数到模块化方案的转型期闭包几乎是最早、最轻量的模块化手段。当时很多老代码都是通过立即执行函数加闭包来模拟“私有变量”的比如jQuery的插件机制就是靠闭包实现内部状态隔离。理解闭包就理解了前端工程化演进的最早一环。2.4 原型链考点不在背诵而在对象查找路径原型链几乎是阿里面试的“题魂”级别知识点。笔试里出现过一道让我印象很深的题function Parent() { this.name parent; } Parent.prototype.getName function() { return this.name; }; function Child() { this.name child; } Child.prototype new Parent(); var child new Child(); console.log(child.getName()); child.hasOwnProperty(name); child.hasOwnProperty(getName);结果是child.getName()输出childchild.hasOwnProperty(name)为truechild.hasOwnProperty(getName)为false。原因不复杂实例自身有name属性getName来自原型链而非实例自身属性。真正答题时容易出错的地方是很多人看到Child.prototype new Parent()就以为child身上有Parent的实例属性了其实child自身只有Child构造函数中赋的nameParent实例上的name挂在Child.prototype上并不在child本身上。关于原型的理解我推荐一套很管用的类比方式每个对象都有原型原型是它的“备胎查找路径”当对象自身没有某个属性时就顺着原型链向上一层层找。就像是文件系统里的继承目录子目录里没有的文件去父目录里找。面试官追问到instanceof的机制、Object.create和new的区别、原型链的终点是哪里这些延伸点时只要把这个“逐层向上查找”的心智模型牢记在心就不容易被绕晕。3. 编码题手写实现背后的工程思维编码题是整份卷子最能反映候选人实际代码能力的地方。2016年阿里的笔试编码题有一个非常明显的特点题目本身不冷门但对你“写出来的代码是否考虑全面”有比较高的要求。裸写一个防抖函数可能人人都会可要考虑到this绑定、参数透传、立即执行开关、取消机制能写全的人就少了一大截。3.1 函数防抖与节流不只是背模板要理解使用场景先看防抖我用得最多的场景就是搜索框的输入联想。用户连续打字的时候不需要每次都发起请求等停下来一段时间后再发能大幅减少无效请求。一个完整可用的防抖函数大概长这样function debounce(func, wait, immediate) { var timeout; return function() { var context this; var args arguments; var callNow immediate !timeout; clearTimeout(timeout); timeout setTimeout(function() { timeout null; if (!immediate) { func.apply(context, args); } }, wait); if (callNow) { func.apply(context, args); } return callNow ? func.apply(context, args) : undefined; }; }这段代码里有几个细节容易被忽略。第一this的保存不可省略因为事件处理函数里this指向当前的DOM元素如果丢了this事件回调里的逻辑很可能出错。第二clearTimeout放在setTimeout之前确保每次调用都会重置之前的延时。第三immediate参数实现了“第一次点击立即执行后续等待wait毫秒”的效果适合按钮去重这类场景。再说节流。防抖是“停下来了才执行”节流是“固定时间间隔执行一次”。典型场景是页面滚动监听和拖拽事件用户滚动过程中需要持续触发计算但又不能每帧都执行所以节流就是给函数加一个“限速带”。function throttle(func, limit) { var inThrottle; return function() { var context this; var args arguments; if (!inThrottle) { func.apply(context, args); inThrottle true; setTimeout(function() { inThrottle false; }, limit); } }; }注意这个实现是标准的“时间戳版节流”首次会立即执行之后在limit毫秒内的调用被忽略。另一种是定时器版节流最后一次调用会在limit之后补执行。实际选哪种取决于业务想要“均匀响应”还是“兜底最后一下”。当年笔试如果能把两种实现都写出来并说明各自的适用场景绝对会是一个加分项。3.2 深拷贝手写实现中的边界条件大全2016年笔试里有一道很经典的手写深拷贝题。新手写出来的版本通常长这样function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }这个写法应付纯数据对象没问题但一旦遇到undefined、function、symbol、循环引用就会丢字段或者直接报错。更关键的是面试官的下一句话往往是“那不用JSON API你手写一个”。面试场景下的深拷贝实现至少要覆盖几个层次判断基础类型直接返回、判断数组和对象递归处理、处理循环引用、以及区分普通对象和特殊对象。这里给出一个既能应对面试、又能用于真实业务的版本function deepClone(obj, hash new WeakMap()) { if (obj null || typeof obj ! object) { return obj; } if (obj instanceof Date) { return new Date(obj); } if (obj instanceof RegExp) { return new RegExp(obj); } if (hash.has(obj)) { return hash.get(obj); } var cloneObj Array.isArray(obj) ? [] : {}; hash.set(obj, cloneObj); for (var key in obj) { if (obj.hasOwnProperty(key)) { if (typeof obj[key] object) { cloneObj[key] deepClone(obj[key], hash); } else { cloneObj[key] obj[key]; } } } return cloneObj; }我特别想强调循环引用的处理。JSON方案对于循环引用直接抛出TypeError: Converting circular structure to JSON手写时如果不处理也会导致无限递归栈溢出。用WeakMap记录已经克隆过的对象再次遇到时直接返回缓存既解决了循环引用问题也顺便优化了重复引用的性能。3.3 数组去重与扁平化多思路对比才是加分项数组去重在2016年的笔试题里出现的频率非常高我复盘过同一个题目的几种解法从最原始的双层循环到Set写法每一层都代表一种思维方式的变化。// 方法一双层循环 function unique1(arr) { var result []; for (var i 0; i arr.length; i) { for (var j 0; j result.length; j) { if (result[j] arr[i]) { break; } } if (j result.length) { result.push(arr[i]); } } return result; } // 方法二indexOf去重 function unique2(arr) { return arr.filter(function(item, index) { return arr.indexOf(item) index; }); } // 方法三ES6 Set去重 function unique3(arr) { return Array.from(new Set(arr)); }三种写法的差异不只是语法糖的进化背后是复杂度从O(n²)到O(n)的质变。Set基于哈希表查找O(1)空间换时间。2016年面试时如果能把复杂度分析讲透就算不是最优解面试官也会觉得你有算法思维。一个容易踩的坑是indexOf遇到NaN会失效因为NaN ! NaN而Set的SameValueZero比较规则能正确处理NaN。数组扁平化也类似。递归是基础做法reduce写法更简洁而flat(Infinity)虽然一行搞定但最好还是能解释清楚其内部逻辑。灵活调动多思路答题本质上是向面试官展示你对JavaScript数据结构工具的熟络程度。3.4 手写bindthis、闭包和new的重合考法手写bind是很多面试官的心头好因为它一个题就能同时考到this、闭包、原型和new的特性。完整的实现版本Function.prototype.myBind function(context) { if (typeof this ! function) { throw new TypeError(Error); } var fn this; var args Array.prototype.slice.call(arguments, 1); function bound() { var bindArgs Array.prototype.slice.call(arguments); // 如果被new调用this指向实例此时不应该绑定context return fn.apply(this instanceof bound ? this : context, args.concat(bindArgs)); } bound.prototype Object.create(fn.prototype); return bound; };这里有两个关键点。第一个是参数拼接bind可以在绑定时传一部分参数调用时再传另一部分这实际上就是柯里化。第二个关键点是new和bind的优先级问题如果通过new调用bind返回的函数this指向新创建的对象而不是bind时传入的context所以需要利用this instanceof bound做判断。绝大多数人写bind只写了前一半能补上new判断的人凤毛麟角而这恰恰是笔试里拉开差距的地方。4. 浏览器与HTTP笔试中的“看不见的题”2016年的前端笔试题目里浏览器和HTTP相关的知识点通常不会单独出大题但会穿插在小问里而且往往是区分“会用浏览器”和“理解浏览器”的关键。4.1 事件机制从捕获冒泡到事件委托那年的卷子里有一道关于事件处理的题给定一个嵌套结构的DOM点击最内层元素输出事件传播的顺序。这个题考察的就是事件处理的三个阶段捕获阶段从window向下到目标元素目标阶段冒泡阶段从目标元素向上到window。兼容性处理是2016年笔试绕不开的重点。addEventListener在IE8及以下不支持需要用attachEvent。老笔试题还喜欢专门考这个兼容写法不过现在浏览器市场已经非常统一这套兼容代码的实用性已经下降但是事件委托的思想依然活跃。比如一个列表有上千个li与其给每个li绑定事件不如在ul上监听一次利用冒泡机制在事件对象中拿到真正的目标元素。这样既减少内存占用又方便动态增删节点。4.2 HTTP缓存机制从笔试到生产都绕不开2016年考HTTP缓存主要集中在Cache-Control和Expires的区别、Last-Modified和ETag的配合使用上。我当时记过一个比较容易理解的类比缓存就像图书馆的借书规则Cache-Control是“这本书可以借多久”Last-Modified是“这本书上次更新是什么时候”如果别人的书比我手里的新就换一本。ETag则是更精确的版本号只要内容变了版本号就变。生产环境里缓存策略的核心思路是给静态资源文件名打上hash文件没变则强制使用本地缓存文件变了则通过新的URL拉取新内容。这个思路在2016年已经很成熟到今天依然是各大前端工程化方案默认开启的能力。笔试中如果能把缓存策略分成“强缓存”和“协商缓存”两层来答再配合实际项目中的更新机制举例远比只背几个响应头字段要打动人。4.3 从URL输入到页面渲染必须能讲清楚整条链路这类题目虽然不是阿里的专利但在笔试题中出现的频率极高。完整回答的路径应该是输入URL后先解析DNS、建立TCP连接、发送HTTP请求、接收响应内容然后浏览器开始解析HTML构建DOM树同时解析CSS构建CSSOM树两者结合形成渲染树再经过布局和绘制两大步骤最终呈现页面。如果脚本是同步加载的解析HTML的过程中遇到script标签会阻塞渲染直到JS执行完毕。这个问题的信息量很大而且每一层都可以追问。比如说DNS解析的原理、TCP握手为什么是三次、回流和重绘的区别、script标签的defer和async差异。2016年笔试可能不太会追问到太深但面试环节百分之百会顺着这个题往下挖所以我在复习时会把每一条线都铺开看一遍而不是只背答案。5. 笔试之外的隐藏信息与应试策略做完一整份2016年阿里的前端笔试题我最大的感受是这份卷子并没有在一味追求偏题怪题而是在认认真真筛选“基础扎实、会写代码、有理解深度”的候选人。它给后来者的备考方向提供了几个明确信号。5.1 不要花太多时间背框架API要吃透语言核心2016年正是前端框架百花齐放的时期但阿里的笔试里几乎没有直接考Vue或者React的题。这不是因为阿里内部不用框架而是因为大厂默认你能熟练使用框架是“基本盘”真正要筛选的差距在语言和原理层面。今天依然如此面试中一个能从头讲清楚this绑定规则、闭包作用域链、事件循环的候选人远比一个把Vue源码API背得滚瓜烂熟的人更吃香。我的建议是复习时把精力按这个比例分配JS语言基础占50%浏览器和网络占20%编码手写题占20%框架相关占10%。这套比例放到现在依然适用只不过浏览器和网络部分可以加上性能优化和前端安全等新考点。5.2 手写实现题不要背答案要理解每行代码编码题是笔试里最容易通过刻意练习拿到分数的部分但也是最容易“背漏”的部分。比如防抖函数背模板的人能写出来却可能在面试官问“immediate参数是什么意思”时卡住。我的训练方法很简单每道手写题至少自己想三个应用场景然后从场景反推参数设计。比如防抖的“取消机制”就是从“用户点了提交但想取消”这个场景里想到的。还有深拷贝的循环引用问题如果没处理过复杂数据的项目经验很难凭空想到。所以我建议平时在项目里多留意这些边界场景笔试不只是考知识量更考你平时思考问题的时候有没有把边界条件装进脑子里。5.3 学会用“结构化表达”展示知识深度笔试虽然不需要口述但答题的文字组织也是一种表达能力。描述一个知识点时我习惯的框架是是什么、为什么重要、怎么用、有什么坑。比如提到闭包时先说明闭包是函数与其词法作用域的组合再谈它在封装私有变量和模块化中的作用然后给出计数器或单例的实现最后指出闭包导致的内存引用问题。这套框架在回答开放设计题时格外好用。2016年笔试里的开放题我记得大致是描述如何设计一个大型前端项目的前端架构我按模块划分、状态管理、构建方案、性能优化、团队协作五个维度来答虽然没有标准答案但思路清晰、结构完整至少能让阅卷人觉得你有系统化的工程认知。这种结构化表达的能力笔试和面试都吃香值得专门练习。最后再分享一个当年复习时的小技巧把一切能变成代码的考点都写一遍。原型链理解了就手写一个newthis指向理解了就手写一个call缓存策略理解了就自己搭一个静态资源服务器亲眼看响应头的变化。这套“以写促学”的方法比其他任何刷题方式都扎实因为代码是这个行业里最不会说谎的东西。