你不知道的 console.log()

2,218 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

前言

在写代码的时候,我们经常会添加 console.log() 来查看代码是否执行或者执行到某一个位置的时候打印出某个信息来帮助调试。在大多数的情况下,我们这样做是完全没有问题的,但是在你打印引用数据类型想要通过console.log() 去查看它某个时间点的值,那么就要注意了,你看到的,可能并不是它在这个时候的值。

本文主要是在实际使用的时候碰到了类似的问题,由于使用的是小程序开发相关的软件来调试代码,断点并不是很好用(会出现我断到点了,但是看不到对象里面的值),所以使用 console.log() 来进行调试。在这前提下碰到的问题。

问题重现

首先我先来模拟一下我碰到的问题所在:

console.log(this.list);
console.log(this.list.length)
//...其他代码
this.list.push(1)

这段代码在开发者工具中会输出:

image.png

由于 this.list 是一个可观察对象,所以在点开来之前是不知道它里面有什么的。

image.png

在这就让我很奇怪了,为什么明明数组中是有值的,但是打印出来的数组长度却是0,并且我在这两个 console.log() 后面对数组进行了一些删减操作还会报错。

那么实际上在两个 console.log() 执行的时候,数组中是不存在值的,我只是被 console.log() 的输出误导了,我当时认为这个值就一定是这个时机点的 this.list,却不知道,对于引用数据类型来说,console.log() 并不一定准确。

问题点

先用浏览器来复现一下问题所在:

image.png

这时候去点开 a 就会发现:

image.png

a 里面多出来了一个元素。

1648870209(1).png

可以看到浏览器给出了一串提示:'This value was evaluated upon first expanding.It may have changed since then'

浏览器也提示了我们,这个值在第一次确认之后可能发生了改变,这就是因为引用数据类型的存储方法和 console.log() 在不同的个平台上不同的表现导致的。

关于引用数据类型

关于引用数据类型,也就是在 js 中的对象,当然,在js里面数组、函数,其实本质上都是一个对象。在我们声明一个对象之后,会在内存中开辟一部分空间用于保存这些数据

image.png

这也是为什么浅拷贝的两个对象内会共享属性:

image.png

那么就是我们在修改一个对象的时候,其实是在修改它指向的这一块内存数据

关于 console.log()

再者,在不同平台上的 console.log() 有着不同的执行,比如上面的这个问题,在node环境中就不会出现,但是在 Chrome 浏览器中,当我们打印一个引用数据类型的时候,其实浏览器打印给我们看的是它在当时的快照:

image.png

当你再次去点击这个对象展开的时候,浏览器会再去内存中获取一次数据:

image.png

这也就是为什么会出现上面那种问题,因为在 Chrome 中你去展开控制台中的 Object 的时候,Chrome 又会对它求一次值,这一次是去显示它的属性,所以才会出现前后打印不同的情况,因为对象引用的实体的值改变了。

浏览器或者可以说是开发者工具为什么会有这样的表现?

这个问题在《你不知道的javascript中卷》第二部分异步和性能1.1节异步控制台部分有提及:

There is no specification or set of requirements around how the console.* methods work -- they are not officially part of JavaScript, but are instead added to JS by the hosting environment (see the Types & Grammar title of this book series).
So, different browsers and JS environments do as they please, which can sometimes lead to confusing behavior.
In particular, there are some browsers and some conditions that console.log(..) does not actually immediately output what it's given. The main reason this may happen is because I/O is a very slow and blocking part of many programs (not just JS). So, it may perform better (from the page/UI perspective) for a browser to handle console I/O asynchronously in the background, without you perhaps even knowing that occurred.

翻译:

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

书中还了个例子:

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(..)。

关于更多的 console.log() 的执行

然后关于更多的 console.log() 的执行,在 node 文档当中有提到,想要了解更多运行原理可能查看 node 文档

image.png

console 控制台

解决方法

1. 使用 JSON.stringify(..) 来打印数据

使用 JSON.stringify(..) 就会将当时的对象转化为 json 类型,这样就和对象本身取消了关联,只是作为一个字符串打印出来,这样就能够看到我们想要的结果

image.png

2. 使用 ES6 的 ...

还可以使用 {...a} 这种方式来进行输出

image.png

这个操作的本质上是在打印的时候新开辟了一个空间用于存放打印的对象,就像是深拷贝一样,已经是一个不同与原来对象的全新对象了。

总结

由此可见,console.log() 打印出来的内容不一定是可信的,对于基本数据类型来说可能是可以相信的,但是在打印一个引用数据类型的时候就需要小心了,可以关注一下浏览器的提示来查看这个数据在之后是否被更改。

所以一般在可以断点调试的情况下,还是进行断点调试是更加科学稳妥的方法。