你不知道的console.log()

背景

偶然在使用JavaScript做题时发现了一个关于console.log()的坑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<script>
let a = [1, 2, 3];
console.log(a[0], a);
a[0] = 0;
console.log(a[0], a);
</script>
</body>

</html>

理论上在浏览器的控制台中会输出:

1
2
1 [1,2,3]
0 [0,2,3]

但是,实际上浏览器的输出:

两次输出的数组竟然都是**[0,2,3]**。

但是在刷新浏览器后:

发现输出结果是正确的,但是在点击下拉框后发现竟然和输出的数组对不上。

而后我将同样的代码使用node进行输出:

发现输出结果又是正确的。

至此可以发现输出结果与运行环境有关,应该是浏览器中console.log()中的问题。

探索过程

然后,通过控制台进行调试,发现在调试过程中,数组a其实发生了变化。这就很奇怪了,为什么调试过程中输出是符合预期的呢?

之后询问了其它大佬,提出过 可以使用JSON.stringify()进行处理,照做之后发现输出结果是正确的:

为什么通过JSON.stringify()进行处理后,输出结果又是正确的呢?

在查找了相关资料后,发现了原因所在。

《你不知道的JavaScript-中卷》第二部分第一章中写道:

并没有什么规范或一组需求指定 console.* 方法族如何工作——它们并不是 JavaScript 正式的一部分,而是由宿主环境(请参考本书的“类型和语法”部分)添加到 JavaScript 中的。 因此,不同的浏览器和 JavaScript 环境可以按照自己的意愿来实现,有时候这会引起混淆。 尤其要提出的是,在某些条件下,某些浏览器的 console.log(..) 并不会把传入的内容立 即输出。出现这种情况的主要原因是,在许多程序(不只是 JavaScript)中,I/O 是非常低 速的阻塞部分。所以,(从页面 /UI 的角度来说)浏览器在后台异步处理控制台 I/O 能够提高性能,这时用户甚至可能根本意识不到其发生。

书中举了一个例子:

下面这种情景不是很常见,但也可能发生,从中(不是从代码本身而是从外部)可以观察到这种情况:

1
2
3
4
5
var a = { index: 1 };
// 然后
console.log( a );// ??
// 再然后
a.index++;

我们通常认为恰好在执行到 console.log(..) 语句的时候会看到 a 对象的快照,打印出类 似于 { index: 1 } 这样的内容,然后在下一条语句 a.index++ 执行时将其修改,这句的执行会严格在 a 的输出之后。

多数情况下,前述代码在开发者工具的控制台中输出的对象表示与期望是一致的。但是, 这段代码运行的时候,浏览器可能会认为需要把控制台 I/O 延迟到后台,在这种情况下, 等到浏览器控制台输出对象内容时,a.index++ 可能已经执行,因此会显示 { index: 2 }。 到底什么时候控制台 I/O 会延迟,甚至是否能够被观察到,这都是游移不定的。如果在调试的过程中遇到对象在 console.log(..) 语句之后被修改,可你却看到了意料之外的结果, 要意识到这可能是这种 I/O 的异步化造成的.

解决办法

  • 如果遇到这种少见的情况,最好的选择是在 JavaScript 调试器中使用断点, 而不要依赖控制台输出。
  • 次优的方案是把对象序列化到一个字符串中,以强制执行一次“快照”,比如通过 JSON.stringify(..)。