一道腾讯笔试题想到的JS中的一些特性

昨天参加了腾讯2015实习生招聘的线下笔试,之前在他们提供的在线模拟笔试中已经被鄙视了,我真的不会C++,数据结构和算法方面也一点没准备。这次笔试就在我们学校里,就想着去锻炼下,无所谓结果。C++的比重还是不少,多了一些数学方面的题目,我只是想投web前端的,可是看来看去只有一道javascript的题目,也没有web方向的选做题。

题目是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = 0;

function one(){
for(var i=0; i<10; i++){
setTimeout(function(){
a += i;
}, 0);
}
}

function two(){
setTimeout(function(){
alert(a);
}, 0);
}

one();
two();

问:当弹出alert框时里面的值是多少?

一看又是循环引用问题,可以看看我前面两篇写闭包的文章

这题如果只有这一个point的话,我也不高兴再写了,因为还有一个我觉得比较重要的语言特性。

JS是单线程的

说“JS是单线程”可能有点不确切,应该说javascript引擎是单线程的。

浏览器内核中至少有三个常驻线程:javascript引擎线程,界面渲染线程,浏览器事件触发线程。除此以外,也有一些执行完就终止的线程,如Http请求线程,这些异步线程都会产生不同的异步事件。

浏览器是多线程的,但JS引擎是单线程的。我们来看个图

  • JS引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行js程序。

  • 在JS引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被“冻结”了。

setTimeout与setInterval

如上图所示,当我们把一个function放到setTimeout中后,该function相当于被加到了图上的Timer队列中。同时JS引擎中还有一个定时触发器,当setTimeout所设的延迟时间到了后,它会去唤醒Timer队列中相应的function。但是定时触发是有前提的,就是JS引擎处于空闲的时候才会去触发

1
2
3
4
5
6
7
setTimeout(function(){
alert('hello');
}, 1000);

for(var i=0; i<100000; i++){
// 开销大的计算
}

举个例子,先往Timer中放入了函数,指定它1秒后执行。但是随后JS引擎要执行一段超大循环的复杂计算,这个过程需要10秒才能全部计算完。如此的话,那个定时触发的函数也需要在10秒后(等JS引擎空闲下来后)才会被触发。

而setInterval设置间隔触发的时间,会在JS引擎中反复放该函数块,相当于“时间片”,JS引擎会间隔地去触发这些函数片。但是呢,这个间隔时间也是不一定的,它们被触发的前提都是需要JS引擎在空闲的时候。

1
2
3
4
5
6
setInterval(function(){
for(var i=0; i<100000; i++){
// 开销大的计算
}
alert('hello');
}, 1000);

这个例子跟上面类似,setInterval中设置的函数是一个时间开销很大的过程,如果设置完后JS引擎就空闲了,那么第一次触发的时候会在1秒后。但是第一次触发执行时,需要花费10秒才能执行完,那么该函数执行完后紧接着会继续被触发(因为已经超过了它当初被设置的1秒的间隔)。这样相当于该函数被连续执行并且彼此之间没有时间间隔。

小结下

setTimeout与setInterval设置的时间并不一定就是实际情况下的触发时间,要根据它们被定义完后剩下的JS脚本的执行时间,以及它们被定义的内部函数的执行时间。

需要注意的是,不能在setInterval中放入时间开销巨大的过程,这样会让JS引擎一直繁忙,页面上的事件回调也就无法被触发了,因为它们都在单线程的JS引擎中

如果实际应用中真的有时间开销很大的JS脚本,会导致浏览器UI无法响应用户的任何操作(因为JS执行时UI线程是被挂起的)。应该尽量避免这种情况,可以将JS任务拆分成多个小任务,把每个小任务都放到setTimeout中并设置一点小间隔时间。虽然这样会导致完成整个任务需要花费的时间更长,但是这样能避免JS引擎始终繁忙,给它留有一点空闲时间去响应用户的操作和事件回调,这样用户体验会更好。

最近的浏览器为长期运行的脚本提供了另外一个解决方案:Web Workers。它为浏览器提供了背景线程支持,可以将任务比较繁重的计算放在单独一个文件中,从主程序(网页)中调用该文件。详细内容超出了本文的范围,具体可以看官方文档

setTimeout(0)并不是立即执行

看了上面,setTimeout(function(){}, 0)仍然会把function加到JS引擎的Timer中。设置timeout时间为1毫秒或者0毫秒,实际上与浏览器和操作系统有关。0毫秒不意味着没有timeout,而是指尽可能快的处理。例如在IE中,最快的时钟周期是15毫秒。

综合分析题目

有了上面的理论基础,这道题就迎刃而解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = 0;

function one(){
for(var i=0; i<10; i++){
setTimeout(function(){
a += i;
}, 0);
}
}

function two(){
setTimeout(function(){
alert(a);
}, 0);
}

one();
two();

首先one()被执行时,它里面有个for循环10次,往Timer中添加了10个function(){ a += i; }这样的函数。然后two()被执行时,它往Timer中添加了function(){ alert(a); }这个函数。现在Timer队列中有11个函数。

由于设置的timeout为0毫秒,等待了一个时钟周期后(时间可以忽略不计),就开始依次触发Timer队列中的函数。

由于one()中的for循环执行完后,变量i的终值是10,所以Timer中的function(){ a += i; }执行时看到的i值就是10。这里不清楚原因的话,可以看这篇使用闭包解决循环引用问题

所以Timer中前10个函数执行下来后,全局变量a的值就是100。然后执行第11个函数,alert出100。

总结

小小的一道程序结果题,也能涉及到很底层的语言机制。只有把语言机制理解透了,才能改进真正开发过程中的代码,而不是仅仅把题目做对。这篇中讲到的JS单线程和setTimeout机制,是我对以前看书和看别人文章的零碎笔记的二次整理。重新思考后,自己理解的印象更深了。

我平时使用印象笔记积累知识,包括自己的看书笔记和网页裁剪来的别人的文章。但是那么多不可能一下子都能记住,通过写博客重新整理和思考,以去解决实际中遇到的问题。最后谢谢大家的支持,我会努力学习和写博客!理解不对的地方,也欢迎留言一起讨论!