February 22, 2024

JavaScript 中的 undefined、不存在、delete

TL; DR

在 JavaScript 中不能通过将对象的属性设置成 undefined 来删除属性。

反直觉的是,有的全局变量可以用 delete 来删除,有的属性不能用 delete 来删除。换句话说,var obj = 0globalThis.obj = 0 效果上不等价,即使在 99% 的情景中是一样的。

与此同时:

所有属性都是平等的,但是有些属性比其他的属性更平等。

启发背景

若获取一个存在的对象中不存在的属性(或者可以视作 key),将会得到 undefined

> const obj = {}
undefined
> obj.name
undefined

若将变量或者属性赋值为 undefined,获取时显然也会得到 undefined。那么,是否可以通过将属性赋值为 undefined 来删除某个属性呢?

初步实践

在 JavaScript 中可以通过 delete 来删除一个属性;例如:

> const obj = { name: "admin" }
undefined
> delete obj.name
true
> Object.keys(obj)
[]

若将 name 设置为 undefined,则:

> obj.name = undefined
undefined
> Object.keys(obj)
[ 'name' ]

可见,后面这种方式并不能删除属性。

进一步讨论

虽然在很多情况下,如果把该对象视作 Java、C# 等语言中的对象来使用时,这两种做法似乎并没有什么区别。但是,在 JavaScript、Python 等语言中,对象可以用作哈希表,而且在解决一些高级需求的时候会「反射」「改造」对象;这时,delete 将不能被赋值为 undefined 取代,即使 delete 这种语言特性的设计给人一种「狗皮药膏」的感觉。

事实上,「获取不存在的属性得到 undefined」显然是 JavaScript 设计中的一个败笔,不过因为历史遗留原因已经无法改正。更好的做法应该是像 Python 一样抛出异常,并且提供一个方便检查属性是否存在的 API。

更糟糕的是,JavaScript 并没有把局部变量和属性在这方面一视同仁。如果获取不存在的变量,将会抛出异常;例如:

> console.log(sakana)
Uncaught ReferenceError: sakana is not defined

而且变量也不能通过 delete 删除;例如:

> const sakana = 0
undefined
> delete sakana
false
> sakana
0

进一步,全局变量也不可以被 delete,即使全局变量本质上是 window 或者 globalThis 对象的属性;例如:

> var obj = 0
undefined
> globalThis.obj
0
> delete obj
false
> delete globalThis.obj
false

但是,如果通过往 windowglobalThis 添加属性来声明全局变量,则可以被 delete 删除;例如:

> globalThis.obj = 0
0
> obj
0
> delete obj
true

那么,如果我们用 var 声明一个 windowglobalThis 中已经存在的全局变量呢?如果按照之前发现的规律,这个全局变量应该是不能被删除的。但是,现实真的如此吗?

> globalThis.fetch
[Function: fetch]
> var fetch = 1
undefined
> globalThis.fetch
1
> delete fetch
true
> globalThis.fetch
undefined

无语凝噎。

陈词滥调

在 ES5 以及更早的版本中,undefined 是一个变量,可以被更改。因此,如果开发者确实需要用到 undefined 的时候,会采用 void 0 这个写法。此外,由于 void 0 相比 undefined 占用更少的字符,所以很多代码最小化工具会将 undefined 最小化为 void 0

结语

不愧是 JavaScript,到处都是狗皮药膏和不一致性的表现,新特性为了兼容性只能为以前的不良设计擦屁股。

rewired (c) 2024