JavaScript空值判断工具函数全解析:从isNil到isEmpty
做个前端开发快十年项目里几乎每一处地方都绕不开“这个值到底是不是空的”这种判断。你写if (!value)以为能挡住所有异常结果value 0或者value 0的时候照样翻车你写if (value || value null || value undefined)代码行数越来越长逻辑还很容易漏。JavaScript 的空值判断就是这种“看起来简单踩坑无数”的典型场景。今天我想把我自己沉淀的一套空值判断工具函数完整分享出来从设计思路到源码再到实际项目中的使用技巧一次性说清楚。这篇内容不追求炫技只希望你读完以后能直接把代码拿走并且知道每一步为什么要这么写。1. 为什么空值判断比想象中复杂1.1 到底什么是“空值”先别急着写代码得先厘清业务里说的“空值”到底指什么。不同语境下空值的语义完全不同。数据库里 NULL 表示未知接口返回里null和undefined都表示“没有数据”表单校验里空字符串和全空格通常也要算空数组空、对象属性缺失、Map/Set 没有元素这些都可能是“空”。把“为空”和“等于某个值”混为一谈是很多 bug 的根源。我看到很多人喜欢写if (!x)这是把值转成 bool 后判断。0、、NaN、null、undefined、false都会走进这个分支但业务上0和false往往是有意义的值。比如价格字段是 0开关状态是 false用!x判断就会直接误杀。所以第一步一定要把业务语义拆开而不是用一个表达式通吃所有场景。1.2 JavaScript 中“空”的特殊性JS 里null和undefined是两个不同东西。undefined表示变量声明了但没有赋值也代表对象属性不存在null通常表示开发者主动把变量置为空。这俩在下相等在下不相等。所以很多老代码写value null其实是在同时判断 null 和 undefined这也是多快好省的写法。更麻烦的是typeof null返回object这个是历史遗留问题。再加上NaN虽然不算“空”但它不等于任何值包括自己[]是空数组但不是空{}是空对象但不是空。JavaScript 本身没有一个原生的isEmpty方法所以工具函数才有存在的必要。也正因为这些历史包袱自定义空值判断时不能想当然地依赖 JS 内建规则。1.3 自带的判断为什么不够用if (value null)只能判断 nulltypeof value undefined才能判断 undefinedArray.isArray(arr) arr.length 0判断空数组Object.keys(obj).length 0判断空对象。每个都单独写不仅啰嗦而且容易漏。比如你封装一个函数参数可能来自第三方接口类型完全不确定只用if (!value)会导致0被判为空只用if (value undefined)又漏掉null想判断空字符串还得想着trim之后是不是空白。所以一套统一的工具函数是为了把“判断语义”从业务代码里抽出来让调用方表达意图而不是每处都重复摸石头过河。2. 设计工具函数前先想清楚这些事2.1 明确空值的业务语义在设计之前建议先把项目里的“空”分门别类至少要分三层缺省值只有null、undefined空白值包括空字符串、空白字符串空集合数组长度 0、对象无 key、Map/Set 无元素我自己的习惯是提供多个精细函数而不是一个大而全的isEmpty因为业务在不同的位置需要不同的判断粒度。比如接口字段缺失用isNil表单输入校验用isBlank列表渲染前判断用isEmpty。这样在调用处看函数名就知道判断的是什么。语义可能包含的值建议函数缺省null, undefinedisNil/isNull/isUndefined空白, ,\n\t, 也可能包括 null/undefinedisBlank空集合[],{},new Map(),new Set()null/undefined 默认也认为空isEmpty无意义数字NaNNumber.isNaN单独处理这个表不是标准而是一个便于实践的起点。团队里如果已经有约定以约定为准。2.2 命名与返回值约定命名要一看就懂。我用isNil是否 null/undefined、isNull是否 null、isUndefined是否 undefined、isBlank是否空白字符串、isEmpty广义空、isEmptyObject是否空对象、isDeepEmpty深层空。全部返回布尔值。布尔值的好处是可以直接组合比如if (isNil(user) || isBlank(user.name)) { // 处理用户缺失或用户名为空 }返回值保持纯布尔还有一个好处当它被用在filter、every这类高阶函数里时不会因为返回了0或这种假值而产生额外的边界问题。如果你的工具函数返回了“空字符串还是 null”那这个函数的语义就模糊了调用方还得再判断一层。2.3 用 TypeScript 做类型收窄如果是 TypeScript 项目最好的写法是给每个函数加上类型谓词。比如isNil(value: unknown): value is null | undefined调用之后 TypeScript 就能自动收窄类型后面不用再做重复判断。function isNil(value: unknown): value is null | undefined { return value null || typeof value undefined; } // 使用 const data: unknown getData(); if (isNil(data)) { // 这里 data 的类型已收窄为 null | undefined return; } // 这里 data 仍然是 unknown但编译器知道它不是 null/undefined如果没有类型谓词在严格模式下你判断完if (value null)之后访问属性编译器可能仍然报错。类型谓词是工具函数比“随手写字面量判断”更有优势的地方之一。2.4 保持纯函数和容错工具函数应该是纯函数不修改传入参数不抛异常除非参数本身无法判断。对所有未知类型都能返回合理结果才是好的实现。比如在isEmpty里第一步判断 null/undefined避免后续去读 null 的属性第二步对字符串做 typeof 判断避免把数字直接调trim报错。我见过一些封装一上来就用Object.keys(value)结果传入 null 直接 TypeError。这种工具函数装进项目后不仅没有解决问题还要花更多精力排查。所以实现时“无脑容错”是很重要的别假设调用方一定会规规矩矩传参。3. 核心实现一组可用的空值判断工具函数3.1 isNil 与 null/undefined 的精确判断先写最基本的isNil它用来判断缺省值function isNil(value) { return value null || typeof value undefined; }你还可以用简洁写法const isNil (value) value null;value null只在 value 为 null 或 undefined 时为 true因为 JS 中 null 和 undefined 在宽松相等下互相相等。但它容易让不熟悉的人产生迷惑代码评审时也容易被质疑。我的建议是如果你追求可读性就用第一种如果团队接受用第二种也完全没问题。isNull和isUndefined更简单function isNull(value) { return value null; } function isUndefined(value) { return typeof value undefined; }为什么isUndefined不写value undefined因为undefined在局部作用域存在被覆盖的可能虽然现在极其少见但用typeof更稳。这个函数会常配合isNil使用用来区分一个人是“主动清空”还是“从来没设置过”。3.2 isEmpty判断广义“空集合”设计isEmpty时我采用的策略是null和undefined都视为空字符串经过trim后长度 0 视为空数组长度为 0 视为空对象无自身可枚举属性视为空Map/Set 的 size 为 0 视为空其他情况一律返回 false如0、false、NaN。function isEmpty(value) { if (isNil(value)) return true; if (typeof value string) return value.trim().length 0; if (Array.isArray(value)) return value.length 0; if (value instanceof Map || value instanceof Set) return value.size 0; if (Object.prototype.toString.call(value) [object Object]) { return Object.keys(value).length 0; } return false; }这里有几个细节需要解释。为什么要用Object.prototype.toString.call(value)因为typeof {}和typeof []都是object需要区分普通对象和数组。Array 已经用Array.isArray判断过了剩下的 object 还要排除 Date、RegExp 这类特殊对象。Object.prototype.toString.call(new Date())返回[object Date]不是[object Object]所以这些非空对象不会走进后面的逻辑最终返回 false这是合理行为。Object.keys只统计可枚举字符串属性。Symbol 属性不会被统计到但常规业务里 Symbol 作为对象 key 的场景很少。如果你确实需要统计可以用Reflect.ownKeys。另外如果一个对象使用Object.defineProperty设置了enumerable: false的属性也会被Object.keys忽略。比如Object.create(null)创建的原型为 null 的对象用instanceof Object会出问题但Object.prototype.toString.call仍然可以判断所以这类对象也能被正确处理。3.3 isBlank专门判断“空白字符串”表单场景最常见的问题是用户输入了一堆空格。判断逻辑建议统一function isBlank(value) { if (typeof value string) { return value.trim().length 0; } return isNil(value); }非字符串传入时怎么处理我选择把 null/undefined 视为 blank数字和布尔值视为非空白。因为0和false不应该被当作空输入。但如果你在某个业务里希望把 0 也当作“无效输入”那可以单独封装函数不要污染通用的isBlank。这里还有一个小坑String.prototype.trim去除的是WhiteSpace和LineTerminator包括空格、Tab、换行等。全角空格也在其中吗不一定。\u3000这种全角空格会被 trim 掉但一些特殊 Unicode 空格比如\u00A0不间断空格老版本环境可能不会处理干净。遇到国际化输入时如果想更严格可以用正则value.replace(/\s/g, ).length 0但这种需求比较少见普通表单用trim就够了。3.4 isEmptyObject 和深层空对象检测空对象判断function isEmptyObject(value) { if (value null || value undefined) return false; if (Object.prototype.toString.call(value) ! [object Object]) return false; return Object.keys(value).length 0; }注意这里和isEmpty的差别isEmpty把 null/undefined 当空因为业务语义是“没有内容”isEmptyObject把 null/undefined 返回 false因为调用方想判断“是不是一个空对象”。如果这里也返回 true就会导致“没有对象”和“有对象但没 key”混为一谈在条件判断里造成很难排查的 bug。深层空对象可能更复杂比如{ a: {}, b: [] }从 JSON 角度看是空值集合但Object.keys长度不是 0。这时候需要递归function isDeepEmpty(value, seen new WeakSet()) { if (isNil(value)) return true; if (typeof value ! object) { if (typeof value string) return value.trim().length 0; return false; } if (seen.has(value)) return true; seen.add(value); if (Array.isArray(value)) { return value.every((item) isDeepEmpty(item, seen)); } if (value instanceof Map) { return Array.from(value.values()).every((item) isDeepEmpty(item, seen)); } if (value instanceof Set) { return Array.from(value.values()).every((item) isDeepEmpty(item, seen)); } const keys Object.keys(value); if (keys.length 0) return true; return keys.every((key) isDeepEmpty(value[key], seen)); }这里用WeakSet防止循环引用。如果对象里有a.self a没有 seen 会无限递归。WeakSet 不会阻止垃圾回收所以不用担心内存泄漏。不过isDeepEmpty是比较重的工具我一般只在处理后端返回的大型嵌套配置时使用正常情况下isEmpty就够用了。性能敏感场景也要谨慎深层递归可能导致调用栈过深。3.5 源码整合与用法示例把上面函数集中成一个模块可以这样导出// empty.js export function isNil(value) { ... } export function isNull(value) { ... } export function isUndefined(value) { ... } export function isBlank(value) { ... } export function isEmpty(value) { ... } export function isEmptyObject(value) { ... } export function isDeepEmpty(value, seen new WeakSet()) { ... }使用示例import { isEmpty, isBlank } from ./empty.js; function renderList(data) { if (isEmpty(data?.items)) { return 暂无数据; } return data.items.map(...); } function validateForm(form) { if (isBlank(form.username)) { return 用户名不能为空; } return null; }这段代码能直接用到项目里但实际情况中建议根据团队约定裁剪。不是所有项目都需要isDeepEmpty也不是所有项目都把字符串 trim 之后当成空。工具函数的价值在于统一判断策略因此进项目之后第一件事就是和团队对齐语义。别上来就塞一整套函数有时候一个isNil加一个isBlank已经很够用了。4. 实战场景在哪些地方用得上4.1 接口数据容错前后端联调时接口返回的数据经常缺失。比如详情接口可能返回{ user: null, list: undefined }如果直接访问res.user.name直接报错。封装空值判断后可以先判断const user res?.user; if (isNil(user)) { // 走默认值或错误提示 }可选链?.只能避免访问报错但不能替代“是否为空”的判断。所以工具函数和可选链是互补的。使用res?.user ?? defaultUser的时候要理解??只处理 null/undefined不会处理空字符串、0。如果你的业务里空字符串也想用默认值那就得先isBlank再赋值。我实际处理过一个案例后端返回用户列表时某个字段缺省是 null当列表没有数据时返回null而不是[]。如果不做处理前端直接遍历会报错。用isEmpty(data?.list)就能一次性兼容 null、undefined 和空数组渲染空状态。4.2 表单校验表单里最容易出的问题用户输入半角空格if (username ! )判断不出来原生 input 没填时值是空字符串但有些组件会转成 null。统一用isBlank代码会稳定很多。比如if (isBlank(values.username)) { errors.username 请输入用户名; }这在 PC 端表单和移动端表单都适用。尤其做动态表单时校验器经常要检查一个字段“是否存在、是否为空”用函数表达会比手写条件清晰得多。遇到富文本编辑器或 textarea用户可能输入换行或连续空格isBlank能够识别出“看起来有内容其实没有实际内容”的状态。4.3 配置参数默认值模块化项目里配置对象可能来自默认配置和用户配置合并。判断某个字段有没有被用户覆盖可以用isNil因为用户显式设为 null 表示“想清空”设为 undefined 表示“没设置”两者含义不同。但只判断 null/undefined空字符串和 0 都是有效配置。比如const pageSize isNil(config.pageSize) ? DEFAULT_PAGE_SIZE : config.pageSize;这里如果用户设置pageSize: 0就不该走默认值因为 0 可能是一个合法配置。用||写config.pageSize || 10会直接把 0 吃掉这是最常见的 bug 之一。我顺手对比一下几个等价写法的差异表达式当 value 为当 value 为 0当 value 为 falsevalue ?? default保留保留 0保留 falsevalue || default使用 default使用 default使用 defaultisNil(value) ? default : value保留保留 0保留 falseisBlank(value) ? default : value使用 default保留 0保留 false所以选哪种完全取决于业务里哪些值无效。用工具函数的好处是这个“语义判断”可以在函数里统一维护而不是散落在几十处三元表达式里。4.4 通用组件 props 校验写通用组件时父组件可能传undefined或null表示不传。比如一个头像组件src为空应该显示占位图const Avatar ({ src, fallback }) { if (isBlank(src)) { return Placeholder /; } return img src{src} /; };如果不做空值判断直接渲染img src会显示裂图。这类函数在组件库开发里就是基础设施。再比如表格组件列配置里的render函数可能没传判断时用isNil(column.render)而不是if (!column.render)因为用户传一个空函数() {}也是函数有明确语义不该被当成没传。5. 常见坑与排查技巧5.1 typeof null 的历史包袱typeof null object这是 ES 规范里改不掉的“设计缺陷”。所以判断 null 只能value null或者value null。有个技巧想判断一个变量是不是对象时不能只写typeof value object因为 null 也会通过必须加上value ! null。这也是为什么我在工具函数里把 null 的判断放最前面。如果你发现某个工具函数在传 null 时返回了奇怪的值先去看是不是用typeof或Object.keys直接处理了。空值判断最怕不假思索地依赖 typeof这是新手最容易踩的坑。5.2 0、false、NaN 是“假”但不是“空”JavaScript 的 falsy 列表里有false、0、-0、0n、、null、undefined、NaN。用if (!value)会命中所有 falsy 值但日常业务中 0 和 false 几乎都是有效值。排查线上 bug 时如果发现传了数字 0 却走了空分支十有八九是用了!value判断。这个问题在评分、库存、开关状态这类场景里尤其突出。假如一个评分组件score0表示“尚未评分”后端返回 0 时前端却显示“无数据”。这里0明明是有意义的状态但如果用了if (!score)就会把 0 和 null 混在一起。同理假值不只是“空”的别名这一点需要刻在脑子里。5.3 NaN 怎么判断NaN 不是空但经常因为数学计算产生。全局isNaN会把非数字也判成 true比如isNaN(abc)返回 true因为它在比较前会做 Number 转换。建议使用Number.isNaN(value)。Number.isNaN(NaN); // true Number.isNaN(abc); // false如果项目里有“无意义数字”的判断可以单独封装isInvalidNumber不要混进空值工具函数。实际例子接口返回n: null表示不参与计算返回 0 表示数量为 0如果计算前if (!n)就把 0 也过滤了导致数量 0 时取不到正确结果。正确做法是先用isNil判断是否缺失缺失时跳过计算再对数字单独校验。5.4 用 JSON.stringify 判断空对象小心几个大坑有人喜欢写JSON.stringify(obj) {}判断空对象这在简单对象上可行但有几个坑值为 undefined、函数、Symbol 的属性会被忽略JSON.stringify({a: undefined})得到{}会被误判为空出现 BigInt 时直接抛异常循环引用也会抛异常。所以判断空对象优先使用Object.keys。const obj { a: undefined }; JSON.stringify(obj) {}; // true但 obj 明明有一个 key a Object.keys(obj).length 0; // false如果你的对象带了不可枚举属性还要用Object.getOwnPropertyNames或Reflect.ownKeys但一般业务场景Object.keys足够了。这个坑在数据序列化前后对比时尤其常见检查“两个对象是否相同”不要依赖 JSON.stringify空值判断同样不要依赖它。5.5 用可选链和空值合并运算符简化代码现代 JavaScript 完全可以用?.和??减少判断层级。比如user?.address?.city不会因为中间空值报错value ?? 默认值只在 null/undefined 时生效。但注意??不能和||直接混用否则会语法错误。很多人在一个表达式里想既兼容空字符串又兼容 null结果写成value || 默认值虽然语法没错语义却变成了 falsy 就用默认值不是想要的。日常排查时我常做的一件事是先在浏览器里跑一遍工具函数把所有可疑值列出来看结果[null, undefined, , , 0, false, NaN, [], {}, new Set(), new Map()].forEach((v) { console.log(v, isEmpty(v), isBlank(v)); });这样能最快发现约定和业务语义之间的偏差。每次新项目引入工具函数我都建议把它当“契约”看待而不是随便粘一段代码。空值判断虽然简单但它能影响整个系统的数据边界值得多花一点时间设计好。个人经验上我第一次在项目里系统地替换空值判断时改掉了几十处if (!data)导致评分字段 0 分显示异常的问题。从那以后我再也不在新代码里用!value判断“是否为空”而是明确调用对应语义的工具函数。这套函数已经跟着我走过了好几个项目现在分享出来希望能帮你少踩一些坑。