尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaScript空值判断全解:从真假值到null、undefined、NaN的底层原理与工程实践

JavaScript空值判断全解:从真假值到null、undefined、NaN的底层原理与工程实践 做前端这几年我发现在 JavaScript 里最容易被反复纠缠的问题不是闭包不是原型链而是“这个值到底空不空”。尤其是标题里列出的这一串 、 、0、NaN、false、null、undefined它们看着都像“空”但实际上性格完全不同判断方式也各有讲究。很多线上 bug 翻来覆去最后定位到根因都是判空逻辑写得不够严谨。这篇就把这几个值一次讲透从底层原理到实际封装再到我踩过的坑一步步拆给你看。这套内容适合刚入门的前端新手也适合写了两三年业务代码但没系统梳理过判空逻辑的同学。看完之后你至少能搞清楚几个问题为什么null和undefined不能一概而论为什么NaN连自己都不等于自己怎么封装一个在真实项目里靠得住的通用判空函数以及面试时被问到和的区别到底怎么答才显得有深度。1. 先把这几个“空值”的底细摸清楚1.1 七个值不是一回事分三类才好记很多人学 JavaScript 的时候喜欢把所有“非真”的值堆在一起记什么0、false、、null、undefined、NaN全是 falsy但这恰恰是后面很多坑的起点。它们虽然都能让if不进去但业务语义完全不同。按我多年的经验这七个值最适合分成三类来理解。第一类是“真正的空”只有null和undefined。它俩代表的是“这里确实没有值”一个是主动声明为空null一个是压根儿就没赋值undefined。在接口返回、变量初始化的场景里这俩出现频率最高也是判断时最容易出事的。第二类是“特殊的假值”包括NaN和空字符串。NaN是Number类型里的一个特殊成员表示“当前这个结果不是有效数字”空字符串则意味着“有值但是长度为 0”。这俩都有明确的类型只是内容上表现成一种“空”的状态。第三类是“看起来像空其实是有效值”就是0和false。这两个是整篇文章里最容易被误杀的对象。你做金额计算时0是无价的合法结果你做开关状态时false就是明确的关闭状态。如果统一用if (!value)去判断这类值会被一刀切掉业务逻辑直接错乱。 空格就更特殊了它是一个长度为 1 的字符串本身不是空但视觉上会让人误以为空。凡是做表单校验的人肯定都被它坑过。把这三类的边界划清楚后面写判断逻辑的时候思路就比价清晰了你到底想判断“没有值”还是想判断“不是有效输入”这两件事代码逻辑是完全不一样的。1.2 typeof 和布尔转换判空的两块基石要搞懂判空typeof运算符和布尔转换规则是绕不开的两个底层机制。typeof用来区分变量类型布尔转换规则决定了一个值在条件判断语境里到底算true还是false。先看typeof对这几个值的返回结果const values [null, undefined, NaN, 0, false, , ]; values.forEach(v console.log(typeof v)); // 输出object、undefined、number、number、boolean、string、string这里最显眼的奇怪结论就是typeof null返回object这是 JavaScript 最早版本留下来的设计缺陷。当时设计者用低位标志位来区分对象和基础类型null的标志位是 0恰好被归成了对象这个行为几十年都没法修修了就全乱了。明白这个历史原因之后你就不会再纠结“为什么偏偏 null 这么特殊”。再看布尔转换。JavaScript 在if、!、!!这些场景里会把值转成布尔类型只有少数几个值会被转成false。完整列表如下Boolean(false) // false Boolean(0) // false Boolean(-0) // false Boolean(0n) // falseBigInt 的 0 Boolean() // false Boolean(NaN) // false Boolean(null) // false Boolean(undefined) // false除此之外什么[]、{}、 、false转出来全是true。这一点非常反直觉很多人以为if ([] true)或者if (false)会不进分支实际上[]是真值字符串false也是真值分支照进不误。1.3 宽松等于是判空混乱的罪魁祸首和的区别几乎是前端面试必考题但在实际开发里坑人的频率远比你想象的高。原因在于会做隐式类型转换而它转换的规则非常不直观。直接看几个结果null undefined // true null undefined // false 0 false // true 0 false // false false // true false // false NaN NaN // false NaN NaN // false看到没有把null和undefined划成了一家人把0、false、也划成了一家人。这在某些场景下像是方便比如x null能同时判断两种空值但更多时候是混乱的根源。0 false返回true意味着你用做条件时数值 0 和布尔关状态会互相干扰。我个人的判断准则很简单默认一律用只有明确想同时兼容null和undefined时才用x null。你甚至可以把它当作一种约定让读代码的人一眼就明白你的意图而不是还要在脑子里做一遍类型转换。2. 每个“空值”的判断姿势与正确写法2.1 判断 null最容易被误判的值null在语义上表示“有意的空”通常由开发者主动赋值比如“这个字段还没有值但它是存在的”。判断null本身并不复杂一句话就能说清if (value null) { // 确实为 null }但为什么我还要单独拎出来讲因为实际开发中绝大多数人对null的判断都混入了undefined的考虑。最常见的错误写法是这样// 错误示范不仅判断了 null还把 undefined 也算进去了 if (value null) { // ... }这段代码本身不算错问题在于写的人不一定清楚 null会把undefined也包进去。如果你只是想命中null结果undefined也溜进来了后面逻辑处理就会出现偏差。更隐蔽的坑是链式属性读取时null的报错。比如接口返回一个user对象但user是null你直接访问user.name就会抛出经典的TypeError: Cannot read properties of null。很多人第一反应是加一层判断// 丑但稳 if (user ! null user ! undefined) { console.log(user.name); }这种写法老实说没有大问题只是每次都要写两个判断太啰嗦。所以后来我逐渐习惯用可选链操作符?.console.log(user?.name); // user 为 null 或 undefined 时直接返回 undefined不报错它等价于在访问属性前自动做了一次空值保护。但注意?.只能避免读取报错如果你需要判断“这算不算空”仍然要回到显式的 null或 null上。2.2 判断 undefined三种写法的安全边界不一样undefined表示“声明了但没赋值”或者“对象上压根没这个属性”。判断undefined至少有三种常见写法安全边界差别很大。第一种是直接变量比较value undefined这种写法在大多数情况下够用但有undefined被局部变量覆盖的先例。虽然现在的代码规范一般不推荐覆盖全局undefined但在旧代码或者某些第三方库的沙箱环境里架不住有人会写出let undefined xxx之类的代码。一旦发生value undefined就变成比较字符串了逻辑全线崩溃。虽然概率低但这属于那种“踩一次就忘不掉”的坑。第二种是用typeoftypeof value undefined这个写法有两个明显好处首先它是字符串比较根本不受全局undefined被污染的干扰其次是它会自动处理“变量未声明”的场景直接访问未声明的变量会抛ReferenceError但typeof对这种场景非常宽容返回的就是undefined字符串。第三种的适用场景很特定就是判断对象属性是否存在prop in obj // 属性不存在时为 false obj.prop undefined // 属性值恰好为 undefined 时也为 true区别在于in运算符只看属性是否存在不看值直接比较值的话可能属性存在但值就是undefined也区分不出来。如果你想判断“对象里到底有没有这个字段”优先用prop in obj或者Object.prototype.hasOwnProperty.call(obj, prop)后者还能排除原型链上的同名属性。2.3 判断 NaN唯一连自己都不等于自己的值NaN的全称是 Not a Number意思是“这不是一个数字”但它偏偏属于Number类型。它产生于各种非法的数学运算比如parseInt(abc)、Math.sqrt(-1)、0 / 0这些操作不会报错而是返回一个NaN。判断NaN最独特的地方在于它是 JavaScript 里唯一一个不等于自身的值。NaN NaN // false NaN ! NaN // true基于这个特性最早的判断方式就是function isNaNPolyfill(value) { return value ! value; }这个写法现在少见了因为语言层面出了更标准的方法但在面试场景里依然可能被问到。真正要注意的是isNaN和Number.isNaN的区别。isNaN(abc) // true Number.isNaN(abc) // falseisNaN会先把参数强制转成数字abc转数字失败所以变成NaN返回true。而Number.isNaN连类型都不放水只有参数本身是number类型且值确实是NaN时才返回true。所以我的建议很直接项目代码里一律用Number.isNaN(value)少用裸的isNaN。除非你明确知道自己在做什么比如想判断一个字符串能不能安全转成数字那用Number.isNaN(Number(value))这种显式组合都比直接用isNaN清楚得多。2.4 判断空字符串和空白字符串差一个 trim 的事空字符串是长度为 0 的字符串判断方式非常直接value // 或 value.length 0这两种没有本质区别value.length 0在语义上更偏“字符串为空”的描述。不过在实际应用中真正难缠的是空字符串的“亲戚”——空白字符串。 、\t、\n这类字符串长度不为 0但一眼看过去跟没输入一样。在表单验证、搜索框、备注内容这些场景里用户完全可能不小心按了几个空格就点了提交。处理这个问题的标准姿势是trim// 判断「空字符串或纯空白字符串」 value.trim() trim会移除字符串首尾的空白字符如果去掉之后啥也不剩那就说明用户实际上什么都没输入。要注意的是trim返回的是新字符串不会修改原值所以判断逻辑不会产生副作用。如果你需要兼容老环境或者想在性能敏感的场景里减少一次字符串创建可以用正则/^\s*$/.test(value)这个正则在字符串只有空白字符时返回true。不过说实话现代浏览器对trim的实现已经足够高效正常业务代码里用trim()就足够了完全没必要为了省一点性能去牺牲代码可读性。2.5 判断 0 和 false看清楚业务再动手0和false在整个判空体系里是最容易“误伤”的两个值因为它们既可以被当成“空”又可以是合法有效值。先说0。在数值语义里0是数学意义上有意义的值金额是 0、库存是 0、评分是 0这些都是有效结果。如果你用一个通用判空函数把所有 falsy 值都打成空0就被错了。判断它裸值很简单value 0但更关键的是场景判断。举个例子一个抽奖接口返回抽中的奖品数量用户可能真的一个都没抽中这时返回0是完全合理的结果你不能把它当作“没数据”处理掉。再说false。布尔值本来就只有两个可能false是有效状态的一极。判断它同样简单value false复杂在于很多“判空”的通用封装喜欢用!value来提前返回导致false直接被当成空值短路了。之前我做过一个配置中心的前端后端返回false表示开关关闭结果因为项目里一个公共的if (!value)判断把所有关闭状态的开关全过滤掉了用户看到的就是“所有配置项都消失了”排查了很久才发现是这个原因。所以结论很清楚如果你要判断“是不是 0”或“是不是 false”直接就行别走通用判空分支如果你的业务中0和false需要参与非空校验比如必填项里填了 0 也是有效填写那就不能把它们排除在有效值之外。3. 从判断到实操封装一套靠谱的判空工具3.1 先搞清楚“空”的边界定义清楚才能封装在动手封装通用函数之前有个更基础的问题必须想明白你的业务里“空”到底包含哪些情况是按语言层面的假值来算还是按业务视角的“无有效数据”来算纯语言层面上假值列表有false、0、-0、0n、、NaN、null、undefined这一串。但业务里的“空”通常只理解为字符串为空、数组为空、对象为空、以及null/undefined。0和false在绝大多数业务里不该被当作“空”。我一般用脑中的一条线来划分业务空值 无值null/undefined 无内容空字符串/空白字符串/空数组/空对象 非数字结果NaN。至于0和false它们是有效值不在通用判空范围里。有了这个边界封装出来的函数才不会过度误杀。3.2 一个通用 isEmpty 函数的演进过程我最早写“判空工具函数”的时候代码极其简陋长这样function isEmpty(value) { return !value; }这个版本的致命问题就是无差别误杀。0、false、全会返回true在业务里根本不敢用。后来演进成判断 null 和 undefinedfunction isNil(value) { return value null || value undefined; }再后来发现字符串要 trim数组要查 length对象要查 keys 数量于是逐步演进成一个比较完整的通用版本function isEmpty(value) { // 1. null / undefined直接视为空 if (value null || value undefined) { return true; } // 2. 字符串去掉首尾空白后判断 if (typeof value string) { return value.trim() ; } // 3. 数组length 为 0 视为空 if (Array.isArray(value)) { return value.length 0; } // 4. 普通对象可枚举键数量为 0 视为空 if (typeof value object) { return Object.keys(value).length 0; } // 5. Map / Set 也值得处理一下 if (value instanceof Map || value instanceof Set) { return value.size 0; } // 6. 数字里的 NaN按照“无有效数值”处理 if (typeof value number Number.isNaN(value)) { return true; } return false; }这个版本已经能在大部分业务场景中直接用但注意一个关键细节判断对象时用了Object.keys(value)它只统计可枚举的自有字符串键属性拿 Symbol 键或者不可枚举属性统计不到。好在日常业务接口返回的数据基本都是普通 JSON这个限制影响不大。3.3 实战场景表单校验、接口兜底、数组去重工具函数落地到具体业务里比函数本身更能体现价值。我挑三个最常见的场景讲讲。第一个场景是表单校验。一个“用户信息”表单里姓名、年龄、备注都有可能没填。用户可能在姓名栏敲了一串空格在年龄栏提交了0在备注栏留空。用trim()处理姓名输入年龄用“是否为有效数字”来判断备注则允许为空。如果一开始就用了if (!name)那么用户只输入空格也能通过校验因为空格是非空字符串如果用了if (!age)那么年龄填 0 也会被判定为“未填”用户就会觉得很奇怪。第二个场景是接口兜底。后端返回的数据结构经常不稳定某个字段可能有时候是null有时候是空字符串有时候干脆没这个字段。建议在数据入口做一次归一化处理统一转成带默认值的结构const userName user?.name ?? 未知用户;??空值合并运算符只会在左侧是null或undefined时取右侧默认值不会像||那样把空字符串或 0 也吞掉。跟||对比一下user.name || 未知用户会把空字符串和 0 全部替换成默认值往往不符合预期。第三个场景是数组去重。NaN在indexOf和普通的比较下都不等于自己所以[NaN, NaN].indexOf(NaN)的结果是 -1去重时NaN会被保留两个。如果你需要真正把NaN也视为重复项并去掉要借助includes或者Setconst arr [NaN, NaN, 1, 2]; const unique1 [...new Set(arr)]; // 实际结果 [NaN, 1, 2]Set 对 NaN 的处理符合预期确实会只保留一个 const unique2 arr.filter((v, i) arr.indexOf(v) i); // 这个写法 NaN 去不掉注意Set内部用的是SameValueZero算法它会认为NaN等于自身所以能正确去重。这个细节经常被忽略值得记一下。3.4 一张表看清各种判断方式的适用场景写到这里把前面提到的判断方式做一张速查表方便你直接拿去做参考目标值推荐写法谨慎/避免写法说明nullvalue nullvalue null null会把 undefined 也包含进去undefinedvalue undefined或typeof value undefined直接访问未声明变量typeof更安全且能覆盖未声明场景null 或 undefinedvalue null!value!value会误伤 0、false、空字符串NaNNumber.isNaN(value)isNaN(value)裸isNaN会做类型转换误判率很高空字符串value !value!value会误杀 0 和 false空白字符串value.trim() value 只判断 会漏掉空格、制表符等0value 0!value0是有效数值falsevalue false!valuefalse是有效布尔值空数组Array.isArray(value) value.length 0!value.length如果 value 不是数组会读不到 length空对象typeof value object value ! null Object.keys(value).length 0!value空对象是真的真值!没用空 Map/Setvalue instanceof Map value.size 0!value同理引用类型不会因为内容为空就变 falsy这张表看起来列了很多行其实核心就一句话能用严格等于解决的问题不要用取反涉及 null/undefined 二选一时明确用 null其他情况优先考虑业务语义再决定是否用通用判空。4. 常见问题与排查技巧实录4.1 开发中遇到过的典型翻车案例先分享几个我真实踩过的坑每一个都是血泪教训。第一个是“属性读取崩溃”。后端返回来一个对象前端没做兜底直接读属性比如res.data.user.name结果res.data是undefined整个页面直接白屏。控制台报错往往就是那句特别经典的Uncaught TypeError: Cannot read properties of undefined (reading name)。看到这句话第一反应就是链路上的某个节点是null或undefined。建议用可选链逐级保护res?.data?.user?.name同时在数据入口处做一次结构校验保证后续访问都建立在合理默认值之上。第二个是“空字符串与空白字符串不区分”。用户提交表单时在输入框里敲了一串空格前端只管把值塞进请求体后端收到一个 而不是空字符串后端判空逻辑没处理数据进库就是一堆空白字符。后来我在所有文本输入口统一做了trim()问题才根治。这也提醒我判空最好放在数据入口统一做而不是分散在业务逻辑里。第三个是“把 0 当成空”。移动端有个功能是显示“本月积分”积分值确实可能是 0。我一开始用if (score)来判断有没有积分结果 0 积分永远不展示等于只展示正积分逻辑直接反了。改成if (score ! null score ! undefined)之后才恢复正常。这个案例特别典型写代码的人往往一眼看不出问题要等到业务测试反馈才发现。4.2 面试里关于判空的高频考点如果你准备前端面试判空相关的问题基本绕不开这几个。第一题typeof null的结果是什么原因是答案就是object。要讲清楚这是早期实现里的 bug当时用类型标签区分对象和基础类型null 的标签位是 0跟对象一样所以被归成了object。能把这个 bug 的来龙去脉讲清楚面试官会觉得你不仅记得结论还了解历史。第二题0 false和null undefined为什么是true但NaN NaN是false这道题考的是的隐式转换规则和NaN的非自反性。能答出“会触发类型转换”只是基本要求能补充“所以项目里尽量用”才有实战意识。第三题如何判断一个变量是NaN标准答案是Number.isNaN(x)加分项是提一下x ! x的 polyfill 思路以及裸isNaN与Number.isNaN的区别。很多人只知道isNaN但没意识到它会把字符串、布尔值都转成数字再判断这实际上是一个经典陷阱。第四题写一个通用isEmpty函数需要考虑什么这题没有标准答案考的是全面性。能想到null、undefined、字符串 trim、数组 length、对象 keys、Map/Set size就已经覆盖得比较完整了。能补充“0 和 false 不该被当成空”的考虑会在面试里加分。4.3 我个人总结的几条判空原则做了这么多年前端我把判空的实践经验浓缩成几条原则每次写代码前都会在脑子里过一遍。原则一区分“无值”与“假值”。null/undefined是一种状态“没有值”0/false/是一种内容值本身是明确的。很多 bug 都源于把两者混淆。原则二优先用显式判断少依赖隐式转换。能写value null就不写!value能写Number.isNaN(value)就不写isNaN(value)。显式判断让读代码的人一眼看懂意图也让类型转换的意外降到最低。原则三判空逻辑尽量收敛到统一入口。表单校验、接口返回、状态管理的 getter这些都是判空逻辑容易出现的地方。与其在每个业务地方写一堆重复判断不如统一封装写清楚测试用例后续维护成本会大幅下降。原则四不要为“省事”牺牲可读性。!!value看起来很酷但读代码的人还要想一下它到底在做什么。直接写value null || value undefined || value 虽然长一点但每个人都能看懂。结尾暂时不总结再聊一个我个人的小技巧在实际项目里我很少一开始就写通用判空函数而是先针对具体场景写出“点名式”的判断比如userName.trim() 、count 0、isLogin false。等这些判断在一个模块里重复出现三四次以后再抽象成公共函数这样才能保证封装出来的东西是真正贴合业务需要的而不是一开始就憋一个“万能工具”看起来什么都能判断结果哪个场景都差点意思。如果你打算参考这套思路去优化现有代码建议先从最容易踩坑的地方入手全局搜一下!value、value null这种写法看看有没有误伤0或false的情况。把这些显眼的坑填平代码质量就已经提升一大截了。
返回列表