80%应聘者都不及格的JS面试题


Posted in Javascript onMarch 21, 2017

共 5024 字,读完需 6 分钟,速读需 2 分钟,本文首发于知乎专栏前端周刊。写在前面,笔者在做面试官这 2 年多的时间内,面试了数百个前端工程师,惊讶的发现,超过 80% 的候选人对下面这道题的回答情况连及格都达不到。这究竟是怎样神奇的一道面试题?他考察了候选人的哪些能力?对正在读本文的你有什么启示?且听我慢慢道来

不起眼的开始

招聘前端工程师,尤其是中高级前端工程师,扎实的 JS 基础绝对是必要条件,基础不扎实的工程师在面对前端开发中的各种问题时大概率会束手无策。在考察候选人 JS 基础的时候,我经常会提供下面这段代码,然后让候选人分析它实际运行的结果:

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

console.log(new Date, i);

这段代码很短,只有 7 行,我想,能读到这里的同学应该不需要我逐行解释这段代码在做什么吧。候选人面对这段代码时给出的结果也不尽相同,以下是典型的答案:

A. 20% 的人会快速扫描代码,然后给出结果:0,1,2,3,4,5;
B. 30% 的人会拿着代码逐行看,然后给出结果:5,0,1,2,3,4;
C. 50% 的人会拿着代码仔细琢磨,然后给出结果:5,5,5,5,5,5;

只要你对 JS 中同步和异步代码的区别、变量作用域、闭包等概念有正确的理解,就知道正确答案是 C,代码的实际输出是:

2017-03-18T00:43:45.873Z 5
2017-03-18T00:43:46.866Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5

接下来我会追问:如果我们约定,用箭头表示其前后的两次输出之间有 1 秒的时间间隔,而逗号表示其前后的两次输出之间的时间间隔可以忽略,代码实际运行的结果该如何描述?会有下面两种答案:

A. 60% 的人会描述为:5 -> 5 -> 5 -> 5 -> 5,即每个 5 之间都有 1 秒的时间间隔;
B. 40% 的人会描述为:5 -> 5,5,5,5,5,即第 1 个 5 直接输出,1 秒之后,输出 5 个 5;

这就要求候选人对 JS 中的定时器工作机制非常熟悉,循环执行过程中,几乎同时设置了 5 个定时器,一般情况下,这些定时器都会在 1 秒之后触发,而循环完的输出是立即执行的,显而易见,正确的描述是 B。

如果到这里算是及格的话,100 个人参加面试只有 20 人能及格,读到这里的同学可以仔细思考,你及格了么?

追问 1:闭包

如果这道题仅仅是考察候选人对 JS 异步代码、变量作用域的理解,局限性未免太大,接下来我会追问,如果期望代码的输出变成:5 -> 0,1,2,3,4,该怎么改造代码?熟悉闭包的同学很快能给出下面的解决办法:

for (var i = 0; i < 5; i++) {
 (function(j) { // j = i
 setTimeout(function() {
  console.log(new Date, j);
 }, 1000);
 })(i);
}

console.log(new Date, i);

巧妙的利用 IIFE(Immediately Invoked Function Expression:声明即执行的函数表达式)来解决闭包造成的问题,确实是不错的思路,但是初学者可能并不觉得这样的代码很好懂,至少笔者初入门的时候这里琢磨了一会儿才真正理解。

有没有更符合直觉的做法?答案是有,我们只需要对循环体稍做手脚,让负责输出的那段代码能拿到每次循环的 i 值即可。该怎么做呢?利用 JS 中基本类型(Primitive Type)的参数传递是按值传递(Pass by Value)的特征,不难改造出下面的代码:

var output = function (i) {
 setTimeout(function() {
 console.log(new Date, i);
 }, 1000);
};

for (var i = 0; i < 5; i++) {
 output(i); // 这里传过去的 i 值被复制了
}

console.log(new Date, i);

能给出上述 2 种解决方案的候选人可以认为对 JS 基础的理解和运用是不错的,可以各加 10 分。当然实际面试中还有候选人给出如下的代码:

for (let i = 0; i < 5; i++) {
 setTimeout(function() {
 console.log(new Date, i);
 }, 1000);
}

console.log(new Date, i);

细心的同学会发现,这里只有个非常细微的变动,即使用 ES6 块级作用域(Block Scope)中的 let 替代了 var,但是代码在实际运行时会报错,因为最后那个输出使用的 i 在其所在的作用域中并不存在,i 只存在于循环内部。

能想到 ES6 特性的同学虽然没有答对,但是展示了自己对 ES6 的了解,可以加 5 分,继续进行下面的追问。

