【面试题】由 alert 引发的思考

这是我最近碰到的一道面试题,其实是很简单的题目,但不知道怎么回事,脑子一抽,居然 YY 出了 alert 有优先执行的特性。其实很好反应过来,以前大家最喜欢用 alert 调试代码嘛,如果 alert 会优先执行,那还怎么调试代码。。。

window.alert

这个 BOM 方法是浏览器提供的对话窗口方法,能显示一个警告对话框,上面显示有指定的文本内容以及一个"确定"按钮。

用法:

window.alert(message);

其中 message 为字符串,如果是非字符串参数,怎会自动转换成字符串类型。

这里显示的对话框是一个模态窗口,它能阻止用户对浏览器窗口界面的其他部位进行操作,所以不应该过多的使用这种模态窗口。

线程阻塞

alert 的最大一个特性就是线程阻塞,它能阻止它后面代码的执行,只有点了确定按钮,后续代码才能继续执行。

例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>alert 线程阻塞</title>
</head>
<body>
    <script>
        var audio = new Audio();
        audio.src = "http://m2.music.126.net/wsOaTdfg_N0nIxw9GCJ8Zg==/18724683022784324.mp3";
        audio.play();
        alert("播放被阻塞了么?");
    </script>
</body>
</html>

上面代码的执行结果能猜到么?很简单,音乐播放会被阻塞掉,只有点了确定按钮之后,音乐才会继续播放,因为浏览器环境中 JS 是单线程,一旦出现 alert,后续代码就无法执行了。

很多聪明的童鞋肯定会想到另一个方法 setTimeout,通过 setTimeout 来“异步”执行播放操作,结果又会怎样呢?

代码:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>alert 线程阻塞</title>
</head>
<body>
	<script>
		setTimeout(function(){
			var audio = new Audio();
			audio.src = "http://m2.music.126.net/wsOaTdfg_N0nIxw9GCJ8Zg==/18724683022784324.mp3";
			audio.play();
		});
		alert("播放被阻塞");
	</script>
</body>
</html>

结果应该和一些童鞋的预料不一样,音乐播放仍然被阻塞了,这又是为什么呢?其实在浏览器中,setTimeout 方法并不是一个真正的异步方法,调用这个函数并不会产生另一个线程,只是把要执行的函数推入队列,然后浏览器通过不断读取来执行代码,也就是说,代码的执行仍然在主线程中执行的,也就是 alert 仍然可以阻塞代码。

那我们换一种方式,对 alert 使用 setTimeout 延迟执行。

代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>alert 线程阻塞</title>
</head>
<body>
    <script>
        var audio = new Audio();
        audio.src = "http://m2.music.126.net/wsOaTdfg_N0nIxw9GCJ8Zg==/18724683022784324.mp3";
        audio.play();
        setTimeout(function(){
        	alert("播放被阻塞了么?");
        }, 1000)
    </script>
</body>
</html>

结果有点不同,网速快的话,音乐已经开始播放了,但是一旦出现 alert 弹窗,正在播放的音乐却暂停了,点击确定又继续播放。

熟悉 HTML5 的童鞋又想到一个好东西了 —— Worker,这玩意可是能产生真正意义上的系统级别的线程。用这个对象来产生一个线程能不能避开 alert 的线程阻塞?

代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>alert 线程阻塞</title>
</head>
<body>
    <code>
    	var count = 0;
        setInterval(function(){
            console.log(count++);
        }, 1000)
    </code>
    <script>
        var script = document.querySelector("code").innerHTML;
        var blob = new Blob([script], {type: "text\/html"});
        new Worker(window.URL.createObjectURL(blob));
        alert("计数被阻塞了吗?");
    </script>
</body>
</html>

结果出乎意料,alert 仍然阻塞了代码执行,说明了 js 引擎在执行到 new Worker 的时候并不是立即新建了一个线程,而是等待代码全部执行完了之后才新建。

还和上面一样,我对 alert 应用 setTimeout 再试试呢?

代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>alert 线程阻塞</title>
</head>
<body>
    <code>
        var count = 0;
        setInterval(function(){
            console.log(count++);
        }, 1000)
    </code>
    <script>
        var script = document.querySelector("code").innerHTML;
        var blob = new Blob([script], {type: "text\/html"});
        new Worker(window.URL.createObjectURL(blob));
        setTimeout(function(){
            alert("计数被阻塞了吗?");
        }, 3000)
    </script>
</body>
</html>

这次结果和我们预期的一样了,在弹出 alert 之前,console 在每隔一秒钟输出一次,但一旦出现 alert,输出被阻塞了,点击确定后会将没有显示的计数全部输出,说明了 alert 的线程阻塞是全局性的,chrome 中出现 alert 后,所有标签都不能点击。不过这里我们可以看到,通过 Worker 和setTimeout,是可以不让 alert 阻塞线程的。

面试题

上面已经扯的有点远了,下面回到面试题。

add();
var num = 10;
function add(){
    var num = 10;
    num += 10;
    alert(num);
    return num;
}
alert(num);
alert(add() + 10);

这道题回想起来还是觉得自己犯二了,考察的点也就那么几个:

  • var 变量声明提升:会把用 var 声明的变量提升到代码最开始,但是赋值仍在原位置。
  • Function 函数声明:用Function定义的函数会在JS解析结束时就完成定义,它的定义比 var 优先级还高。
  • 函数作用域(其实就是考察作用域链)。
  • alert 执行特性,alert 只接受字符串类型变量,任何非字符串变量都会转换成字符串。

针对上面的考察点,我们用一些代码来判断下:

  • var 变量提升:
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
</head>
<body>
	<script>
	    // var 变量提升
		alert(`变量str:${str}`);
		var str = 'var 定义比赋值优先';
		alert(`变量str:${str}`);
	</script>
</body>
</html>
  • Function函数声明:
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
</head>
<body>
	<script>
	    alert(test())
	    function test() {
	    	return 'Function函数声明'
	    }
	    var test = function() {
	    	return 'test已被覆盖'
	    }
	    alert(test())
	</script>
</body>
</html>

所以这道题的实际执行代码如下:

function add(){
    var num = 10;
    num += 10;
    alert(num);
    return num;
}
var num;
add();
num = 10;
alert(num);
alert(add() + 10);

结果很明了,第一次会弹出 20,第二次弹出 10,第三次弹出 20,第四次弹出 30。