Node.js中JSON.stringify可见对象属性但!obj.val判断仍进入分支
Node.js 10环境下属性存在但
!obj.val判断偶现进入分支问题 问题背景
某次网络调用返回的响应对象预期结构如下:
{ id: 'xyz', val: 'abc:123' }
运行时出现偶现异常:如下if判断分支,在val属性确认存在的场景下仍会进入,分支内打印的日志始终能观测到val属性:
if (!obj.val) { console.log(JSON.stringify(obj)); }
日志输出的序列化结果为:
{"id": "xyz", "val": "abc:123" }
该问题本地无法稳定复现,运行环境为NodeJs 10.22.1。
根因分析
这个现象本质是两次独立的属性访问返回了不同结果:if (!obj.val)做了第一次obj.val访问,JSON.stringify(obj)遍历属性时做了第二次obj.val访问,两次访问之间值发生了变化,才会出现“判断时val为falsy,打印时val有正常值”的矛盾现象。结合Node.js 10的环境特性,常见触发原因有三类:
- 访问器属性(Getter)/懒解析逻辑副作用:网络请求返回的
obj大概率不是普通JS对象,而是HTTP客户端、流式JSON解析器封装的带懒加载逻辑的对象:第一次访问val时,对应字段的解析逻辑还未完成,getter返回undefined,!undefined === true因此进入分支;等执行到JSON序列化时,字段解析已经完成,第二次访问val就返回了正确的字符串值。部分写得有问题的懒加载getter、Proxy拦截逻辑也会出现第一次访问返回falsy值、后续访问返回正常值的问题。 - Node.js 10版本的底层Bug:Node.js 10.22.1对应的V8引擎版本为6.8,存在内联缓存(IC)优化的偶现Bug,动态添加属性的对象在触发去优化的场景下,第一次访问属性可能返回undefined;同时该版本下
worker_threads、原生C++扩展对象传递均为实验特性,也存在跨上下文访问属性偶现返回异常值的问题,这类Bug在Node.js 12及后续LTS版本中均已修复。 - 极少见的竞态场景:如果
obj是被多个异步逻辑共享的引用,且getter/属性拦截逻辑中存在触发微任务、异步回调的隐式逻辑,也可能出现两次访问之间值被修改的情况(普通同步代码块、普通对象属性不会出现这类问题)。
修复方案
- 先存属性快照再做判断:不要直接在条件表达式中访问可能存在动态逻辑的属性,先将属性值存到临时变量,既可以避免多次访问返回值不一致的问题,也方便排查判断时刻的真实属性值:
// 先取值做快照,只访问一次obj.val const currentVal = obj.val; if (!currentVal) { // 日志里同时打印快照值,避免被后续属性变化误导 console.log(JSON.stringify(obj), '判断时val值为:', currentVal); } - 响应对象转普通对象后再使用:网络请求拿到响应后,不要直接操作客户端返回的原始代理对象,先做一次普通对象转换,彻底剥离访问器、代理拦截逻辑:
// 转成无副作用的普通字面量对象 const resp = JSON.parse(JSON.stringify(rawRespObj)); // 后续逻辑都操作resp即可 - 升级Node.js版本:如果业务依赖了worker_threads、原生扩展等能力,建议升级到Node.js 16/18/20等官方维护的LTS版本,规避旧版本的底层偶现Bug。
- 排查自定义属性拦截代码:如果业务中自行实现了
Object.defineProperty、Proxy拦截逻辑,检查get拦截分支是否存在第一次访问返回falsy值、未正确返回缓存值的问题。
内容的提问来源于stack exchange,提问作者user13887792
相关产品推荐
相关产品推荐