追问 2:ES6

有经验的前端同学读到这里可能有些不耐烦了,扯了这么多,都是他知道的内容,先别着急,挑战的难度会继续增加。

接着上文继续追问:如果期望代码的输出变成 0 -> 1 -> 2 -> 3 -> 4 -> 5,并且要求原有的代码块中的循环和两处 console.log 不变,该怎么改造代码?新的需求可以精确的描述为:代码执行时,立即输出 0,之后每隔 1 秒依次输出 1,2,3,4,循环结束后在大概第 5 秒的时候输出 5(这里使用大概,是为了避免钻牛角尖的同学陷进去,因为 JS 中的定时器触发时机有可能是不确定的,具体可参见 How Javascript Timers Work)。

看到这里,部分同学会给出下面的可行解:

for (var i = 0; i < 5; i++) {
 (function(j) {
 setTimeout(function() {
  console.log(new Date, j);
 }, 1000 * j)); // 这里修改 0~4 的定时器时间
 })(i);
}

setTimeout(function() { // 这里增加定时器,超时设置为 5 秒
 console.log(new Date, i);
}, 1000 * i);

不得不承认,这种做法虽粗暴有效,但是不算是能额外加分的方案。如果把这次的需求抽象为:在系列异步操作完成(每次循环都产生了 1 个异步操作)之后,再做其他的事情,代码该怎么组织?聪明的你是不是想起了什么?对,就是 Promise。

可能有的同学会问,不就是在控制台输出几个数字么?至于这样杀鸡用牛刀?你要知道,面试官真正想考察的是候选人是否具备某种能力和素质,因为在现代的前端开发中,处理异步的代码随处可见,熟悉和掌握异步操作的流程控制是成为合格开发者的基本功。

顺着下来,不难给出基于 Promise 的解决方案(既然 Promise 是 ES6 中的新特性,我们的新代码使用 ES6 编写是不是会更好?如果你这么写了,大概率会让面试官心生好感):

const tasks = [];
for (var i = 0; i < 5; i++) { // 这里 i 的声明不能改成 let,如果要改该怎么做?
 ((j) => {
 tasks.push(new Promise((resolve) => {
  setTimeout(() => {
  console.log(new Date, j);
  resolve(); // 这里一定要 resolve,否则代码不会按预期 work
  }, 1000 * j); // 定时器的超时时间逐步增加
 }));
 })(i);
}

Promise.all(tasks).then(() => {
 setTimeout(() => {
 console.log(new Date, i);
 }, 1000); // 注意这里只需要把超时设置为 1 秒
});

相比而言,笔者更倾向于下面这样看起来更简洁的代码,要知道编程风格也是很多面试官重点考察的点,代码阅读时的颗粒度更小,模块化更好,无疑会是加分点。

const tasks = []; // 这里存放异步操作的 Promise
const output = (i) => new Promise((resolve) => {
 setTimeout(() => {
 console.log(new Date, i);
 resolve();
 }, 1000 * i);
});

// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
 tasks.push(output(i));
}

// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
 setTimeout(() => {
 console.log(new Date, i);
 }, 1000);
});

读到这里的同学,恭喜你,你下次面试遇到类似的问题,至少能拿到 80 分。

我们都知道使用 Promise 处理异步代码比回调机制让代码可读性更高,但是使用 Promise 的问题也很明显,即如果没有处理 Promise 的 reject,会导致错误被丢进黑洞,好在新版的 Chrome 和 Node 7.x 能对未处理的异常给出 Unhandled Rejection Warning,而排查这些错误还需要一些特别的技巧(浏览器、Node.js)。

追问 3:ES7

既然你都看到这里了,那就再坚持 2 分钟,接下来的内容会让你明白你的坚持是值得的。

多数面试官在决定聘用某个候选人之前还需要考察另外一项重要能力,即技术自驱力,直白的说就是候选人像有内部的马达在驱动他,用漂亮的方式解决工程领域的问题,不断的跟随业务和技术变得越来越牛逼,究竟什么是牛逼?建议阅读程序人生的这篇剖析。

回到正题,既然 Promise 已经被拿下,如何使用 ES7 中的 async await 特性来让这段代码变的更简洁?你是否能够根据自己目前掌握的知识给出答案?请在这里暂停 1 分钟,思考下。

下面是笔者给出的参考代码:

// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (timeountMS) => new Promise((resolve) => {
 setTimeout(resolve, timeountMS);
});

(async () => { // 声明即执行的 async 函数表达式
 for (var i = 0; i < 5; i++) {
 await sleep(1000);
 console.log(new Date, i);
 }

 await sleep(1000);
 console.log(new Date, i);
})();

