ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript undefined 深度解析:从原理到防御式编程

JavaScript undefined 深度解析:从原理到防御式编程 凡是写过 JavaScript 的人几乎都跟 undefined 打过照面。控制台里最常见的报错之一就是 Cannot read properties of undefined (reading xxx)——很多人第一次看到这条红字的时候第一反应是“xxx 是什么”第二反应是“undefined 又是什么”。上个月还有朋友把后端返回的 startTime 直接拿来渲染结果字段名对不上一整个列表都是红的。undefined 这个值看起来简单但它牵扯到的坑非常多。它既是全局对象上的一个属性又是一种原始类型既能通过 typeof 安全判断又会在解构、序列化、可选链这些场景里表现出各种“反直觉”的行为。这篇博文把 undefined 从头到尾拆开讲清楚从它是怎么来的到它会在哪些地方给你捣乱再到怎么用一套组合拳把这些坑都填上希望能帮你把这块基础彻底补牢。1. undefined 是什么先弄懂这个值的底层身份1.1 原始值类型与 Undefined 类型的关系首先明确一点undefined 是 JavaScript 里七个原始值之一。你说它是“值”没问题你说它属于 Undefined 类型也没问题。ES 规范里的说法是Undefined 类型只有一个值那就是 undefined任何变量在未赋值之前它的值就是 undefined。这里有个细节经常被忽略全局属性 window.undefined在模块环境下就是 globalThis.undefined在旧版本里是可以被重新赋值的。以前有人写过 window.undefined hello 这种代码导致随后所有 undefined 判断全部失效。现在 ECMAScript 5 之后规范把全局 undefined 属性设置成了只读writable: false你没法再用赋值语句覆盖它了。但如果你在某个函数内部写了一个局部变量叫 undefined比如function test() { let undefined yes; let a; console.log(a undefined); // false因为这里的 undefined 是字符串 yes } test();这种代码在严格模式和正常模式下都会“成功”执行a undefined 的结果是 false因为局部命名遮蔽了全局属性。规范规定这个属性不可写但语法上允许你用同名局部变量这算是一个奇特的坑。再说回判断方式typeof undefined 返回的字符串是 undefined。所以判断一个东西“是不是 undefined”最稳妥的写法是if (typeof x undefined) { // 走到这里说明 x 是 undefined或者 x 根本没声明 }1.2 与 null 的“双子星”区别很多初学者分不清 undefined 和 null面试里也爱问。这俩确实像兄弟但定位完全不同。undefined 表示“这里应该是有一个值但还没有给”。比如你声明了一个变量但不赋值引擎就给它一个 undefined函数没有 return默认也是返回 undefined访问对象里不存在的属性得到 undefined。null 表示“我已经知道这里会有一个对象或值但我想显式清空它”。它更多是一种主动设计的空值用来告诉代码“这个位置是空的不要再往这儿取值了”。从操作层面看有几个明显的差异点对比项undefinednulltypeof 返回undefinedobject转换为数字NaN0转换为布尔值falsefalseJSON.stringify属性直接忽略保留为 null原型链上有没有没有特殊对象没有特殊对象常见来源声明未赋值、函数无返回值、访问不存在属性手动赋值、DOM 查询无结果某些老 API尤其是 JSON.stringify 那一条非常容易踩坑。你有一个对象const obj { name: 张三, age: undefined, id: null }; console.log(JSON.stringify(obj)); // {name:张三,id:null}age 字段在序列化之后直接消失了而 id 字段还在。如果你接的接口是那种“对字段缺失敏感”的后端语言比如 Java 的某些反序列化框架缺字段可能就会报错。所以后端接口返回对象里如果某个字段可能是 null你最好在后端就统一处理好不要用 undefined 作为“无值”的表达。2. undefined 出现在哪些场景典型场景全梳理2.1 变量声明未赋值这是最基础的一种let value; console.log(value); // undefined这里有个容易和“变量未声明”混淆的点。上面这种写法变量已经存在于当前作用域只是没有赋值如果你直接使用一个从未声明过的变量比如 console.log(notExist)抛的是 ReferenceError而不是 undefined。这两个是完全不同的错误状态后面第 4 节判断部分还会详细讲。从内存模型的角度理解变量声明就像在作用域的表格里开了一行但没有填具体的数据引擎就默认把这一行标成 undefined。这一步是 JavaScript 引擎在编译阶段做的事情所以你在声明之前其实也是能“看到”这个变量的var 的情况只是值是 undefined这就是所谓“变量提升”的基础。2.2 函数没有返回值JavaScript 里函数默认返回 undefinedfunction doNothing() { /* 没有 return */ } console.log(doNothing()); // undefined function doAlsoNothing() { return; // 空 return 也返回 undefined } console.log(doAlsoNothing()); // undefined这个特性在链式调用场景里经常引发“事故”。比如你封装了一个方法忘了写 return调用方拿着这个 undefined 继续操作属性直接报 Cannot read properties of undefined。还有一种场景是箭头函数的简写箭头函数如果省略花括号会隐式 return 表达式如果你本来想返回对象却写成了const getConfig () { key: value }; // 这是代码块不是对象 console.log(getConfig()); // undefined这里的花括号被解析成了函数体并没有返回对象。想返回对象必须给对象加个括号() ({ key: value })。看到函数返回 undefined 时优先怀疑是不是少了 return或者 return 后面没写东西。2.3 访问对象中不存在的属性对象访问是 undefined 出现频率最高的地方const user { name: 李四 }; console.log(user.age); // undefined console.log(user.address.city); // 这里会报错吗user.address 本身就不存在值是 undefined再继续读取 undefined 的 city 属性就会报 TypeError: Cannot read properties of undefined (reading city)。这就是第五章要重点讲的问题。先说结论你自己写代码的时候要时刻记得“对象访问是有深度的”深度越深每一步都可能掉进 undefined。2.4 函数的实参少于形参调用函数时实参个数少于形参个数缺失的形参就是 undefinedfunction greet(name, greeting) { console.log(greeting); // undefined return greeting name; } greet(王五); // undefined 王五这一类问题最常见的场景是“参数顺序搞反了”或者“调用方少传了一个参数”。在 ES6 引入了默认参数之后这个问题好很多function greet(name, greeting 你好) { return greeting name; } console.log(greet(王五)); // 你好 王五注意默认参数的触发条件是实参为 undefined 时而不是 null。如果你调用 greet(王五, null)greeting 依然是 null不会变成默认值。这是一个很经典的细节差异很多人不知道。3. 高频报错Cannot read properties of undefined 的成因与破解3.1 报错信息拆解先把报错信息拆开看Uncaught TypeError: Cannot read properties of undefined (reading startTime)这句话翻译过来就是代码想从一个 undefined 值上读取 startTime 属性。更深一层是引擎在执行属性访问表达式的时候发现对象引用是 undefined于是不允许继续下去。为什么会这样因为 ECMAScript 规范规定对 undefined 或 null 执行属性读取点运算、中括号运算都会直接抛 TypeError。这是语言层面的硬性保护不允许探索一个不存在的空间。我们在解析这类报错时的“排查公式”很简单先看 reading 后面的属性名是哪个再看这个属性名是在哪个对象上被读取的。报错信息里的属性名只告诉你“最后一步在访问什么”真正的问题往往出在前面几层比如 data.list[0].startTime报错说 reading startTime那更可能是 data.list[0] 这层就是 undefined。3.2 最常见的触发场景接口返回数据结构变化结合我自己和身边同学的经验这类报错十次里有八次是网络请求引起的。你从后端拿到一个对象前两次调试的时候数据好好的后面前端更新了接口文档字段结构变了某个原本存在的数组变成了 null或者干脆不返回了。前端代码没有做兜底直接一路点下去就崩了。举例fetch(/api/task) .then(res res.json()) .then(data { document.getElementById(time).textContent data.list[0].startTime; });如果 data.list 是 undefined整个过程会报 Cannot read properties of undefined (reading 0)如果 data.list 是空数组那 data.list[0] 是 undefined会报 Cannot read properties of undefined (reading startTime)。这两行报错看着相似但原因完全不同。前者是接口结构变化或字段重名后者是数据本身就是空列表。这类问题的排查思路也很直接把接口返回的原始 JSON 打出来先看 data 这个根对象里到底有没有 list 这个字段再判断 list.length 是否为 0。不要一上来就猜测是代码哪里赋错了值。3.3 排查和定位技巧总结我自己的排查套路可以整理成三步先定位报错文件与行号把鼠标点上去看是哪个表达式在访问属性。浏览器控制台的报错信息会直接给你行列甚至还有一个可供展开的调用栈。在那个表达式前面加 console.log把每一层“中间变量”都打出来。比如 data、data.list、data.list[0]打一轮基本就定位了。给数据访问加“保护”。确认是接口数据后在代码里写兜底逻辑让访问链变为“有值才继续”。还有一个点在 React/Vue 这类框架里这种报错经常出现在 render 阶段。Vue 的报错模板可能会显示类似 “Cannot read properties of undefined (reading name)” 的 render function 相关报错本质上还是某个响应式属性在初始化时是 undefined。所以框架场景下除了排查接口还要注意初始值的定义最好把 state 里的字段初始化为“空字符串、空数组”而不是不定义。4. 判断 undefined 的正确姿势别把判断写成新的雷4.1 全等与 typeof 的灵活选择最常见的判断是 a undefined。这对于“变量已经存在”的场景是完全够用的。但要注意一个问题如果 a 没有声明直接写 a undefined 会抛 ReferenceError而 typeof a undefined 不会。举一个典型案例你在写工具函数时要判断一个全局配置是否存在// 不安全如果 MY_CONFIG 没声明直接 ReferenceError if (window.MY_CONFIG undefined) { // 处理逻辑 } // 相对安全typeof 对未声明变量也不抛错 if (typeof MY_CONFIG undefined) { // 处理逻辑 }当然在浏览器环境里你可以通过 window.MY_CONFIG 来避免未声明的问题因为访问对象的属性不会抛 ReferenceError不存在的属性只返回 undefined。但在 Node 的模块作用域里或者在 Web Worker 这种没有全局 object 别名的地方typeof 更稳妥。所以引擎早期很多代码库里的“安全判断 undefined”都会写成 typeof a undefined就是这个原因。4.2 判断 undefined 的两个常见误区第一个误区是用“假值”来替代判断。undefined 是假值没错但 null、、0、NaN 也都是假值。如果你写成 if (!value) 再做处理那 value 为 0 的时候也会进入这个分支。数字 0 在很多业务里是合法数据不该被误伤。判断 undefined 就老老实实判断 undefined不要图省事用 !value。第二个误区是把 void 0 和 undefined 混用。事实上 void 0 一定返回真正的 undefined后面第 6 节再讲所以用 a void 0 判断也等价于 a undefined。但问题是老代码里有人为了“防止 undefined 被覆盖”才用 void 0现在全局 undefined 已经是只读了这种写法意义不大还让代码可读性变差。4.3 框架中的 undefined 判断体验在 React 中你经常需要处理“列表渲染前先判断数组是否有值”{userList userList.length 0 ? ( userList.map(item li key{item.id}{item.name}/li ) : ( p暂无数据/p )}这里用 userList 就是为了防止 userList 为 undefined 时继续执行 .length。更现代一点可以写成 (userList ?? []).length 0。不过 ?? 这个运算符需要注意兼容性具体看第 5 节。在 Vue 中模板引擎对 undefined 的宽容度比较高渲染 undefined 不会报错只会表现为空。但这不代表你可以随意在模板里访问深层属性Vue 3 的模板编译在某些情况下同样会把 undefined 的属性读取转换为警告。5. 防御式编程让 undefined 不再给你“捣乱”5.1 可选链操作符 ?. 的原理与用法ES2020 推出的可选链操作符是处理链式访问 undefined 的“最强利器”。它的原理是在访问一个属性之前先检查它左边的引用是不是 null 或 undefined是的话就直接短路返回 undefined不再继续访问。原来你会写if (data data.list data.list[0]) { console.log(data.list[0].startTime); }现在可以写console.log(data?.list?.[0]?.startTime);注意几种不同写法data?.list 是访问属性data?.[0] 是访问数组下标fn?.() 是调用函数。它们都能在“被调用者/被访问者”是 null 或 undefined 时安全返回 undefined。但我需要提醒一句可选链虽好也不能滥用。如果某个字段是你业务逻辑的“必填项”你用了 ?.后续代码拿到 undefined 之后继续执行实际上把错误往后推迟了可能最后产生一个更奇怪的值或者逻辑错误。更好的做法是必要的时候给一个显式兜底让数据到达真正需要的那个位置时一定有值。5.2 空值合并运算符 ?? 的使用时机?? 和 || 很像但语义完全不同。|| 在左侧是假值时返回右侧而 ?? 只在左侧是 null 或 undefined 时返回右侧。const n 0; const a n || 默认值; // 默认值 const b n ?? 默认值; // 0如果你要的是“没有值就用默认值”用 ?? 更准确如果你要的是“假值就替换”那用 ||。实际开发中0 和空字符串都是无害的合法数据用 ?? 能避免很多“数字被吞掉”的问题。典型场景是配置项读取const pageSize config.pageSize ?? 10;如果 config.pageSize 是 0上面的写法会保留 0如果写成 config.pageSize || 100 就会被替换成 10这有可能是 bug。5.3 函数默认参数与解构默认值ES6 解构赋值也提供了默认值机制const { name 匿名, age 18 } userInfo;这个写法的触发条件是被解构的属性值是 undefined。所以 userInfo.name 是 undefined 时会用 匿名但如果 userInfo.name 是 nullname 的值依然会是 null。这点和函数默认参数的规则一致。对于嵌套结构还可以组合可选链和解构默认值。不过我个人觉得嵌套太深的时候解构语法会变得很绕不如先把数据规整一下再做解构。函数默认参数还有一个容易被忽略的细节默认参数是按“实参为 undefined”时生效这在第 2.4 节提过。把参数默认值写好是防御式编程在函数边界上最有效的做法。5.4 接口数据兜底给渲染层一个安全的数据模型很多团队会做一个“数据清洗层”从后端拿到原始 JSON 后先映射到前端数据模型同时填充默认值。这个思路非常推荐它能把 undefined 的问题挡在源头function normalizeTask(raw) { return { id: raw.id ?? , startTime: raw.startTime ?? , title: raw.title ?? 未命名任务, tags: raw.tags ?? [], }; }有了这一层之后页面上去掉大量和 undefined 搏斗的代码后面渲染只管写业务逻辑。尤其当后端字段经常变化的时候这样一个数据清洗函数是团队节省时间的好帮手。把你代码里的“散装兜底”收拢成一个统一入口比在模板里到处写?.更可持续。6. 进阶细节容易被忽略的 undefined 底层行为6.1 void 0 与 javascript:void(0) 的历史包袱void 是一个一元运算符作用是对后面的表达式求值然后永远返回 undefined。所以 void 0 是“安全且明确地得到 undefined”的经典写法。为什么历史上很多人喜欢写 void 0而不是直接写 undefined核心原因是早期 IE 中 undefined 是可以被重新赋值的IE8 及更低版本。为了拿到一个“一定为真的 undefined”就会用 void 0。还有就是 void 0 比 undefined 少写几个字符压缩工具很喜欢这么干。那 javascript:void(0) 又是什么意思这是浏览器地址栏里的伪协议写法常见于a hrefjavascript:void(0)。href 会被当做一个 JS 表达式执行void 0 的返回值是 undefined浏览器打开链接时值为 undefined自然不会跳转。现在写页面已经不太推荐这种方案了更好的做法是给 button 绑定事件并设置 typebutton或者用 href# 再加 e.preventDefault()。但从理解 undefined 的角度来说知道 void 0 等于 undefined再看到这类代码就不会懵了。6.2 数组、稀疏数组与 undefined 的微妙关系数组里的“空槽”和“显式的 undefined”是不同的const a1 [undefined, undefined]; const a2 new Array(2); console.log(a1[0] a2[0]); // true都是 undefined console.log(0 in a1); // true console.log(0 in a2); // false因为 a2 是稀疏数组索引上没有元素a1 的两个坑位是有“真实属性”的只是值等于 undefineda2 的两个坑位甚至连属性都不存在。使用 for...of 遍历的时候稀疏数组会跳过空槽使用 map 这类数组方法时空槽也会被跳过。这会导致一些很难察觉的 bug比如const original new Array(3); const mapped original.map((_, index) index); console.log(mapped); // [empty x 3]而不是 [0, 1, 2]如果你希望得到一个“每个元素都是 undefined 的普通数组”应该用 Array.fromconst filled Array.from({ length: 3 }); console.log(filled); // [undefined, undefined, undefined]这个区别在接口数据处理时也常见后端返回的空数组不会有这种问题但如果你自己用了 new Array(n) 创建数组再往里面填数据在填充之前对它做遍历就容易出幺蛾子。6.3 解构赋值时的 undefined 陷阱解构一个不存在的属性得到 undefined这一点和普通属性访问一致。但在函数参数解构场景下有个常见的坑function update(user) { const { name 老张 } user; // 如果 user 本身是 undefined直接报错 }如果你调用 update() 不传参数user 是 undefined去解构 undefined 会抛 TypeError。正确做法是给参数本身也加默认值function update(user {}) { const { name 老张 } user; }这是解构默认值里最容易漏掉的一层“双重默认”。参数默认值和解构默认值不是一回事前者管“参数有没有”后者管“属性有没有”。两者配合才能真正做到健壮。另一个细节解构默认值只对 undefined 生效对 null 无效。如果你调用 update(null)user 的默认对象不会生效user 依然是 null解构 null 也会抛错。所以如果调用方可能传 null最好写 user {}并在函数里用user ?? {}这样更稳妥function update(user) { const safeUser user ?? {}; const { name 老张 } safeUser; }6.4 序列化、隐式转换与 undefined 的其他怪癖JSON.stringify 对我们的行为有两条规则数组中的 undefined 会被转换成 null对象属性中的 undefined 会被完全忽略。第一条也很容易踩坑const arr [1, undefined, 3]; console.log(JSON.stringify(arr)); // [1,null,3]后端收到 null 之后可能强转成 0或者判断逻辑走了另外一条路。所以如果你要提交一个数组给后端最好先过滤一下 undefined或者把数据标准化。隐式转换上undefined 参与数字运算会得到 NaNconst total Number(undefined) 1; // NaN而参与字符串拼接时会变成字符串 undefinedconst text 结果 undefined; // 结果undefined这两种情况如果在界面里面直接展示用户就会看到一条“NaN”或者“结果undefined”观感极差。所以渲染员工信息、金额、时间这类数据前一定要先做值校验和展示兜底。6.5 区分“随机自然产生的 undefined”和“其他语言里的 undefined”热词搜索里经常出现一个报错叫 undefined identifier这其实是 C/C 编译器的提示意思是“这个标识符未被定义”。比如 use of undefined type in6_addr 这类编译错误和 JavaScript 的 undefined 完全是两回事。前者发生在编译阶段是“找不到这个符号”的语法级问题后者发生在运行时是“值还没有定义”的数据级问题。还有嵌入式开发里常见的 undefined symbol 报错指的是链接器找不到某个函数或变量的实现比如 xQueueCreate、MPU6050 之类的。这类错误需要去检查链接库、源文件是否包含、声明与定义是否匹配跟 JS 里的 undefined 没有关系。把它们区分开能帮你少走弯路。我见过一些前端同学遇到 undefined identifier 就跑去问后端其实先看清楚报错的来源是编译器、链接器、运行时区别还是很大的。7. 对这个知识点的一点个人体会最后聊聊我的实际感受。undefined 在 JavaScript 里就像一个“默认的空位”语言设计者把所有“还没想好”的情况都塞给了它变量没赋值、函数没返回、属性不存在、参数缺失。理解了这层设计哲学你会发现它并不可怕反而很有规律。以前我遇到 Cannot read properties of undefined 时也会很急躁后来养成了“三层防御”的习惯接口数据进来先清洗函数入口先设置默认值模板渲染时再使用可选链兜底。这三层下来项目里再也没怎么为 undefined 熬过夜。推荐大家都试试这个思路不用一次到位先从最容易出问题的接口数据层开始也是好的。
返回列表