Chrome Console 打印对象和 Object.keys() 不一致?原来是 Console 的「延迟求值」导致的
前言
大家好!我是小尤~。最近排查一个前端 Bug 的时候,遇到了一个特别迷惑的问题。
控制台打印出来的对象里面明明有数据,但是执行 Object.keys() 却返回空数组。
例如:
console.log(obj);
console.log(Object.keys(obj));
控制台结果却是:
{}
但是点开 console.log(obj) 后,却能看到:
{
id: 1,
name: "Tom",
age: 18
}
第一反应肯定是:
Chrome Console 出 Bug 了吗?
其实并不是,而是 Chrome Console 一个非常经典的坑——Lazy Evaluation(延迟求值)。
一、问题现象
假设有下面这段代码:
const obj = {};
console.log(obj);
setTimeout(() => {
obj.name = "Tom";
obj.age = 18;
}, 1000);
console.log(Object.keys(obj));
运行结果:
[]
但是当一秒以后,再展开第一个 console.log(obj),看到的却是:
{
name: "Tom",
age: 18
}
很多人第一反应:
Object.keys() 为什么没有获取到?
实际上,它获取的时候对象确实还是空的。
二、Chrome Console 为什么会这样?
Chrome DevTools 在打印对象时,并不会立刻把对象内容序列化下来。
它打印的是:
对象引用(Reference)
也就是说:
console.log(obj);
实际上保存的是:
obj 的内存地址
而不是:
obj 当前的值
等你以后再展开这个对象时,Chrome 才会去读取对象当前的数据。
整个流程如下:
console.log(obj)
│
▼
保存对象引用
│
▼
对象后续发生修改
│
▼
点击展开对象
│
▼
Chrome 再读取对象内容
因此:
你看到的是
展开时的数据
而不是
打印时的数据
这就是所谓的:
Lazy Evaluation(延迟求值)
三、为什么 Object.keys() 不一样?
来看下面的代码:
const obj = {};
console.log(obj);
console.log(Object.keys(obj));
obj.name = "Tom";
执行顺序实际上是:
第一步:
console.log(obj);
这里只保存引用。
第二步:
Object.keys(obj);
立即执行。
此时对象还是:
{}
所以得到:
[]
第三步:
obj.name = "Tom";
对象发生变化。
第四步:
你展开 Console。
Chrome 再读取对象。
于是看到:
{
name: "Tom"
}
所以出现了:
console.log(obj)
↓↓↓
{name:"Tom"}
但是
Object.keys(obj)
↓↓↓
[]
其实两者都没有错,只是观察的时间点不同。
四、真实开发中最容易踩坑的场景
例如:
const cache = {};
fetchData().then(data => {
cache.data = data;
});
console.log(cache);
网络请求回来之前:
cache = {}
但是等你过几秒再展开:
{
data: …
}
很多人误以为:
fetch 已经执行完了。
其实不是。
只是 Console 显示的是:
当前对象。
再比如:
const state = {
list: []
};
console.log(state);
state.list.push(1);
展开时:
{
list:[1]
}
但打印时其实:
list=[]
五、如何打印对象当时的真实值?
如果希望看到打印瞬间的数据,而不是后续变化后的数据,可以使用以下几种方式。
方法一:结构化克隆(推荐)
浏览器支持 structuredClone 时:
console.log(structuredClone(obj));
打印的是对象当前的快照。
方法二:JSON 序列化
console.log(JSON.parse(JSON.stringify(obj)));
或者:
console.log(JSON.stringify(obj));
优点:
- 能固定打印时的数据。
缺点:
-
会丢失:
- Function
- Symbol
- undefined
- Date(会变成字符串)
- Map
- Set
方法三:展开对象
console.log({
…obj
});
对于浅对象来说,这也是一个不错的方法。
但是:
const obj = {
user: {
name: "Tom"
}
};
console.log({
…obj
});
里面的
user
依旧是引用。
因此:
深层对象依然可能发生变化。
方法四:使用 console.table()
对于数组或者对象列表:
console.table(list);
调试体验通常比直接打印对象更直观。
六、Chrome 官方对此是怎么设计的?
Chrome DevTools 采用对象引用而不是对象快照,主要有两个原因:
1. 性能更高
如果每一次:
console.log(bigObject);
都立即深拷贝整个对象,
那么:
100MB 对象
打印一次,
浏览器就需要复制一次。
这会严重影响调试性能。
2. 可以实时查看对象变化
例如:
const obj = {};
console.log(obj);
obj.a = 1;
obj.b = 2;
展开以后:
{
a:1,
b:2
}
开发者可以方便地观察对象后续的变化,而不用重新打印。
七、总结
Chrome Console 打印对象与 Object.keys() 看起来不一致,并不是浏览器出错,而是 Console 打印的是对象引用,而不是打印时的对象快照。
记住下面这几点,就能避免很多调试误区:
- console.log(obj) 默认打印的是对象引用,展开时才读取最新值。
- Object.keys(obj)、Object.entries(obj) 等方法会在调用时立即计算,因此反映的是当时的对象状态。
- 如果需要记录对象在某一时刻的真实内容,可以使用 structuredClone(obj)、JSON.parse(JSON.stringify(obj)) 或浅拷贝等方式生成快照。
- 遇到 Console 与代码执行结果看似矛盾时,优先考虑是否是 Lazy Evaluation(延迟求值) 导致的,而不是怀疑 JavaScript 本身。
参考示例
const obj = {};
console.log(obj); // 打印对象引用
console.log(Object.keys(obj)); // []
obj.name = "Tom";
obj.age = 18;
最终控制台可能看到:
console.log(obj)
↓
{
name: "Tom",
age: 18
}
Object.keys(obj)
↓
[]
两者并不矛盾,只是观察的时间点不同而已。
理解了这一点,以后调试对象时就不会再被 Chrome Console "骗"了。
网硕互联帮助中心


评论前必须登录!
注册