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 相关文章推荐
Jquery Change与bind事件代码
Sep 29 Javascript
在JavaScript中typeof的用途介绍
Apr 11 Javascript
jQuery 全选/反选以及单击行改变背景色实例
Jul 02 Javascript
JQuery中$.ajax()方法参数详解及应用
Dec 12 Javascript
js实现的点击div区域外隐藏div区域
Jun 30 Javascript
JS Attribute属性操作详解
May 19 Javascript
JavaScript的String字符串对象常用操作总结
May 26 Javascript
js实现千分符和保留几位小数的简单实例
Aug 01 Javascript
JS中用try catch对代码运行的性能影响分析
Dec 26 Javascript
vue 1.x 交互实现仿百度下拉列表示例
Oct 21 Javascript
angular4笔记系列之内置指令小结
Nov 09 Javascript
微信小程序时间标签和时间范围的联动效果
Feb 15 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编码规范-php coding standard
2007/03/16 PHP
PHP中防止直接访问或查看或下载config.php文件的方法
2012/07/07 PHP
PHP实现的MongoDB数据库操作类分享
2014/05/12 PHP
php定时计划任务与fsockopen持续进程实例
2014/05/23 PHP
PHP批斗大会之缺失的异常详解
2019/07/09 PHP
thinkPHP+mysql+ajax实现的仿百度一下即时搜索效果详解
2019/07/15 PHP
php中get_object_vars()在数组的实例用法
2021/02/22 PHP
Juery解决tablesorter中文排序和字符范围的方法
2015/05/06 Javascript
jQuery EasyUI编辑DataGrid用combobox实现多级联动
2016/08/29 Javascript
JS当前页面登录注册框,固定DIV,底层阴影的实例代码
2016/09/29 Javascript
Javascript单例模式的介绍和实例
2016/10/08 Javascript
微信小程序 css使用技巧总结
2017/01/09 Javascript
原生JS实现的碰撞检测功能示例
2018/05/18 Javascript
微信小程序实现打卡日历功能
2020/09/21 Javascript
生产制造追溯系统之再说条码打印
2019/06/03 Javascript
详解vue-cli项目开发/生产环境代理实现跨域请求
2019/07/23 Javascript
layui文件上传控件带更改后数据传值的方法
2019/09/23 Javascript
JavaScript获取当前url路径过程解析
2019/12/27 Javascript
python2.7+selenium2实现淘宝滑块自动认证功能
2018/02/24 Python
Python多线程及其基本使用方法实例分析
2019/10/29 Python
python做接口测试的必要性
2019/11/20 Python
Python安装tar.gz格式文件方法详解
2020/01/19 Python
Python进程间通信multiprocess代码实例
2020/03/18 Python
使用python客户端访问impala的操作方式
2020/03/28 Python
python 自动识别并连接串口的实现
2021/01/19 Python
基于HTML5 Canvas的3D动态Chart图表的示例
2017/11/02 HTML / CSS
J2SDK1.5与J2SDK5.0有什么区别
2012/09/19 面试题
宝信软件JAVA工程师面试经历
2012/08/19 面试题
汽车技术服务与营销专业推荐信
2013/11/29 职场文书
工程管理专业个人求职信范文
2013/12/07 职场文书
英语教师岗位职责
2014/03/16 职场文书
离婚起诉书范本
2015/05/18 职场文书
2016年第十九届推普周活动总结
2016/04/06 职场文书
试了下Golang实现try catch的方法
2021/07/01 Golang
Windows Server 2022 超融合部署(图文教程)
2022/06/25 Servers
JavaScript实现简单的音乐播放器
2022/08/14 Javascript