☰
JavaScript空值判断全解:从真假值到null、undefined、NaN的底层原理与工程实践
2026/9/29 1:47:03 网站建设 项目流程

做前端这几年,我发现在 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) // false(BigInt 的 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就变成比较字符串了,逻辑全线崩溃。虽然概率低,但这属于那种“踩一次就忘不掉”的坑。

第二种是用typeof:

typeof 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') // false

isNaN会先把参数强制转成数字,'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 和 undefined:

function 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或者Set:

const 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的情况。把这些显眼的坑填平,代码质量就已经提升一大截了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询