前言
大家好,我是抹茶。
今天发现了一个特别有意思的事情,进行console.log(obj),展开前的数据和展开后的数据存在差异。原本以为是数据类型处理上的bug,但这里面涉及了console.log对引用类型的访问处理、浏览器的回收机制。且听故事娓娓道来。
故事的背景
需要让用户在网页上粘贴图片,然后再上传服务器,会使用到HTML 的paste事件。监听paste事件,从event中拿到粘贴板的内容数据,并用console.log打印输出。
核心代码如下:
handlePaste (event) {
console.log('event', event);
const clipboardItems = event?.clipboardData?.items || [];
console.log('循环前长度', clipboardItems.length);
console.log('循环前对象', clipboardItems, '\n');
for (let idx = 0; idx < clipboardItems.length; idx++){
console.log('item', clipboardItems[idx]);
}
console.log('循环后长度', clipboardItems.length);
console.log('循环后对象', clipboardItems)
},
console.log()打印引用类型之坑
有意思的地方来了,DataTransferItemList在展开前显示长度为1,在展开后显示长度为0,这是为什么呢?
查阅了诸多资料,原因在于用console.log()打印引用类型时,访问输出的是对象的快照,而在展开时访问的是对象的最新值。快照和最新值之间可能存在差异。用debugger进行调试,可以规避这种差异
再来看看其他的case
const obj = {
num: 1,
text:'我劝天公重抖擞'
}
console.log(obj);
obj.num++;
obj.text = '不拘一格降人才';
console.log(obj);
在谷歌浏览器中结果如下:
在Safari浏览器结果如下:
在火狐浏览器结果如下:
可见,不同的浏览器对console.log()有自己的实现规则,因为并没有什么规范或一组需求指定console.*方法族如何工作--它们并不是JavaScript正式的一部分,而是由宿主环境添加到JavaScript中的。
尤其要指出的事,在某些条件下,某次浏览器的console.log()并不会把传入的内容立即输出。出现这种情况的主要原因是,在许多程序(不只是JavaScript)中,I/O是非常低速的阻塞部分。所以,(从页面/UI的角度来说)浏览器在后台异步处理控制台I/O能够提高性能,这时用户甚至可能根本意识不到其发生。
根据《你不知道的JavaScript 中卷》内容,最好的选择是在JavaScript调试器中使用断点,而不要依赖控制台输出。次优的方案是把对象序列化到一个字符串中,以强制执行一次“快照”,比如通过JSON.stringify()。
从这三大浏览器的共同之处在于,对于引用类型,打印时输出的是它的快照,在展开时显示的是最新的值。
回到第一个case的粘贴事件上,为何DataTransferItemList的最新值会变变成length为0呢?
浏览器针对临时事件的垃圾回收
paste事件是一个一次性事件,在事件触发完成后,事件产生的数据的数据大概率就用不上了(需要用可以用自定义的js变量存储起来),浏览器会对这样的数据进行垃圾回收,也就是释放掉这部分的内存。
总结
从这个案例可以得出两个点:
-
针对引用类型,
console.log()打印输出的是对象的快照(默认内容是折叠的),在展开时显示的是对象的最新值,保险期间,应该用debugger调试定位对象的值,console.log可能会引发未知bug。 -
针对类似粘贴这样的一次性事件,浏览器会在事件触发完成后,自动回收释放掉事件数据的占用的内存。
demo的源码
// vue文件
<template>
<div>
<div id="app">
<div contenteditable="true" @paste="handlePaste" class="paste-area"
>
粘贴你的图片到这里
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
imageSrc: ''
}
},
mounted () {
},
methods: {
handlePaste (event) {
console.log('event', event);
const clipboardItems = event?.clipboardData?.items || [];
debugger
console.log('循环前长度', clipboardItems.length);
console.log('循环前对象', clipboardItems);
console.log('\n');
for (let idx = 0; idx < clipboardItems.length; idx++){
console.log('item', clipboardItems[idx]);
}
console.log('\n');
console.log('循环后长度', clipboardItems.length);
console.log('循环后对象', clipboardItems)
},
}
}
</script>
<style>
.paste-area {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
margin: 20px;
cursor: text;
min-height: 100px;
width:200px;
height:200px;
img{
width: 100%;
height:100%
}
}
</style>