总结

感谢你花时间读到这里,相信你收获的不仅仅是用 JS 精确控制代码输出的各种技巧,更是对于前端工程师的成长期许:扎实的语言基础、与时俱进的能力、强大技术自驱力。

更多面试题学习资料请关注专题《面试题大全》 。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
json简单介绍
Jun 10 Javascript
Jquery公告滚动+AJAX后台得到数据
Apr 14 Javascript
JS完成代码前最好对其做5件事
Apr 07 Javascript
jQuery插件multiScroll实现全屏鼠标滚动切换页面特效
Apr 12 Javascript
JS实现json的序列化和反序列化功能示例
Jun 13 Javascript
Node.JS更改Windows注册表Regedit的方法小结
Aug 18 Javascript
JavaScript代码执行的先后顺序问题
Oct 29 Javascript
详解Vue如何支持JSX语法
Nov 10 Javascript
浅谈vue自定义全局组件并通过全局方法 Vue.use() 使用该组件
Dec 07 Javascript
javascript中一些奇葩的日期换算方法总结
Nov 14 Javascript
js canvas实现二维码和图片合成的海报
Nov 19 Javascript
bootstrap下拉分页样式 带跳转页码
Dec 29 Javascript
使用jQuery监听扫码枪输入并禁止手动输入的实现方法(推荐)
Mar 21 #jQuery
JavaScript中的普通函数和箭头函数的区别和用法详解
Mar 21 #Javascript
JavaScript 中 apply 、call 的详解
Mar 21 #Javascript
图解Javascript——作用域、作用域链、闭包
Mar 21 #Javascript
Bootstrap警告框(Alert)插件使用方法
Mar 21 #Javascript
Bootstrap标签页(Tab)插件使用方法
Mar 21 #Javascript
JavaScript数组和对象的复制
Mar 21 #Javascript
You might like
php网页后退不再出现过期
2007/03/08 PHP
注册页面之前先验证用户名是否存在的php代码
2012/07/14 PHP
Thinkphp使用mongodb数据库实现多条件查询方法
2014/06/26 PHP
php 浮点数比较方法详解
2017/05/05 PHP
function, new function, new Function之间的区别
2007/03/08 Javascript
js 跨域和ajax 跨域问题小结
2009/07/01 Javascript
javascript 匿名函数的理解(透彻版)
2010/01/28 Javascript
JavaScript设计模式之装饰者模式介绍
2014/12/28 Javascript
javascript实现拖放效果
2015/12/16 Javascript
Angularjs material 实现搜索框功能
2016/03/08 Javascript
学习AngularJs:Directive指令用法(完整版)
2016/04/26 Javascript
详解基于node的前端项目编译时内存溢出问题
2017/08/01 Javascript
Webpack devServer中的 proxy 实现跨域的解决
2018/06/15 Javascript
浅谈KOA2 Restful方式路由初探
2019/03/14 Javascript
Vue Echarts实现可视化世界地图代码实例
2019/05/07 Javascript
Vue.js中的extend绑定节点并显示的方法
2019/06/20 Javascript
JS实现使用POST方式发送请求
2019/08/30 Javascript
使用VScode 插件debugger for chrome 调试react源码的方法
2019/09/13 Javascript
Javascript如何实现双指控制图片功能
2020/02/25 Javascript
Vue-cli 移动端布局和动画使用详解
2020/08/10 Javascript
python 数据加密代码
2008/12/24 Python
python妙用之编码的转换详解
2017/04/21 Python
python内存动态分配过程详解
2019/07/15 Python
Python使用ffmpy将amr格式的音频转化为mp3格式的例子
2019/08/08 Python
为什么黑客都用python(123个黑客必备的Python工具)
2020/01/31 Python
Python 2种方法求某个范围内的所有素数(质数)
2020/01/31 Python
用pushplus+python监控亚马逊到货动态推送微信
2021/01/29 Python
Bitiba意大利:在线宠物商店
2020/10/31 全球购物
EMPHASIS艾斐诗官网:周生生旗下原创精品珠宝品牌
2020/12/17 全球购物
Java如何格式化日期
2012/08/07 面试题
采购部主管岗位职责
2014/01/01 职场文书
社区学习党的群众路线教育实践活动心得体会
2014/11/03 职场文书
体育教师个人总结
2015/02/09 职场文书
欠款纠纷起诉状
2015/05/19 职场文书
单位证明范文
2015/06/18 职场文书
英语读书笔记
2015/07/02 职场文书