工号查询功能上线第二天,客服转来一个让我很尴尬的反馈:工号为 0 的内部测试账号在查询界面怎么都搜不出来。我一开始以为是后端接口出问题了,结果翻后台日志发现参数压根就没传过去——前端在发请求之前,就已经把这个条件悄悄“处理”掉了。顺着调用链一路排查,最终定位到一句再常见不过的判空代码:if (!value) { return; },而那个value恰好是数字 0,一个在很多开发者直觉里“和空没什么区别”的值。
标题里列的这一串值——' '、" "、0、NaN、false、null、undefined,几乎每一个我都在生产环境里见过它们引发事故。坦白讲,JS 里的“判空”从来不是一个 API 能解决的事情,它背后是一整套 falsy、严格相等、隐式转换相互纠缠的规则。这篇文章就把我在这些问题上踩过的坑、排查过的线上事故、最后沉淀下来的判断函数,一次讲清楚。刚入门的朋友可以借此建立正确的空值观;写过好几年业务但还没系统梳理过这个话题的老手,也能当一份查漏补缺的清单。
1. 一个反直觉的线上事故:if(!value) 把 0 和 false 全杀了
1.1 事故现场还原
当时的业务是一个内部管理系统,查询页面上有一个“工号”输入框,前端要求在发起请求之前,把表单里所有没有填写的字段过滤掉。代码长这样:
const params = {}; if (form.employeeId) { params.employeeId = form.employeeId; }表面看完全没毛病:用户填了才带上,不填就不带。问题在于,用户如果真的输入了0,if (form.employeeId)判断为假,params.employeeId根本不会生成。后端拿不到查询条件,接口返回的是全量账号列表,用户在页面上看到的结果就是:我明明输入了 0,页面却像什么都没收到一样。
类似的事故我还见过好几个版本。有个积分商城项目,运营把活动折扣配成 0,前端用if (discount)判断“活动是否配置了折扣”,0 被当成没配置,活动页怎么刷新都不展示折扣信息;还有一次埋点上报,代码里把false当作“没有埋点参数”给过滤了,结果一批 A/B 实验样本直接丢失。这些事故的共同点是:写代码的人想判断的是“业务字段是不是空”,实际写出来的判断却是“这个值是不是假值”。
1.2 直接原因:把“业务空”和“假值”划了等号
这段代码的本意是:判断“用户是不是没填工号”。但if (value)的真实含义是:把 value 丢进 Boolean 转换,看结果是不是true。JS 的隐式类型转换会把0、''、NaN、null、undefined、false全部转成false,所以你以为自己在判断“是不是空”,实际上判断的是“是不是 falsy”。
平时这么写能跑,是因为日常表单字段大多是字符串,用户不填就是'',填了就至少有个字符。可一旦字段里出现数字 0、布尔 false 这种“假值但有业务含义”的数据,判断结果直接偏离预期。这里并不是if (!value)这个写法本身有罪,而是它被用在了错误的地方——它只能表达“是不是 falsy”,表达不了“在业务上到底算不算空”。
1.3 被事故逼出来的一个原则
那次排查之后,我给自己定了一个规矩:判空之前,先回答三个问题。
- 这个字段的类型是什么?是字符串、数字、布尔,还是可能为 null/undefined 的任意类型?
- 这个字段的合法值范围是什么?0 合法吗?false 合法吗?空字符串合法吗?
- 这个字段的“空”该怎么定义?是“没传、没有、没填、解析失败”,还是单纯指 falsy?
不同业务场景里,答案完全不一样。“用户昵称”的空值可以包含空白字符串,但“用户工号”的 0 必须放行;“是否开通会员”的 false 是有效信息,不能当成空丢掉。这种“因场景而异”的特点,就是 JS 判空最磨人的地方。网上那些“一行代码判断空值”的帖子,能应付零散场景,但真要服务好业务,还得把原理吃透。
2. 从 falsy 机制出发,理清“空”的真假两套体系
2.1 falsy 合集:一共只有 8 个值会变成 false
JS 标准里,布尔转换后为false的值一共只有 8 个:
false0-00n(BigInt 的零)''(空字符串)nullundefinedNaN
除了上面这一组,其他所有值在布尔转换后都是true。这里有两个特别容易记错的地方:[](空数组)和{}(空对象)都是 truthy,'0'(包含一个零字符的字符串)也是 truthy。经常有人问“空数组明明没有内容,为什么 if([]) 是 true”,原因就在于 JS 的布尔转换只按规则执行,不按人的直觉执行。
理解了 falsy 集合之后,一个重要结论自然浮出来:falsy 不等于业务上的空。falsy 是语言层面的机制,业务空是产品层面的定义。两者有交集(null、undefined、''在几乎所有场景都算空),也有各自的专属成员(0、false、NaN算不算空,完全取决于字段含义)。想通这一点,日后看判空代码会通透很多。
2.2 四种常见判空写法对照
实际项目里我见过四类惯用写法,先列出对比一下。
| 写法 | 覆盖范围 | 有没有坑 |
|---|---|---|
if (!value) | 所有 falsy 值 | 0、false、'' 会被一起过滤,' '又不会被过滤 |
if (value === null || value === undefined) | 仅 null 和 undefined | 最精准的“不存在”判断 |
if (value == null) | 仅 null 和 undefined | 简洁版,效果同上,利用宽松相等规则 |
if (value === '') | 仅空字符串 | 类型敏感,只对字符串有效 |
我不建议无脑用if (!value),但它也并非一无是处。关键在于使用前想清楚字段的合法值范围:如果字段本身是“用户填写的名字”这种纯字符串场景,if (!value)误伤范围很小;可一旦字段类型可能是数字、布尔或对象,风险就成倍上升。
2.3 我在团队里推的选型建议
- 判断“变量是否存在”,优先写
value == null。它同时覆盖 null 和 undefined,语义天然就是“不存在的值”,简洁且不容易写错。 - 判断“字符串是否为空”,优先用
value === '',要拦纯空格就换成value.trim() === ''。 - 判断“数字是否合法”,把
Number.isNaN(value)和value === 0分开处理,不要混在同一个判断里。 - 判断“布尔值是否是 false”,直接写
value === false,大多数场景都不需要!value。 - 判断“是否确实没有值”,以
value == null为主,再配合类型判断补上 NaN 这类异常值。
这套规则并不复杂,但能挡住 90% 以上的判空 bug。
3. 七种空值逐个拆解:身世、判定方法与典型场景
3.1 空字符串 '' 与 空白字符串 ' ':看起来像空,行为完全不同
先看标题里最容易让人犯迷糊的一组:' '和" "。这两个写法本质上就是同一个值——都是一个包含空格的字符串,只是引号风格不同。真正要和它们对立的,是'',一个什么都不包含的字符串。
''是 falsy,' '却是 truthy,所以if (' ')会走进 true 分支。原因很简单:空字符串长度是 0,空白字符串长度是 1,布尔转换只看字符串是否为“零长度”,不看它是否只有空格。
业务上,用户在一个输入框里敲几个空格再提交,绝大多数产品都认为这是“没填”。因此只判断value === ''远远不够,一个全是空格的值会顺利通过校验,最后把脏数据写进数据库,后续统计、搜索、展示都会出问题。稳妥做法是先 trim 再判断:
// 简单但够用的写法 if (typeof value === 'string' && value.trim() === '') { // 空字符串 或 纯空白字符串 }这里特意加typeof value === 'string'前置判断,是因为如果 value 是 null,直接调用value.trim()会抛 TypeError。很多新手就是在这一步漏了类型守卫,空值没拦住,反而先把页面搞崩了。
3.2 数字 0 与 -0:0 算不算空,由业务定义说了算
0是 falsy,-0也是 falsy。这两个值在大量业务里都是合法数据:工号可以是 0,折扣可以是 0,库存可以 0,金额可以 0。但它们只要经过if (!value),就会被无差别过滤,这正是 1.1 中线上事故的技术根源。
另外还有一个冷知识:0 === -0返回 true,但Object.is(0, -0)返回 false。日常业务基本不用区分它们,但如果你的代码涉及矢量方向、符号位,或者需要做数据快照对比,这个行为就值得记住。
判断数字型字段是否为空,我推荐这种写法:
// 只判断是不是 null 或 undefined,0 放行 if (value == null) { // 确实没传 } // 判断是不是一个可计算的数字,NaN 也排除 if (typeof value !== 'number' || Number.isNaN(value)) { // 无法参与计算 }还有一类非常典型的场景:后端返回 0 分和 null 分,页面要求 0 分展示“0”,null 展示“暂无”。如果写成if (!score),0 分就会被误展示成“暂无”,后面 4.2 会专门展开说。
3.3 NaN:判断方式最特殊的值
NaN 的全称是 Not a Number,字面意思“不是数字”,但它的数据类型却偏偏是 number。更特殊的是,它是 JS 中唯一一个不等于自身的值:
NaN === NaN; // false所以value === NaN永远不成立,必须用专门的 API。很多人在这里踩过坑:明明想判断某个值是不是 NaN,写出来却永远不生效。
isNaN有两个版本,行为差异很大:
| 输入 | isNaN(x) | Number.isNaN(x) |
|---|---|---|
| NaN | true | true |
| 'abc' | true | false |
| undefined | true | false |
| 123 | false | false |
| '123' | false | false |
全局isNaN会先尝试把输入转成数字再判断,所以isNaN('abc')是 true,因为Number('abc')是 NaN;Number.isNaN不做类型转换,只有 value 本身就是 number 且值为 NaN 时才返回 true。实际开发中我更推荐Number.isNaN,语义更干净,不会因为隐式类型转换引入误判。
NaN 在生产环境最常见的来源有两个:Number('不是数字')和parseInt('abc'),解析失败都会返回 NaN。如果后续代码直接拿这个 NaN 去做展示或运算,页面就会出现刺眼的“NaN”。
3.4 false:它是有效业务信息,别随便当空处理
false 很特殊。它属于布尔类型的正常取值,在很多场景里代表“明确地否定了某件事”。举一个最常见的配置对象例子:
const env = { enableLog: false // 管理员明确关闭日志 };这里enableLog是 false,表示这是一个显式配置。如果前端用if (!env.enableLog)判断“日志功能没配置”,那 false 和 undefined 会一起命中。后面想区分“关闭了日志”和“没配置日志”这两类语义时,就会发现信息已经被抹掉了。在一些系统里这两种语义完全不同:字段没配置时后端走默认逻辑,配置成 false 时后端则要强制执行不开启。
因此我建议:
if (user.isVip === true) { // 明确是会员 } // 不要用 if (!user.isVip) // 否则 undefined 也会混进来,会员状态就被判错当然,如果产品上确实不关心 false 和 undefined 的区别,直接用!value也没问题。关键是你得清楚自己在做什么,并且确认业务能接受这种语义合并。
3.5 null 与 undefined:整个判空体系的“地基”
在业务上,null 和 undefined 几乎总会被当成“空”。两者的语义差别不大:
- undefined:声明了变量但没赋值,或者访问对象上不存在的属性。
- null:表示有意的空对象引用,很多后端接口用它表示“字段值为空”。
判断这两个值,有个非常省事的写法:
if (value == null) { // 同时覆盖 null 和 undefined }这算 JS 中少有的建议使用宽松相等的场景。== null只在左侧是 null 或 undefined 时才成立,不会对数字、字符串产生隐式转换。老手喜欢用它的原因很实际:写起来短、语义清晰、覆盖面完整。如果团队风格禁止==,那就等价写成:
if (value === null || value === undefined) { // 同上 }还有一个容易忽略的小坑:在数组或对象中做遍历过滤时,null 与 undefined 的展示行为不一样。比如用??时,两者都会触发默认值,而 0 不会:
[null, undefined, 0].map(item => item ?? '默认'); // ['默认', '默认', 0]这个行为在接口字段兜底场景里特别好用,下一章看实际应用。
4. 业务代码中最容易出错的组合场景
4.1 表单校验:输入框里的“0”到底算不算空
在表单场景中,很多人默认“用户没填写”就等于 0 或空字符串,但这里藏着一个隐藏陷阱:输入框拿到的值永远是字符串。用户在工号输入框里输入0,JS 里拿到的是'0'——一个字符串,而'0'是 truthy。所以直接写if (!value),反而不会把工号 0 过滤掉;真正的风险发生在把字符串转成数字之后。
比较典型的错误是这样:
const employeeId = Number(form.employeeId); if (!employeeId) { showError('工号不能为空'); return; }数字 0 在这段代码里会被拦下来。更麻烦的是,如果form.employeeId是空字符串,Number('')得到的结果不是 NaN,而是 0。这下原本的“空值”被隐式转换成了看似合法的 0,错误提示消失了,后端收到的却是 0。这类问题的解决思路是:把“转数字”和“判空”两个动作彻底分开。
const raw = form.employeeId.trim(); if (raw === '') { showError('工号不能为空'); return; } const employeeId = Number(raw); if (Number.isNaN(employeeId)) { showError('工号必须是数字'); return; }先确认原始字符串不为空,再处理数字转换,每一步的语义都清晰明了。
4.2 接口字段兜底展示:0 分要显示“0”,null 才显示“暂无”
这是我在后台管理项目里遇到最多的场景。后端返回一个学生成绩接口:
const apiData = { score: 0, // 真考了 0 分 rank: null, // 排名缺失 name: '张三' };页面要求:没有值时显示“暂无”。很多人顺手写成:
const scoreText = score || '暂无';问题立刻出现:0 分会被展示成“暂无”。学生考了零蛋,连分数都看不到,这不是给用户添堵吗?正确写法是用空值合并运算符:
const scoreText = score ?? '暂无';??只在左侧是 null 或 undefined 时取右侧值,0、false、'' 都会原样保留。我强烈建议把??纳入日常开发常规工具箱,而不是任何兜底都无脑用||。两者语义差异很大,||会覆盖所有 falsy 值,??只覆盖“不存在”的值。
4.3 计算链路中的 NaN 污染:一个 NaN 能带崩整条结果
NaN 真正的可怕之处在于传播性。任何一个 NaN 进入计算链,整个结果都会变成 NaN:
const price = Number(undefined); // NaN const count = 3; const total = price * count; // NaN页面最终显示一个巨大的“NaN”,用户看到后体验极差。这种问题的根源往往不是计算函数本身写错了,而是上游某个字段在转换时产生了 NaN,并且一路传播下来。排查思路应该是从源头控制:计算之前,把所有参与运算的字段统一做一次合法性清洗。
function toValidNumber(value, fallback = 0) { const num = Number(value); return Number.isNaN(num) ? fallback : num; }然后放心做乘法、加法。还要提醒一点:Number.isFinite比全局isFinite更严格,前者不做隐式类型转换,对字符串'123'返回 false;如果确实要兼容字符串数字,就先用Number()显式转换,再交给Number.isFinite判断。
5. 一套经过项目长期打磨的判空函数封装
5.1 设计目标与边界
把前面所有踩坑经验沉淀下来之后,我在团队里封装了一个isEmpty工具。设计阶段定了几个边界:
- 默认将 null、undefined、空字符串、纯空白字符串、NaN、空数组、空对象视为“空”。
- 0 和 false 默认不视为空,因为它们在业务里通常是合法值。
- 提供配置项,允许调用方按场景覆盖默认行为。
5.2 完整实现
function isNil(value) { return value === null || value === undefined; } function isBlank(value) { return typeof value === 'string' && value.trim() === ''; } function isEmpty(value, options = {}) { const { allowZero = false, allowFalse = false, ignoreTrim = false } = options; if (isNil(value)) return true; if (typeof value === 'string') { return ignoreTrim ? value === '' : value.trim() === ''; } if (typeof value === 'number') { if (Number.isNaN(value)) return true; if (value === 0 && !allowZero) return true; return false; } if (value === false && !allowFalse) return true; if (Array.isArray(value)) return value.length === 0; if (Object.prototype.toString.call(value) === '[object Object]') { return Object.keys(value).length === 0; } return false; }几个设计细节:
allowZero和allowFalse是最高频的两个配置项。工号、分数场景传{ allowZero: true },开关配置场景传{ allowFalse: true }。ignoreTrim很少用,只在确需要区分''和' '时打开,默认 trim。- 把空对象、空数组也纳入“空”,是表单联动场景里的刚需,虽然标题没细讲,但实际项目里你会感谢这个扩展。
5.3 实际使用示例
// A. 工号查询:0 是合法值,不能拦截 if (isEmpty(form.employeeId, { allowZero: true })) { return showToast('请输入工号'); } // B. 开关配置:false 是明确配置,不能当空 if (isEmpty(config.enableLog, { allowFalse: true })) { return loadDefaultConfig(); } // C. 搜索关键词:纯空格也算没填 if (isEmpty(keyword)) { return; } // D. 接口数据兜底:0 保留,null 展示占位符 const priceText = isEmpty(price, { allowZero: true }) ? '-' : price;这套函数从第一次踩坑到现在跑了两年多,覆盖了业务里绝大多数判空需求。核心价值不在于代码本身多复杂,而在于它逼着每个调用者思考两个问题:这里的 0 到底是合法值还是空?这里的 false 有没有业务含义?
6. 给后来者的一些排查建议
最后分享一点实战排查经验。如果线上报了一个疑似判空引发的问题,按照下面顺序排查会快很多:
- 别猜,先打印。在出问题分支的前后加
console.log(value, typeof value),看清这个值到底是什么类型、什么内容。 - 问业务。这个字段允许出现 0 吗?false 有意义吗?空字符串和纯空白字符串需要区分吗?
- 定位过滤点。全局搜索可疑的
if (!xxx)、xx || xx、xx ? xx : xx,逐个检查。 - 排查隐式转换。比如
Number('')变成 0、全局isNaN对字符串的强制转换、字符串和数字比较时的类型转换。 - 补测试。针对空值清单写一组边界用例:
null、undefined、''、' '、0、false、NaN,全部塞进工具函数,确保行为符合预期。
我个人的体会是:判空 bug 层出不穷,不是因为 JS 语法有多难,而是因为“空”这个字在不同业务场景里定义完全不同。把判断逻辑收敛到一个统一工具里,让团队成员建立共同默认值,比单靠个人记忆去规避要可靠得多。现在我看代码评审,凡是遇到if (!value),一定会顺势问一句:这个 value 会不会出现 0 或 false?很多时候,就是这一句提醒,就能避免下一条线上事故。