ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

阿里巴巴2017秋招前端笔试题精讲:从闭包到算法的高频考点解析

阿里巴巴2017秋招前端笔试题精讲:从闭包到算法的高频考点解析 我做前端这些年每次有人让我推荐一套面试题我都会第一个想到阿里巴巴2017秋招前端笔试题。这套题不算难但胜在“稳”——它把JavaScript这门语言里最容易出错的角落几乎都扫了一遍。很多今天看起来花里胡哨的面试题其实都是当年这套题衍生出来的变种。它适合三类人正在准备校招的应届生、想转行前端的新人、以及在业务里泡了几年想回头补基础的同学。网上流传的“阿里2017秋招前端笔试”题目版本很多完整还原每一道原题并不现实我也没必要把每个选择题的选项抄一遍。我更想做的事情是把这套题的出题逻辑、高频考点和答题套路拆开讲清楚再挑几道最有代表性的题目从题目本身一路讲到面试官想听到的答案。这样你拿到的不是一份标准答案而是一种解题的思维方式。1. 这套题为什么值得反复做1.1 2017年秋招前端面试的行业背景2017年是前端行业一个非常特殊的节点。Vue 2.x刚发布一年多React 16也在那一年推出前端工程化开始从“会用webpack”变成“要懂webpack”。但有意思的是阿里巴巴那年的笔试并没有考太多框架题而是集中考原生JavaScript、浏览器原理和算法。这背后的逻辑其实很朴素笔试阶段刷掉的是基础不扎实的人框架可以入职后再学但语言和浏览器底层能力很难短期补起来。那年投递前端岗位的应届生普遍手里都会握着一两份Vue或React的项目经验。面试官不是不看项目而是项目很难在笔试里被验证。你写在简历上的“熟练掌握Vue”笔试没法直接测出来但一个闭包输出题、一个数组去重题就能把你的水平看得明明白白。所以这套题的定位从一开始就很明确它是过滤器不是加分项。1.2 整套题的模块分布与底层逻辑按流传的版本和后续面经来看这套题大致可以分成四块JavaScript语言核心、浏览器与网络、DOM与事件、算法与编程。选择题部分喜欢考this指向、闭包、作用域、原型链、继承、数组方法、类型转换这些细颗粒度的知识点简答题部分会要求手写事件委托、防抖节流、深拷贝、数组去重这类高频工具函数编程题部分则集中在排序、递归、二分查找、数组扁平化这些基础算法。这个结构放在今天看依然不过时。你可以对照一下现在的面经除了多出微前端、SSR、性能优化这些工程化话题核心语言的考点几乎平移。换句话说这套题考的不是“你会不会用某个框架”而是“你脱离框架之后还剩多少底子”。这一点恰恰是很多做了两三年业务但没系统补过基础的前端同学最心虚的地方。1.3 放在2026年回看它的难度是什么水平如果单看代码量这套题放到2026年只能算中等偏下。现在随便一个前端岗位的笔试都可能让你手写Promise.all、手写深拷贝、手写防抖节流加一个场景题。但难度不高不代表没有价值反而因为题目足够基础它更能暴露人的知识盲区。我见过不少同学刷题喜欢刷难题、偏题、新题觉得老题没意思。但实际面试里考倒人的往往不是难题而是最基础的概念被换个角度问出来。比如“数组去重有几种写法各自的复杂度是多少”这种题看起来简单能把原理讲透的人并不多。这套2017年的题就像一面镜子你做一遍就能照出自己对JavaScript底层理解到底到不到家。2. JavaScript语言核心题精讲2.1 数组去重从双重循环到Set的演进数组去重是前端笔试出现频率最高的一题没有之一。这题的好处在于它几乎没有上限——新手能写for循环嵌套高手能讲出Set的底层实现、NaN的比较规则、对象数组的key选择、时间复杂度和空间复杂度的取舍。面试官非常喜欢用它来分层能写出来的人至少会基础语法能写多种方案的人说明有积累能把方案演进和复杂度讲明白的人基本可以直接进入下一轮。先看最基础的双重循环版本function unique(arr) { var result []; for (var i 0; i arr.length; i) { var item arr[i]; if (result.indexOf(item) -1) { result.push(item); } } return result; }这个方案逻辑清晰但是时间复杂度是O(n²)一旦数组变大性能会很难看。稍微进阶一点可以用filter加indexOf简化function unique(arr) { return arr.filter(function (item, index) { return arr.indexOf(item) index; }); }这个写法很优雅但它有一个隐藏前提indexOf只能找到第一个匹配的位置。如果数组里有两个相同的对象它们的内容一样但引用不同indexOf匹配的是引用所以这个写法对基本类型没问题对引用类型就失灵了。再往上升级就是Set方案function unique(arr) { return Array.from(new Set(arr)); } // 或者 const unique (arr) [...new Set(arr)];Set内部使用的是SameValueZero算法它对NaN的处理比indexOf严谨得多NaN在Set里被视为同一个值。这里有个高频追问为什么indexOf去重会把NaN漏掉因为indexOf内部使用严格相等比较而NaN NaN的返回结果是false。这个细节一提出来面试官对你的印象分就会不一样。还有一个容易被忽略的坑用对象key做去重时数字1和字符串1会被当成同一个key因为对象的key会被隐式转成字符串。如果你遇到的是对象数组去重更稳妥的做法是用Mapkey可以选对象里的某个唯一字段function uniqueByKey(arr, key) { const map new Map(); return arr.filter(item { if (map.has(item[key])) return false; map.set(item[key], true); return true; }); }我建议你在准备这类题的时候不要只背最后一行Set的写法而是把所有演进过程都过一遍。面试官问“还有别的方案吗”的时候你能从双重循环一直聊到Map这就不是在背答案而是在展示你对这个问题的完整理解。2.2 闭包输出题for循环与setTimeout的相爱相杀这套笔试题里有一道几乎必定出现的经典题变体很多但内核完全一致for (var i 0; i 5; i) { setTimeout(function () { console.log(i); }, 1000); }问最终输出什么答案是 5 5 5 5 5。如果面试官把延迟时间改成0结果依然是 5 5 5 5 5因为setTimeout的回调是在当前同步代码执行完之后才进入任务队列的。var声明的i不存在块级作用域循环结束之后全局的i已经变成了5所有回调拿到的都是同一个i。这题常见的改法有三种。第一种是把var改成letfor (let i 0; i 5; i) { setTimeout(function () { console.log(i); }, 1000); }let会为每一轮循环创建一个独立的块级作用域回调拿到的i是当前轮的值。第二种是IIFE包裹for (var i 0; i 5; i) { (function (j) { setTimeout(function () { console.log(j); }, j * 1000); })(i); }把每一轮的i作为参数传进立即执行函数回调闭包捕获的是这个函数作用域里的j。第三种是用bind固定参数for (var i 0; i 5; i) { setTimeout(console.log.bind(console, i), i * 1000); }binds会创建一个新的函数把i作为预设参数代入同样能解决问题。这题的问法变化很多有时候会改成点击列表项弹窗显示序号有时候会改成Promise循环输出但本质都在考同一个概念闭包捕获的是变量本身而不是变量的值。你把这个机制讲清楚了不管它怎么变形都能接住。2.3 手写new与instanceof理解原型链的入口原型链是JavaScript笔试里的老朋友阿里的题也不例外。我印象里有一类题是这样的手写一个new操作符或者让你解释new一个构造函数时内部做了什么。这题考察的不是背诵能力而是你有没有真正理解构造函数、原型对象和实例三者之间的关系。new在执行的时候大概做了四件事创建了一个新对象把这个新对象的原型链指向构造函数的prototype把构造函数的this绑定到新对象上并执行如果构造函数返回了一个对象类型的结果就返回那个结果否则返回新对象。手写版本function _new(Constructor, ...args) { const obj Object.create(Constructor.prototype); const result Constructor.apply(obj, args); return (result ! null typeof result object) ? result : obj; }注意最后一步很多人会漏掉如果构造函数显式return了一个对象new的返回值是这个对象而不是我们创建的obj。如果return的是基本类型则忽略。这个细节我在面试别人时经常追问十个人里有四五个会卡住。既然聊到原型链instanceof也常被拉出来考function _instanceof(left, right) { let proto Object.getPrototypeOf(left); const prototype right.prototype; while (proto) { if (proto prototype) return true; proto Object.getPrototypeOf(proto); } return false; }核心逻辑就是沿着原型链往上走直到找到和right.prototype相等的对象。如果走到了头还没找到就返回false。这里有一个很容易忽略的边界Object.getPrototypeOf(null)会报错所以left为null时得提前处理。能把这种边界条件主动说出来比写对函数本身更加分。3. 浏览器、网络与事件机制考点3.1 事件委托1000个li背后的设计取舍场景题在阿里的笔试题里也占了不少比重。最典型的一道是页面上有一个ul里面有1000个li点击某个li要把它的索引打印出来你的方案是什么如果你直接给每个li绑定click事件面试官会紧接着问这1000个事件监听器会不会有性能问题动态添加的li怎么办正确的思路是事件委托。利用事件冒泡机制把监听器绑在ul上点击任意li时事件都会冒泡到ul然后通过e.target判断实际点击的是不是lidocument.getElementById(list).addEventListener(click, function (e) { const target e.target; if (target.tagName.toLowerCase() li) { console.log([].indexOf.call(target.parentNode.children, target)); } });更现代一点的写法可以用closestdocument.getElementById(list).addEventListener(click, function (e) { const li e.target.closest(li); if (li) { console.log([].indexOf.call(li.parentNode.children, li)); } });事件委托的核心优势有三个内存占用从1000个监听器变成1个动态新增的li不需要重新绑定逻辑集中在一起维护起来更方便。面试官大概率会追问target和currentTarget的区别你直接回答target是触发事件的元素currentTarget是绑定监听器的元素在事件冒泡过程中两者可能不一样。我建议你把这个场景题背熟因为它考察的不是某一条API而是你对浏览器事件机制整体有没有概念。从冒泡、捕获讲到事件委托再讲到阻止冒泡的场景这条问题链能串出不少知识点。3.2 浏览器缓存强缓存与协商缓存的完整链路浏览器缓存也是前端笔试里的常驻嘉宾。这类题一般会问强缓存和协商缓存的区别响应头里的Cache-Control和Expires有什么区别ETag和Last-Modified谁优先要回答好得先理清一次完整请求的链路。浏览器第一次请求资源时服务器在响应头里带上缓存策略。浏览器再次请求同一个资源时先检查本地缓存是否命中强缓存如果命中了强缓存直接用本地副本不发请求到服务器。如果强缓存过期了就带着协商缓存相关的请求头去服务器验证服务器返回304时继续用本地缓存返回200时则用新资源并更新缓存。这两个阶段的字段优先级很好记场景优先级具体字段强缓存Cache-Control优先于ExpiresCache-Control: max-age3600 / Expires: Wed, 21 Oct 2026 07:28:00 GMT协商缓存ETag优先于Last-ModifiedETag / If-None-MatchLast-Modified / If-Modified-SinceCache-Control使用相对时间Expires使用绝对时间。绝对时间受客户端本地时间影响如果用户改了系统时间缓存就乱套了所以现在实际项目中Cache-Control更常用。ETag是资源内容的哈希值Last-Modified是文件最后修改时间精确度上ETag更可靠因为文件在一秒内多次修改时Last-Modified可能检测不到变化。这道题值得你花时间把它变成一套完整的话术从强缓存讲到协商缓存再从缓存流程讲到刷新操作对缓存的影响。按一下F5、CtrlF5、地址栏回车三者的缓存策略都不一样。这些细节能讲透说明你是真的在日常开发里踩过缓存坑的人。3.3 URL参数解析与前端安全面试官偏爱的表单类题还有一类手写题题目很短实现一个函数把URL里的query string解析成对象。看起来简单但能写出完整可用的版本并不容易。function parseQuery(url) { const params {}; const queryString url.split(?)[1] || ; if (!queryString) return params; queryString.split().forEach(pair { if (!pair) return; const [key, value] pair.split(); if (key) { params[decodeURIComponent(key)] decodeURIComponent(value || ); } }); return params; }这里有几个细节值得注意。第一如果值里包含中文或特殊字符URL会做百分号编码所以要用decodeURIComponent解码。第二某些参数可能没有等号比如?foo这时候value是undefined要给兜底值。第三同一个key可能出现多次比如?tagatagb具体是覆盖还是转数组要根据业务决定。这道题还有一层隐藏考点是前端安全。如果解码之后的字符串直接用于DOM操作很容易引入XSS风险这里的最佳实践是永远不要用未经验证的用户输入拼接HTML。笔试不会让你真的写出攻击代码但面试官问一句“你解析出来的参数如果包含script标签怎么办”你要能接住。4. 算法与编程能力题4.1 数组扁平化与递归思想编程题里有一道很典型的数组扁平化给的输入可能是[1, [2, [3, [4]]]]要求输出[1, 2, 3, 4]。这题考的是递归思想也是很多同学第一次接触“自己调用自己”时的噩梦。递归版本function flatten(arr) { return arr.reduce((acc, cur) { if (Array.isArray(cur)) { return acc.concat(flatten(cur)); } return acc.concat(cur); }, []); }这个写法的思路是遍历数组的每一项如果是数组就递归展开否则直接放入结果。用reduce的好处是一行代码就能完成累加逻辑。如果不想用reduce也可以用while配合some和展开运算符function flatten(arr) { while (arr.some(item Array.isArray(item))) { arr [].concat(...arr); } return arr; }这种方案每循环一次就展开一层直到数组里不再存在数组元素。它只能处理一层一层的展开但胜在逻辑直观不熟悉递归的人也能理解。还有一道衍生题是控制扁平化的深度只展开到指定层数。面试官看到你能写基本版之后大概率会追加这一问。这时候递归函数需要多接收一个depth参数每进入一层就减一深度为0时直接原样返回。这类题练完你会发现自己对“递归的终止条件”理解得更深了因为数组扁平化的终止条件就是“当前元素不是数组”。4.2 二分查找边界条件决定生死二分查找也是阿里笔试编程题的常客。题目一般很直接给定一个有序数组和一个目标值返回目标值的下标不存在则返回-1。看起来简单但边界条件写错是常态尤其是当数组元素为偶数个时mid的取整方向和左右指针的移动逻辑会直接影响循环能否正常结束。function binarySearch(arr, target) { let left 0; let right arr.length - 1; while (left right) { const mid Math.floor((left right) / 2); if (arr[mid] target) return mid; if (arr[mid] target) { left mid 1; } else { right mid - 1; } } return -1; }这段代码使用的是左闭右闭区间所以循环条件是left rightmid被排除后left更新为mid 1right更新为mid - 1。如果你用左闭右开区间循环条件就变成left right边界处理也会不一样。面试的时候建议先明确自己用的是哪种区间写法再动笔否则容易在边界判断上自相矛盾。这道题还有一个进阶版本找出目标值在有序数组中的第一个位置或最后一个位置。比如[1, 2, 2, 2, 3]里找2的第一个下标和最后一个下标。此时找到目标值不能直接返回还要继续把区间往左侧或右侧收缩。能把这个扩展讲清楚说明你是真的理解了二分查找的循环不变量而不是背了个模板。4.3 全排列从回溯理解搜索树的剪枝阿里的编程题偶尔会上一些典型的回溯算法题全排列就是代表之一。题目是给定一个不包含重复数字的数组返回它的所有排列。function permute(nums) { const result []; const used new Array(nums.length).fill(false); const path []; function dfs() { if (path.length nums.length) { result.push([...path]); return; } for (let i 0; i nums.length; i) { if (used[i]) continue; used[i] true; path.push(nums[i]); dfs(); path.pop(); used[i] false; } } dfs(); return result; }这段代码的核心是“选择、递归、撤销选择”的回溯三步走。used数组用来标记哪些元素已经被选过path记录当前排列当path长度等于nums长度时就得到了一组完整排列。这里有一个细节result.push([...path])不能写成result.push(path)因为path后续会被pop修改直接push引用得到的会全是空数组。全排列的时间复杂度是O(n!)面试官可能会追问“能不能优化”此时如果你能提到重复元素的剪枝方案——先排序再跳过和前一个相等且前一个还没被使用的元素——就又多了一个亮点。这类题不一定要全部背下来但至少得知道回溯的基本框架因为笔试里很多排列、组合、子集问题都是同一个模板换皮。5. 笔试现场的战术与踩坑实录5.1 六个高频失分点每一个都是血泪教训我刷过不少前端笔试也帮人看过不少笔试答案总结下来最吃亏的往往不是不会做而是会做却拿不到分。这六个失分点几乎每次都有人踩。第一只写方案不讲思路。笔试系统里很多简答题不是只看代码的你可以在代码旁边用注释说明自己的思路。比如数组去重你写了Set一行方案再补一句“Set内部使用SameValueZero可以处理NaN”这道题的得分立马不一样。第二边界条件全部回避。笔试题里输入可能为空数组、null、undefined代码一上来就操作了length属性直接崩掉。老老实实先判空既安全又能展示严谨性。第三变量命名混乱。写出来的代码全是a、b、c、d面试官还要去猜你是什么意思。用arr、target、result这种语义化命名代码可读性高很多。第四不测试示例数据。代码写完不自己走一遍边界用例如空数组、单个元素、全是相同元素、含有NaN全都交给系统去发现。你多花两分钟自查就能避免低级错误。第五在死磕一道题上浪费太多时间。编程题往往有好几道遇到卡壳超过20分钟就果断跳过先把能拿的分拿到手。笔试的得分率往往比单题的完美度更重要。第六忽视代码风格。缩进混乱、缺分号、中英文混用这些不会直接判错但会大大影响阅读体验遇到人工阅卷时很吃亏。5.2 面试官到底想从手写题里看到什么很多同学以为手写题就是考察“代码能不能跑”这是误解。面试官想看的其实是三件事你的思路是否清晰、你的基础知识是否扎实、你的代码是否具备工程可读性。代码能不能在本地运行反而没那么重要因为笔试环境里很少真的执行代码更多是人工阅卷。所以答题时别一上来就埋头写代码先在脑子里过一遍思路然后按这个顺序组织答案先说思路再估一个复杂度再写代码写完用示例数据走一遍最后主动补充边界情况和可能的优化方向。这套答题节奏很多人不习惯但确实是最能体现水平的表达方式。“思路先行”还有一个好处即使最后代码写bug了面试官能看到你的思考过程也能给你一个基础分。反过来代码写得对但完全说不出为什么这么写面试官反而会怀疑你是背下来的。笔试是面试的敲门砖但面试官更希望招到一个能沟通、能拆解问题的同事而不是一个答题机器。5.3 把一套题变成一张知识网我的备战方法做这套题的时候我建议你准备一张A4纸把每一道题涉及的知识点写下来。数组去重不只是“去重”它关联到Set、Map、对象的隐式类型转换、严格相等、NaN比较规则、时间复杂度和空间复杂度。闭包题关联到作用域、执行上下文、事件循环、异步任务队列。写完之后你会发现一套题涉及的几十个知识点其实可以浓缩成五六个知识簇。我的习惯是分三轮来做。第一轮不限时能做多少做多少做完之后对答案把自己没掌握的知识点标记出来。第二轮限时一小时模拟真实笔试状态重点训练时间分配。第三轮不再逐题做而是把自己整理的A4纸拿出来对着每个知识点展开复述能讲清楚就算过关讲不清楚就回到对应的题目重新理解。另外提醒一句网上能找到的历年笔试题质量参差不齐尤其是流传很多年的题目经常被转载得面目全非甚至混入一些与题目完全无关的内容。做题时要学会筛选优先看那些有明确解析、有代码、有讨论的版本别在残缺和错误的题目上浪费太多时间。真题的价值在于帮自己找盲区而不是追求“押中原题”。最后再分享一个我自己备考这套题的小习惯。当年我拿到这套题之后没有直接做题而是先把题目里出现的所有名词写在纸上闭包、原型链、事件委托、缓存、二分查找……然后每天挑一个名词用自己的话讲一遍讲不出来的就是缺口。做完一套题重要的不是记住了多少标准答案而是你发现自己还有多少个说不清楚的概念。把那串名单上的空洞填满这套题才算真正做完。
返回列表