先聊个真实场景:上线前一天,测试提了个 bug,说“折扣金额填 0 保存不了”。我打开代码一看,同事写的是if (!discount) return '请填写折扣金额',结果数字 0 被!直接判成了“没填”。这就是标题里那串值最典型的坑:' '、0、NaN、false、null、undefined放在一起说“判空”,但它们的语义完全不同,判断方式也完全不是一套逻辑。尤其是很多人以为空格字符串' '和空字符串''差不多,其实在 JavaScript 里Boolean(' ')的结果是true,它压根不是空。这篇文章就把这六种情形挨个拆开,讲清楚它们到底是什么、什么时候该当空处理、以及每种场景下怎么写判断才不出错。适合刚接触前端不久的同学扫盲,也适合写了两三年 JS 但总在边界值上翻车的朋友查漏补缺。
1. 先分清两个层面:你有没有值,和你能不能通过布尔判断
1.1 真假值(truthy / falsy)是 JS 判空的第一层
JavaScript 里几乎所有值都可以被强制转换成布尔类型,转换规则形成了一套“真假值”体系。按 ECMAScript 规范,被当成false的假值(falsy)其实就那几个:false、0、-0、0n(BigInt 的零)、空字符串''、null、undefined、NaN。除此之外的所有值,包括空数组[]、空对象{}、字符串' ',在布尔语境下都是true。
这一点是很多人踩坑的根源。你写if (!value),表面上是在问“value 是不是空”,实际上问的是“value 是不是假值”。这两个问题在大多数场景下重合,但在0、false、''这类合法业务值面前会立刻分道扬镳。比如接口返回一个开关状态enabled: false,你写if (!enabled) return确实能拦住,但回头你想表达“用户没设置过这个开关”时,false和null的语义就被混在一起了。
所以我把判空拆成两个层面:第一层是布尔层面,用真假值过滤空值;第二层是语义层面,判断“这个字段是否存在、是否被设置为一个合法值”。真正的项目里,两层经常要一起用。
1.2 语义上的“空”:0、false 可能是合法值,null、undefined 才是真没有
我们拿最常见的“价格字段”举例。接口返回price: 0,业务上可能表示“免费商品”,这是一个有效值;返回price: null,表示“数据库里没有填价格”;而price这个 key 整个不存在时,取出来是undefined,表示“接口根本没返回这个字段”。三者的业务含义不同,处理方式也应该不同:免费商品要正常展示,没填价格要提示运营补录,字段缺失可能要走容错逻辑。
这就是为什么不能用一句if (!price)通吃所有空值。!price会同时把0、null、undefined、NaN都吞进去,你分不清用户看见的到底是“免费”还是“数据异常”。标题列出的六种值,本质上就是要把这些容易混为一谈的情况拆开,让判断落到具体语义上。
2. 六种情形逐个拆开看
先给一张速查表,后面每个小节展开讲。这张表建议直接存下来,面试和写代码前翻一翻都管用。
| 值 | typeof 结果 | Boolean(value) | !value | value === null | value === undefined | 最常见的正确判空写法 |
|---|---|---|---|---|---|---|
' '(空格字符串) | 'string' | true | false | false | false | value.trim().length === 0 |
''(空字符串) | 'string' | false | true | false | false | value === '' |
0 | 'number' | false | true | false | false | typeof value === 'number' && !Number.isNaN(value) |
NaN | 'number' | false | true | false | false | Number.isNaN(value) |
false | 'boolean' | false | true | false | false | value === false |
null | 'object'(历史遗留) | false | true | true | false | value === null |
undefined | 'undefined' | false | true | false | true | value === undefined |
2.1 空格字符串' '和" ":看着像空,实际上不空
标题把' '和" "放在“判空”列表,这恰恰是最大的误区。一个只包含空格字符的字符串,长度是 1,布尔值是true,它跟空字符串''完全是两回事。我在代码评审里见过太多次这样的写法:用户输入' '通过了校验,后端存了一个“看起来什么都没有”的字符串,前端列表渲染时又显示不出内容,最后排查半天发现是空格串。
判断这类“空白字符串”要单独处理:先用typeof value === 'string'确认类型,再用value.trim().length === 0判断去除首尾空格后是否为空。这个方法能同时覆盖''、' '、' '、'\t'等所有你想当成空白的字符串。
提示:别用
Boolean(value.trim())代替上面那行,虽然结果一样,但语义上Boolean()表达的是真假值,不如.length === 0直观。代码是给人读的,一眼能看出“长度为零”比“转成布尔再取反”靠谱得多。
2.2 数字 0:一个既是合法值又是假值的数字
0是 falsy,所以if (!value)会把0拦截掉。如果这个变量是数量、金额、库存这类业务字段,拦截就会酿成 2.1 开头那个 bug:库存为 0 的商品没法编辑,折扣为 0 的单子没法提交。
判断数字是否为“空”要分两种情况。第一种是纯粹的“这个值存不存在”,用typeof value === 'number'就能判断,0当然是存在的数字。第二种是表单场景下“用户有没有填数字”,这时候0通常也算填写,关键要看业务允不允许。经验是:但凡业务字段可能出现0,就不要用!value做判断,改成显式的value === 0,或者直接判断typeof value === 'number'。
顺带一提,NaN也是 number 类型,所以“判断是否是有效数字”时,光有typeof还不够,后面要跟一个!Number.isNaN(value)。
2.3 NaN:唯一一个六亲不认的值
NaN全称是 Not-a-Number,但它typeof出来的结果是'number'。它是 JavaScript 里最特殊的一个值:不等于任何值,甚至不等于自己。NaN === NaN的结果是false,所以你不能用===判断一个值是不是NaN。
判断NaN有三个派别:全局的isNaN()、ES6 的Number.isNaN()、以及Object.is()。它们结果不一样,坑很大。全局isNaN会先尝试把参数转成数字,比如isNaN('abc')返回true,因为Number('abc')是NaN;但isNaN('123')返回false,因为它能转成数字。Number.isNaN不做类型转换,只有参数本身就是NaN才返回true,所以我推荐用它。Object.is(NaN, NaN)返回true,这个属于边角知识点,有些公司笔试题爱考。
实际开发中NaN最容易出现在parseInt、parseFloat和除法运算的结果里。比如parseInt('')返回NaN,你拿这个值直接做加减乘除,结果全是NaN,排查起来费劲。我的习惯是:凡是外部输入转数字的地方,都先Number.isNaN(result)兜底。
2.4 false:布尔值不等于“没有”
false是 falsy,但它是一个确确实实存在的布尔值。就好比开关的“关”状态,你不能因为它是“关”就说它不存在。这个区分在接口联调时特别明显:后端返回status: false表示“关闭”,返回null表示“状态未知”,返回undefined表示“没传这个字段”。
如果你图省事用if (!status)判断,会把“合法关闭”和“数据缺失”混为一谈,后续要排查问题就说不清了。正确做法是区分场景:如果只需要关心“是否为打开状态”,可以写if (status === true);如果三种状态都要区分,就得if (status === null)、if (status === undefined)、if (status === false)分开处理。虽然代码行数多了点,但每个分支的语义非常清楚。
2.5 null 和 undefined:官方钦定的两个“空”
null和undefined是整个列表里最名正言顺的空值,但两者也有细微差别。undefined表示变量声明了但没有赋值,对象里没有这个 key,或者函数没有返回值;null表示“有值,但值为空引用”,通常是开发者主动赋值表示“这里清空了”或“还没有数据”。
两者用==比较结果是true,用===比较结果是false。这就是为什么老代码里经常有人写if (value == null)——它同时匹配null和undefined,写法很简洁。但团队里如果用 ESLint 且开启eqeqeq规则,这种写法会被标红。我更推荐明确写value === null || value === undefined,或者在允许用??的场景直接用空值合并运算符兜底,可读性更好,也避免双等号带来的隐性转换风险。
3. 真实场景下的判空姿势
3.1 表单校验:把空格、空串、null、undefined 一网打尽
表单是判空需求最集中的地方。用户可能什么都不填(拿到''),可能只敲了空格(拿到' '),也可能组件本身回了null。我建议抽一个公共工具函数,别再到处写if (!value):
function isBlank(value) { return ( value === null || value === undefined || (typeof value === 'string' && value.trim().length === 0) ); } function isNotBlank(value) { return !isBlank(value); }这个函数覆盖了“真没有”和“看起来没有”两类情况。用的时候语义也很明确:if (isBlank(username)) { showError('请输入用户名'); }。代码评审时一看就懂,比if (!username.trim())更严谨——因为直接对非字符串调.trim()会直接抛TypeError。
注意:这里的
isBlank故意没有把0和false算进去,因为它们往往是合法值。如果你要校验数字输入是否填写,额外加参数判断即可,别在通用函数里一刀切。
3.2 接口字段处理:区分“未返回”和“返回了空”
接口返回的数据是最容易出边界问题的地方。服务端有些字段是null,有些干脆不返回,前端拿到的就是undefined。新版 JS 提供了可选链?.和空值合并??,用来处理这种场景非常顺手:
// 旧写法 const name = (res && res.data && res.data.user && res.data.user.name) || '匿名'; // 新写法 const name = res?.data?.user?.name ?? '匿名';这里要特别强调??和||的区别。||只要左边是 falsy 就会走右边,所以左边是0或false时也会被替换成默认值;??只看左边是不是null或undefined,其他值都保留。取值兜底场景,绝大多数应该用??,否则会出现“免费商品价格被改成 0 元默认值”这种诡异问题。
如果后端字段“没返回 key”和“返回 null”需要区分,用Object.prototype.hasOwnProperty判断 key 是否存在,别用if (obj.xxx):
if (!Object.prototype.hasOwnProperty.call(data, 'price')) { // 处理“未返回”的情况 } else if (data.price === null) { // 处理“返回 null”的情况 } else { // 正常使用 data.price }3.3 对象和数组的判空:typeof null 的衍生问题
判空不能只盯单值,对象和数组也会“空”。判断空对象不能直接写if (obj),因为任何对象转布尔都是true,空对象也一样。要用Object.keys(obj).length === 0判断,同时要注意Object.keys的参数如果是null或undefined会抛错,所以前面要加保护:
function isEmptyObject(obj) { return obj !== null && typeof obj === 'object' && Object.keys(obj).length === 0; }数组有没有元素就简单了,Array.isArray(arr) && arr.length === 0。这里有个隐藏坑:typeof null的结果是'object',所以typeof obj === 'object'不能作为“这是对象”的判断依据,必须显式排除null。这个坑后面单独再讲。
3.4 给团队的约定:判空必须绑定业务语义
代码规范里我最看重的一条:判空不是纯粹的技术动作,它要反映业务规则。“为空”到底代表什么,每个字段含义不同,写代码的人一定要想清楚。我的建议是团队内部约定几种固定用法:
- 判断“用户没填内容”用
isBlank这类带 trim 的工具函数; - 判断“字段值合法存在”用显式的类型判断,如
typeof value === 'number'; - 判断“后端没给数据”用
value === null || value === undefined,能合并就写清楚合并; - 取值兜底统一用
??,不要用||; - 涉及布尔开关,一律
value === true或value === false,不写if (value)。
这些约定执行一段时间后,代码里的“空值判断混乱”会明显减少,线上 bug 也少很多。
4. 高频坑位与排查实录
4.1if (!value)吞掉了多少合法数据
这是我在代码评审里讲得最多的反例。!value适合什么时候用?适合你确定要过滤所有 falsy 值,比如“判断是否处于空状态”这种宽泛场景。但一旦value是用户输入、接口返回、业务字段,它就会把0、false、''这些合法值一起误杀。
举个真实的项目案例:一个活动报名表单,年龄字段用户填了0(表示 0 岁婴儿),校验代码写的是if (!age) return '请填写年龄',结果成年人的年龄能过,婴儿的年龄反而被拦。说明单一的真假值判断解决不了业务问题,想让“0 岁”作为合法值,判断就得写成if (typeof age !== 'number')或者明确允许 0。排查这类问题最有效的方式就是全局搜if (!,逐个确认左边变量的业务语义,是数字就换数字判断,是布尔就换布尔判断。
4.2 宽松相等引发的连环爆炸
如果你还在写==,请务必了解下面这张表,它们全是true:
null == undefined // true '' == 0 // true '' == false // true 0 == false // true ' ' == 0 // true,空格字符串会先转成数字 0 ' ' == false // true看到没,' '这种“看着像空但布尔为真”的字符串,放到==里照样会和0、false相等,因为宽松相等会做隐式类型转换。这就是为什么我反复强调:判断空值一律用===,不要给 JS 留“自由发挥”的空间。ESLint 的eqeqeq规则建议直接开成 error 级别,能挡住一大半这种问题。
4.3 typeof null 为什么是 'object':历史问题,但要当心
typeof null === 'object'是 JavaScript 从第一天起就有的怪癖,原因是早期实现里类型标签的问题,后来为了兼容也没法修。它的实际影响是:你不能用typeof判断一个值是不是普通对象,必须先排除null。
我建议写一个工具函数统一处理:
function isPlainObject(value) { if (value === null || typeof value !== 'object') return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; }普通对象、Object.create(null)创建的对象都能正确识别,Array、Date、RegExp会被排除。做深度遍历、深拷贝这类操作时,这个函数能避免不少因null误判导致的崩溃。
4.4 isNaN 和 Number.isNaN:同名函数结果天差地别
全局isNaN和Number.isNaN是面试高频考点,也是实际开发里的易错点。它们的核心区别在于是否做类型转换:
isNaN('abc') // true,字符串被转成 NaN Number.isNaN('abc') // false,参数本身不是 NaN isNaN(undefined) // true,Number(undefined) 是 NaN Number.isNaN(undefined) // false Number.isNaN(NaN) // true Number.isNaN(0 / 0) // true所以代码里判断NaN一律用Number.isNaN,别用全局的isNaN。如果你习惯用Object.is(value, NaN)也可以,但团队可读性上Number.isNaN更直白。
4.5 现场实录:一个判空引发的渲染崩溃
最后放一个典型的报错案例:Uncaught TypeError: Cannot read properties of undefined (reading 'startTime')。这个错在联调时几乎天天见,原因就是接口某个字段没返回,前端直接访问了它的子属性。
修复前:
const startTime = res.data.detail.startTime;修复后:
const startTime = res?.data?.detail?.startTime ?? '';一行改动,崩溃变容错。但要注意,res?.data?.detail?.startTime ?? ''只能拿到一个默认值,如果你需要知道“detail 到底有没有返回”,还得靠前面的hasOwnProperty思路。我的习惯是:渲染类字段用可选链兜底,逻辑判断类字段用显式判空,两者结合才能既稳定又安全。
最后分享一点实战体会
我自己刚写前端那几年,也常被0和null坑到怀疑人生。后来慢慢总结出一个习惯:每个写判空的if里,先问自己一句话——“这个字段为空,用户看到的结果应该是什么”。是提示填表单,还是展示默认值,还是直接隐藏模块?想清楚之后再选判断方式,就不容易写出if (!value)这种“偷懒但危险”的代码了。另外建议团队里维护一份统一的判空工具函数库,把isBlank、isEmptyObject、isPlainObject、hasOwn这类高频函数沉淀下来,大家统一调用,比每个人各写一套靠谱得多。这次把六种空值拆完,希望下次你看到' '能第一时间想到:它不空,要 trim 之后再看。