JavaScript报错Cannot read properties of undefined的定位与解决方法(Source Map、可选链、防御性取值)

控制台最常撞见的一行报错是这个。

Uncaught TypeError: Cannot read properties of undefined (reading 'name')

还有一个变体,出现在赋值的时候。

Uncaught TypeError: Cannot set properties of undefined (setting 'checked')

两行说的是同一件事:undefined上不能取属性,也不能写属性,null同理。报错只告诉你崩在最后那一下,不告诉你这个undefined是谁传进来的,得自己往回追一层。

先把原始堆栈拿到手

控制台默认展开的是压缩后的bundle.js:1:23456,看不出原始文件。生产构建要留sourcemap,但别把它和js一起放在公网目录,map里是完整源码,谁都能扒走。做法是构建时生成、上传到内网的错误监控或者对象存储,本地留一份用于还原行号。

webpack里用hidden-source-map,只生成文件、不在js末尾挂引用注释:

module.exports = {
  devtool: 'hidden-source-map',
};

Vite对应的是build.sourcemap,设成'hidden'效果一样。拿到压缩后的行列号之后,用source-map包还原:

const { SourceMapConsumer } = require('source-map');

const consumer = await new SourceMapConsumer(rawMap);
consumer.originalPositionFor({ line: 1, column: 23456 });

source-map 0.7.4之后构造函数返回的是Promise,老代码里同步new完直接用的写法会抛错,版本升级的时候踩过一次。

让浏览器自己停在现场

比满屏插console.log快的是异常断点。Sources面板右侧那个八角形图标,勾上Pause on caught exceptions再刷新,代码会停在抛出这一行的位置,右边Call Stack里能看到是谁调进来的。临时插一句console.trace()也能打栈,但要改代码重新构建,不如断点直接。

undefined的四个常见来源

接口返回结构对不上排第一。后端把list挪了个层级,或者参数校验失败时直接回了{ code: 500 },前端照着res.data.list.map去遍历,data就是undefined。判断方法很土但有效:Network面板看一眼真实响应体,再在取值前插一句console.log(typeof res.data)。线上碰到过一次更绕的,网关超时返回了空体,前端JSON.parse之后拿到null,报错却崩在离请求八丈远的渲染函数里,光看报错行号找不到源头。

异步没回来就渲染排第二。React里useState(null)的初值,首帧就去读user.name;Vue 2的data里没声明的属性不进响应式系统,模板里写了也是undefined。这类报错的特点是刷新必现,随手加个setTimeout(0)就消失。

数组越界排第三。arr[i].name、str.split(',')[2].trim()、list.find(...)之后直接取属性,find找不到时返回的就是undefined,split结果长度不够时那个下标同样是undefined。

this指向排第四。严格模式下普通函数里的this是undefined,写成const { name } = this.user直接崩。Vue 2的methods里用箭头函数定义方法,this不再是组件实例,取this.xxx也是undefined,属于同一类。

改法:可选链、空值合并、默认值

const name = res?.data?.list?.[0]?.name ?? '';
const { list = [] } = res.data ?? {};

可选链进的是ES2020,2019年定稿,浏览器支持情况是Chrome 80.0、Firefox 74.0、Safari 13.1,往后的版本都没问题。TypeScript 3.7、4.0、4.9,都支持,target设低了会自动降级成一串三元表达式。走babel的话,@babel/preset-env 7.8、7.20、7.24,配合browserslist自己决定转不转,不需要单独装插件。

数组那一类,显式判空比可选链读着更清楚:

const row = list.find(x => x.id === id);
if (!row) return null;

别把?.当万能药

可选链写顺手之后,很容易变成整条a?.b?.c?.d?.e到处加。看着不报错了,实际是把"接口少返回一个字段"这类真问题藏了起来,页面显示空白还查不出原因。实测下来更划算的做法是在请求层做一次结构校验,必填字段缺了就当场抛一个明确的错,而不是让undefined一路流到渲染函数。可选字段用?.,必填字段不该用?.兜,这是取舍。

map文件别让外部访问

构建产物里带了.map的话,nginx加一段直接拦掉,比发版时删文件省事:

location ~* \.map$ {
  deny all;
}

代价是线上还原行号也一起失效,需要定位的时候改成allow内网网段。之前有个项目图省事把map留在CDN上,源码被整包拖走,补救的办法只有重新发版并轮换密钥。

报错本身不难修,难的是找到undefined是从哪一层传进来的。断点停在抛错那一行,往Call Stack上面翻一两层,基本就能看到源头。数据截至2026年9月。