☰
JavaScript空值判断工具函数全解析:从isNil到isEmpty
2026/10/18 23:17:52 网站建设 项目流程

做个前端开发快十年,项目里几乎每一处地方都绕不开“这个值到底是不是空的”这种判断。你写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)只能判断 null;typeof value === 'undefined'才能判断 undefined;Array.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 为 false
value ?? default保留''保留 0保留 false
value || default使用 default使用 default使用 default
isNil(value) ? default : value保留''保留 0保留 false
isBlank(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判断。这个问题在评分、库存、开关状态这类场景里尤其突出。

假如一个评分组件,score=0表示“尚未评分”,后端返回 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判断“是否为空”,而是明确调用对应语义的工具函数。这套函数已经跟着我走过了好几个项目,现在分享出来,希望能帮你少踩一些坑。

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

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

立即咨询