深入理解JavaScript中的null和undefined:从底层原理到生产避坑

发布时间:2026/10/11 10:05:20
深入理解JavaScript中的null和undefined:从底层原理到生产避坑 做前端这些年null和undefined这对“空值双胞胎”是讨论频率最高的基础概念之一。随便打开一个业务项目几乎每个文件都能看到它们可真要追问一句“两者的底层区别是什么判断一个值是否为空应该用 null || undefined还是 null为什么typeof null返回object”很多写了三五年代码的同学也会卡壳。今天这篇不整面试八股我尽量从语言设计、真实场景、封装判断、生产环境踩坑几个角度把null和undefined拉通讲清楚顺便给出一套可以直接搬进项目的处理建议。刚接触 JavaScript 的初学者可以用它打地基工作三五年的开发也能回头查漏补缺。1. 追根溯源为什么JavaScript里会有两个“空值”1.1 设计层面undefined 和 null 的原始分工在 ECMAScript 规范里undefined和null从定义上就是两种完全不同的概念。undefined表示“未定义”比如声明了一个变量但没有赋值或者函数没有返回任何内容系统会自动给出undefinednull表示“空引用”它明确指向一个“没有对象”的状态需要开发者主动写出来。这个分工对应两种截然不同的使用心智。undefined大多是运行时自动产生的结果一个对象属性从来没人赋值读出来就是undefined一个函数没有return调用结果也是undefined。而null通常承载“查询动作已完成结果是空对象指针”这层意思。比如document.querySelector(#app)找不到目标节点时返回null它不是告诉你“这个查询功能没定义”而是告诉你“我找到了但是是空的”。我习惯用一个生活化的类比来解释undefined像桌面上还没有摆杯子的位置你去摸那个位置什么都摸不到null像桌面上摆了一个空杯子你摸到了一个容器但里面没有水。前者是“根本不存在”后者是“存在但内容为空”。这个区分在实际排查 bug 时非常关键。看到某个值是undefined第一反应是“是不是没赋值、没返回、没传参”看到值是null第一反应是“是不是某个 API 主动给了一个空结果”。排查方向搞对了问题能快一半。1.2 语法层面typeof null 的历史遗留问题凡是面过 JavaScript 的基本都被问过这道题为什么typeof null object答案是历史遗留 bug。第一版语言设计里null被标记为对象的占位符内部类型标签是 0后来加入的object也沿用了 0 这个标签于是typeof无法区分直接给null返回了字符串object。规范组织讨论过要不要修复结论是“不敢动”因为大量线上代码依赖这个行为修复会导致整个 JavaScript 生态大面积报错。typeof undefined则是正常的返回undefined。所以很多老代码会这样写if (typeof value undefined) { // 说明 value 是 undefined且不区分“未声明”和“值确实是 undefined” }为什么老代码偏爱typeof因为typeof是唯一一个访问未声明变量也不会抛错的操作typeof notDeclaredVar; // undefined正常 notDeclaredVar; // ReferenceError这里有个极容易被搞混的点typeof undeclaredVar返回字符串undefined不报错但直接写undeclaredVar undefined会抛 ReferenceError因为左值根本没有绑定。所以“判断变量是否存在”用typeof判断“值是不是 undefined”用 undefined更精确。还有种更稳妥的写法是void 0。在极古老的实现里全局undefined理论上可以被重新赋值现代引擎基本杜绝了但写库的人仍然习惯防御const isUndefined (v) v void 0;void 0永远得到真正的undefined不受任何外部影响。面试时能说出这一层是明显的加分项。2. 语义拆解两者在真实代码里的表现差异2.1 undefined 的典型出场方式undefined在真实代码里出现的场景非常固定咱们可以列一个速查表场景示例结果声明未赋值let a;a为undefined对象属性不存在const obj {}; obj.aundefined函数没有 returnfunction f() {} f()undefined函数参数未传入function f(a) {} f()a为undefined数组越界访问[1][5]undefined解构不存在的属性const { a } {}a为undefined你会发现undefined几乎都是“没有值”的默认表现。尤其是参数缺省这是业务编码里最容易埋雷的地方。比如写一个工具函数function formatPrice(price) { return price.toFixed(2); } formatPrice(); // TypeError: Cannot read properties of undefined这里price是undefined调用.toFixed直接崩。这类报错的本质不是 null 问题而是参数没传排查思路完全不同。2.2 null 的典型出场方式null在真实代码里大多是“主动给出”的常见场景有这几种浏览器 API 返回空结果document.querySelector(.not-found)返回null。正则匹配失败abc.match(/d/)返回null。手动释放引用把不再使用的对象引用置为null方便垃圾回收。链表节点没有下一项node.next约定为null。接口字段显式返回空后端返回{data: null}表示“这个字段当前为空”。其中match返回null这一点很值得留意很多刚入行的同学以为匹配不到会返回空数组实际上match在没有成功匹配时返回null导致后续match[0]直接抛错。先判断再取值是这种场景的标准姿势const matched str.match(/pattern/); if (matched) { // 安全取用 matched[0] }顺便说一句JavaScript 正则里还有个返回null的 APIRegExp.prototype.exec。它匹配不到时同样返回null处理方式和match一致。2.3 工程上如何选择编码原则既然语言给了两个“空值”写代码时就得有个原则否则团队代码里一会儿 null 一会儿 undefined排查成本直接翻倍。我的建议是三句话第一声明变量时如果暂时没有值不要刻意写成null让它保持undefined就好。undefined是语言默认的空值形态强行写let count null并没有额外信息量。第二当你需要明确表达“这个值允许为空且当前就是空对象引用”时用null。典型例子是组件渲染函数返回null表示“我决定不渲染任何内容”undefined在某些框架里可能触发警告甚至报错两者语义完全不同。第三团队内部必须约定“可选字段怎么表示”。我见过一种比较实用的约定业务接口里字段缺失就完全没有这个 key前端读取得到undefined字段存在但可选内容为空后端返回null。这样前端拿到undefined知道是“没传”拿到null知道是“传了但空”。这个约定能让前后端联调时的误会减少一大半。3. 判断与处理一套实用判断体系3.1 四类常见判断方式横向对比判断一个值是不是“空”最怕写着写着把自己搞晕。我先把主流方式摆出来对比方式代码判断结果严格等于 nullv null只为 true 当且仅当 v 为 null严格等于 undefinedv undefined只为 true 当且仅当 v 为 undefined严格等于二选一v null || v undefinednull 或 undefined 都算空宽松等于 nullv nullnull 或 undefined 都算空typeoftypeof v undefined只判断 undefined可安全检测未声明变量重点说说v null这个写法。null undefined在宽松相等规则下是true所以v null其实等价于“v 是 null 或 undefined”。很多开源工具库内部就是这么收敛判断的const isNil (v) v null; isNil(null); // true isNil(undefined); // true isNil(0); // false isNil(); // false isNil(false); // false这个写法简洁、接近英文语义但缺点是它用了很容易触发 ESLint 的eqeqeq规则报错。团队如果开启了严格相等规范更稳妥的写法是const isNil (v) v null || v undefined;两种写法效果一样取舍主要看团队 lint 规则。我个人在工具函数里喜欢用v null因为它是少数几个我觉得用得合理的地方但在业务代码里为了避免和其他人的编码习惯冲突我会写成显式的二选一。如果还要区分 null 和 undefined 本身可以用Object.prototype.toString这种“终极辨别”手段Object.prototype.toString.call(null); // [object Null] Object.prototype.toString.call(undefined); // [object Undefined]不过日常业务基本用不到这个它更多是给底层库或调试工具用的。3.2 可空值处理三件套可选链、空值合并、解构默认值现代 JavaScript 处理“可空值”有三件套熟练使用后能删掉大量冗余判断代码。第一件可选链操作符?.。它的作用是当访问路径上任意一环是null或undefined时整个表达式短路返回undefined不再往后访问。const name user?.profile?.name; // 相当于user 为 null/undefined 时返回 undefined否则继续访问 profile它还能用在方法调用和索引访问上user?.sayHi?.(); // 方法不存在时不报错 arr?.[0]; // 数组可能为空时不报错我在业务代码里见过最舒服的写法是这样的const cityName res?.data?.address?.city ?? 未知城市;res为undefined、data为null、address不存在任何一环出问题都不会崩全部交给最后的??兜底。第二件空值合并操作符??。它只在左侧是null或undefined时返回右侧默认值const count input ?? 0; count; // input 为 null/undefined 时取 0否则原样返回这里要特别对比传统的||。很多人习惯写const count input || 0这在input为0或时会出大问题0 || 0结果是 0 没问题但hello || unknown会得到hello而空字符串 || unknown却得到unknown。如果空字符串本身是有意义的业务值||就帮了倒忙。??只针对 null/undefined不会误伤空字符串和 0。第三件变量的默认值机制。函数参数默认值和解构默认值都只在“值严格为 undefined”时触发null 不会触发function greet(name 陌生人) { return 你好${name}; } greet(undefined); // 你好陌生人 greet(null); // 你好null null 不会被默认值替换 const { age 18 } person; // person.age 为 undefined 时 age 为 18 // person.age 为 null 时 age 为 null这个细微差别很容易被忽略。尤其写公共组件时外部传入null表示“我要显式清空内容”如果默认值机制把 null 替换掉语义就变了。说得更直白一点默认值只兜“没传”的情况不兜“传了空值”的情况。3.3 一些容易翻车的“假”判断写代码时经常有人顺手用!v判断“值是否为空”这是大坑。!v判断的是“假值”0、、false、NaN、null、undefined 全会命中!0; // true !; // true !false; // true !NaN; // true !null; // true !undefined; // true如果业务里数字 0 是有意义的这就明显不应该用!v。比如解析接口返回的优惠金额0 表示“没有优惠”但用!v判断后 0 被当成“无值”可能直接替换成默认值。还有一个容易误判的方向是 DOM 查找结果。querySelector找不到时返回null有人为了省事写成if (el)判断这在大多数场景没问题因为存在节点时el是 truthy 的。但要注意如果以后改了代码用getElementsByClassName系列 API返回的是 HTMLCollection即使没有匹配元素也是一个length为 0 的类数组对象不是null判断逻辑又要跟着变。判断“数字是否合法”也有类似陷阱。Number(abc)返回NaNNaN既不是 null 也不是 undefined它是数字类型里表示“非法数字”的值。不要用空值判断去承接它应该用Number.isNaN单独处理。4. 实战坑位从报错到隐式转换都在哪里找麻烦4.1 常见报错分类与排查思路新手最常见的两类报错一眼就能区分问题方向TypeError: Cannot read properties of undefined (reading xxx) TypeError: Cannot read properties of null (reading xxx)前者说明你访问了一个值为undefined的对象的属性方向是“对象没有被正确赋值或传递”后者说明你访问了一个值为null的对象的属性方向是“查询结果为空但仍然继续访问了”。排查时我一般按三步走。第一步在报错行打日志把上一级对象整个打印出来看它到底是undefined还是null还是结构里少了一层。第二步在调用入口处临时加上console.log确认入参是否被外部吞掉很多问题是异步请求还没回来就用数据了。第三步借助可选链临时“护桩”比如把res.data.list改成res?.data?.list如果报错消失就说明是链路途中某个环节为空而不是数据本身结构有问题。大型项目里我还会建议给每个模块的读取入口统一封装一个get函数function get(obj, path, defaultValue) { const parts path.split(.); let current obj; for (const key of parts) { if (current null) return defaultValue; current current[key]; } return current undefined ? defaultValue : current; } get(res, data.address.city, 未知);这个工具函数的好处是把“空值判断”收敛在一个地方业务里不要再到处散落嵌套。老项目里没有可选链时这种封装能减少大量报错。4.2 隐式转换与比较中的意外行为null和undefined在比较和转换上有一堆违反直觉的规则面试常考业务里也容易踩。先把基础结论摆出来null undefined; // true null undefined; // false null 0; // false undefined 0; // false null false; // false undefined false; // false Number(null); // 0 Number(undefined); // NaN null 1; // 1 undefined 1; // NaN Boolean(null); // false Boolean(undefined); // false重点解释两个反直觉的点。第一null undefined为 true但null 0为 false。宽松相等规则只看左右两端是否为 null/undefined不把 null 转成 0 去比。所以实际业务里if (value null)是一个安全又通用的“空值判断”它不会误伤 0 和空字符串这一点和很多人直觉相反。第二Number(null)是 0Number(undefined)是 NaN这个差异在算术运算里会悄悄改变结果。比如const total (price ?? 0) (count ?? 0);如果你写成price count当price为null时相当于0 count好像没崩但price为undefined时结果直接变NaN后面所有计算全部污染。这类问题最难查因为不会立刻报 TypeError而是等到某个NaN的样式或金额渲染出来后才发现。我之前排查过一个场景用户输入框删空时表单底层把值从字符串置成了null又有一层逻辑把 null 传进了金额计算结果算出来的优惠金额全是 NaN。根因就是Number(null)为 0 的直觉导致没人想到 null 会让计算出问题。后来把所有金额字段在进入计算前统一做了显式转换和空值兜底问题就稳定消失了。4.3 JSON序列化与传输边界的影响前后端联调时null 和 undefined 在 JSON 序列化阶段的处理截然不同这是另一个高频坑。JSON.stringify({ a: null }); // {a:null} JSON.stringify({ a: undefined }); // {} JSON.stringify({ a: undefined, b: 1 }); // {b:1}undefined作为属性值会被 JSON 序列化直接丢弃即使它在内存里确实存在于对象上null则会被保留。函数属性和 Symbol 属性同样会被忽略。更微妙的是数组JSON.stringify([1, undefined, 3]); // [1,null,3]数组里的undefined会被转成null因为在 JSON 的数据模型里数组没有“空洞”概念只能用 null 占位。这意味着同一个值作为数组元素和作为对象属性序列化结果完全不一样。这个特性直接影响接口数据的“语义保真度”。前端往后端传对象时如果某个字段是undefined序列化后字段直接消失后端拿到的是一个没有该 key 的对象而不是值为 null 的对象。后端再往下游传递时可能把“缺失字段”和“null 字段”当成两种业务含义比如存档里“没有填写备注”和“填写了空备注”被区分对待前端就会很被动。我比较推荐在前后端接口层做统一约定凡是可选字段要么显式填null要么不传前端读取时用??做兜底避免把undefined和null的语义差异传导到业务判断里。如果团队有数据校验层最好在边界处统一把undefined归一化为null或者反过来总之只保留一种空值语义。5. 面向真实工程的编码建议5.1 给团队订一条“空值策略”很多项目里的 null/undefined 混乱根源不是技术能力而是没有约定。我待过的团队里最有效的一条规则是所有可能出现空值的函数参数在入口处统一用??或显式判断收敛所有可选对象属性默认不写 undefined存在但为空时写 null。举个例子一段常见的组件代码function Tooltip({ title }) { return title ? span{title}/span : null; }调用方如果传title{undefined}组件返回 null 不渲染传title{null}同样行为。这里 undefined 和 null 的语义被 component 内部消化掉了还行。但如果组件内部把title存到 state 里再加工就很可能发生“undefined 被 JSON 序列化丢掉null 被保留”的差异排查起来非常烦。我更推荐的写法是在边界立刻归一化function Tooltip({ title }) { const normalizedTitle title ?? ; if (!normalizedTitle) return null; return span{normalizedTitle}/span; }入口处用??把 undefined/null 一起兜成业务默认值之后逻辑里不再关心“到底是哪个空值”。这条策略简单、可执行对新手非常友好。5.2 用 TypeScript 和静态检查把坑前置如果项目已经用了 TypeScriptstrictNullChecks一定要打开。打开后类型系统会强制你处理 null/undefinedlet name: string | null null; name.length; // 报错对象可能为 null配合类型收窄代码会变得非常严谨function printLength(value: string | null) { if (value null) { console.log(value is null); return; } console.log(value.length); // 这里 TS 知道 value 一定是 string }NonNullable工具类型也能把null和undefined同时剔除type T NonNullablestring | null | undefined; // T 是 string静态检查的价值在于把“运行时才暴露的问题”提前到“编译期”成本远低于生产事故。ESLint 那边我建议重点关注eqeqeq和no-cond-assign它们能倒逼你把 null的写法显式化从而让人一眼看出判断意图。5.3 自测一下这组题能不能全答对最后给一组自测题建议你先在脑子里过一遍答案再跑一下验证typeof null; // object typeof undefined; // undefined null undefined; // true null undefined; // false Number(null); // 0 Number(undefined); // NaN function f(a 10) {} f(null); // a 是 null默认值不生效 f(undefined); // a 是 10 const { x 1 } {}; x; // 1 const { y 1 } { y: null }; y; // null const arr new Array(3); arr.map(i i); // 稀疏数组map 会跳过空位 Array.from(arr).map(i i); // [undefined, undefined, undefined]最后一道题涉及数组空位补充解释一下new Array(3)创建的是“稀疏数组”有 3 个空位map遍历时会跳过空位用Array.from转换为正常数组后空位变成undefinedmap才会真正执行回调。很多人在“基于下标初始化数组”时踩过这个坑本质也是对 undefined 的理解不够细。我个人在实际项目里的体会是null 和 undefined 的知识点看着是基础题但几乎每个季度都会以某种形式冒出来一次要么是联调数据对不上要么是某个组件在边界场景崩溃。重点是别靠“记住了答案”去应付而是理解语言把“空”拆成两种语义的设计初衷再通过团队约定和工具链把混乱降到最低。真遇到新项目我建议技术负责人开工第一天就把“空值策略”写进开发规范哪怕只是三句话后续省下的排查时间都会远超预期。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询