likes
comments
collection
share

《ES6 奇葩说》:我们为什么需要undefined?

作者站长头像
站长
· 阅读数 15

某前端的知识图谱(省流版)

魔改 from 《某科学的超电磁炮》@镰池和马

重铸前端霸权,吾辈义不容辞!

Halo Word!大家好,我是大家的林语冰(挨踢版)~

上期《ES6 奇葩说》我们共享了undefined 的历史包袱》,关注点在于被嫌弃的 undefined 的前世今生。

今天的前端圆桌我们来伪科普一下下 undefined 的设计动机/编程意图。

本期语冰会共享若干前端脑洞,包括但不限于——

  1. undefined 的设计动机是什么?
  2. undefined 指“未定义”还是“未赋值”?
  3. 使用 undefined 的最佳 timing 是什么时候?
  4. 懂得都懂,不懂关注,日后再说~

不为谁而作的值

魔改 from《不为谁而作的歌》@林俊杰

我们在undefined 的历史包袱》中曾经说过——

“ES3 之前没有 undefined 全局属性,但已经存在 undefined 原始值。”

虽然但是,不知道大家有没有想入非非——

Undefined 数据类型和对应的 undefined 原始值的设计动机是什么?

语冰有一个大胆的假说——

undefined 是 JS(JavaScript)最抽象的“元值(metavalue)。

遇事不决,形而上学。所谓“元”指的是——描述 XX 的 XX。

举个粒子,我们尝试解读几个“元概念”。

  • 元宇宙——描述宇宙的宇宙
  • 元编程——描述编程的编程
  • 元数据——描述数据的数据
  • 其他三连可观看内容......

猫眼可见,undefined 作为元值,可以理解为“无中生有”的“无”——即“描述(其他)值的值”。

前端人都知道,JS 有一个都市传说——万物皆对象。

倘若 JS 真的是万物皆对象,那 null 作为空值其实是恰到好处的,因为祂可以表示“此处无对象”,作为一个万能的空值存在。

虽然但是,事实上 JS 并不是万物皆对象。

你知道的,JS 中有两大类数据类型——

  • 对象类型(Object,AKA“引用类型/复合类型”)
  • 非对象类型——原始类型(Primitive,AKA“基本类型”)

变量从创建到销毁的过程,我们称之为变量的生命周期(Lifecycle)。

当我们创建一个变量时,实际上我们对该变量的类型和值进行了初始化。

举个粒子,我们来比较一下下对象类型和原始类型。 《ES6 奇葩说》:我们为什么需要undefined? 猫眼可见,对象类型和原始类型的操作是对称的——都可以兼容初始化和重置操作。

虽然但是,对于销毁操作而言,对象类型和原始类型就并非“众生平等”了。

对象可以通过对象的空值 null 来销毁,而原始值的“空值”是什么呢?

为了让对象类型和原始类型的操作对称,我们需要 DIY 一个原始值的“空值”——“元原语(Metaprimitive)”。

是的没错,你猜对了,这个元原语就是 Undefined 原始类型的唯一值——undefined 原始值。

虽然但是,实际上 undefined 并不只是原始类型的“空值”,祂是所有数据类型最底层的“无值”。

《Speaking JavaScript: An In-Depth Guide for Programmers》曾经说过——

undefined 有时表示不存在的“元值”。反而言之,null 表示“空值”。”

换而言之,undefined 能够兼容原始类型和对象类型,祂不仅是“元原语”,还是更加抽象的“元值”。

事实上,倘若你愿意,你可以使用 undefined 来表达销毁对象的语义。

虽然但是,出于“类型收窄”和最小授权原则(POLP,AKA“最小权限原则”)考虑,一般我们还是建议使用 null 来表示空对象。

《JavaScript: The First 20 Years》曾经说过——

“其实也可以说这与 Java 保持了一致,因为 Java 的所有值都是对象,而 null 本质上是表达没有对象的对象。”

所以优先使用 null 表示空对象可读性更佳。

再者说,在undefined 的历史包袱》中,我们也证明了 undefined 的本质是一个不鲁棒的标识符,使用 null 来表示空对象也更符合“KISS 原则(Keep it Simple & Stupid)”。

BTW,nullundefined 的强制类型转换被设计为结果一龙一猪,这对于早期 JS 的异常处理更友好。 《ES6 奇葩说》:我们为什么需要undefined? 猫眼可见,两者强制类型转换为数字结果一龙一猪——null“数字化”为 0,而 undefined“数字化”为 NaN

关于 undefined 的设计动机,《Speaking JavaScript: An In-Depth Guide for Programmers》曾经说过——

  • 这个值不应该是引用(类型)的含义,因为祂不仅用于表示对象类型
  • 这个值不应该被转换为 0,因为这样错误更难被发现

由于借鉴 Java 的 null 设计并不足以满足上述条件,于是乎,“JS 之父”B.E. 就决定集成 undefined 原始值。

综上所述,语冰的个人心证是——

undefined 是 JS 最抽象的元值,祂使语言类型系统更加完备,对异常处理更友好。

看不见的缺省值

魔改 from《看不见的客人》@奥里奥尔·保罗

前端人都知道,undefined 一般被翻译为“未定义”,原则上直译合法,但是意译并不合理。

虽然但是,不知道大家有没有想入非非——

undefined 应该理解为“未定义”还是“未赋值”呢?

语冰有一个大胆的假说——

undefined 应该理解为“未赋值”。

你知道的,如何定义“定义”本身就很暧昧。定义是指“未声明”还是指“已声明”呢?

前端人都知道,undefined is not "not defined".(“未定义”不是“未定义”。)

undefined 常常和 undeclared 混淆。

举个粒子,当我们尝试读写一个未声明的变量时,就会抛出异常。 《ES6 奇葩说》:我们为什么需要undefined? 猫眼可见,“not defined”的异常很容易让新手感到困惑。

undefinedundeclared
not assignednot defined
已声明,未赋值未声明/未定义
默认初始化为 undefined 原始值抛出 ReferenceError 异常

综上所述,倘若你对“未定义”感到困惑,你可以考虑将 undefined 理解为“未赋值”时的缺省值。

所谓缺省值指的是——可以缺少/省略的值。

举个粒子,下列情况会自动初始化 undefined 原始值作为缺省值,包括但不限于——

undefined 的“隐性性状”示例 demo
变量已声明未赋值let VOID
缺省解构赋值let [VOID] = []
函数冗余参数(无实参)void (VOID => {})()
函数(隐式)无返回值let VOID = (() => {return})()
void 操作符返回值let VOID = void 0
构造函数非构造调用new.target
对象不存在的属性let VOID = {}.girlFriend
?. 操作符短路返回值let VOID = null?._
其他粉丝专属福利......

猫眼可见,上述情况都会自动使用 undefined 原始值作为缺省值。

综上所述,语冰的个人心证是——

undefined 应该理解为“未赋值”——写做 undefined,读做“未赋值”。

undefined 的悖论

魔改 from《选择的悖论》@巴里·施瓦茨

前端人都知道,undefined 是一个元值,原则上允许祂作为 JS 所有类型的合法初始值。

虽然但是,不知道大家有没有想入非非——

使用 undefined 的最佳 timing 是什么时候?

语冰有一个大胆的假说——

使用 undefined 的最佳方式是不使用祂。

你知道的,我们接触的大多数资料会告诉我们 undefined 的意义是“未定义/无值”。

虽然但是,准确而无用的观念,终究还是无用的。

私以为这种描述太过普世,并不算实际的指导方针/使用指南。

举个粒子,因为太麻烦就全写 undefined 了,万物皆可 undefined《ES6 奇葩说》:我们为什么需要undefined? 猫眼可见,undefined 的无能在于祂无所不能,原则上允许我们可以无脑地使用 undefined 初始化任何变量。

之所以出现这种情况,首先是因为 undefined 本身就是足够抽象的元值,其次是因为 JS 是动态弱类型语言。

换而言之,JS 原则上允许黑客动态地魔改变量的类型和值。

虽然但是,绝对的权力导致绝对的腐败。同理,绝对的动态导致绝对的变态。

动态语言的优势在于祂的精简与灵活,但并不意味着黑客也要使用动态的思维/方式编程。

技术是手段,而不是目的。同理,动态语言只是工具/手段,而不是目的。

倘若一个变量的类型和值已经确定,那么再次为所欲为地修改其类型是不科学的。

举个粒子,动态的坏设计可能降低代码的可读性。 《ES6 奇葩说》:我们为什么需要undefined? 猫眼可见,上述代码是合法的的,变量在不同的类型反复横跳,JS 运行时并不会抛出异常。

虽然但是,“程序正义”不代表“结果正义”。字符串的 cat 变量原本表达的是猫猫的名字“薛定谔”,之后却被动态赋值为数字。

这种灵活性赋能的实现会让读代码的人感到困惑,因为语义不够明确——9 是 9 只猫还是 9 岁?

其次逻辑也很奇怪,字符串名字突变为数字类型,是不是变量因为不合理的操作被污染了?

你知道的,JS 是动态弱类型语言,但并不是无类型语言,然而我们很多时候偏偏对类型视而不见。

最极端的方式是声明一个变量,然后该变量被模块所有方法共享,这种情况下模块所有方法都有权读写该变量,同时该变量的类型也是动态的、不可预测的。表面上看,好像只用一个变量节省了内存空间,实际上代码已经变成了一坨 bullshit。

代码首先是给人看的,而不是给机器。人读代码的时长远大于写代码的时长,因为代码可被共享,读代码的人远多于写代码的人。

这种一处定义、到处使用的变量是不合理的,语义化自然不好,同时也违背了最小授权原则和 KISS 原则等,是不优雅、不鲁棒的编程习惯。

事实上,当变量的类型和值确定之后,我们都会尽可能地保证变量的类型不变,即使重写也可以赋值为同款类型的其他值。

举个粒子,用同款类型的其他值重写变量。 《ES6 奇葩说》:我们为什么需要undefined? 猫眼可见,我们保持变量的类型不变,可以保证变量的语义不会失真,从而引起歧义。

对于原始类型,譬如说 Number 类型的变量,我们可以初始化为 00 在数学里本就是特殊的存在,虽然罗马数字没有 0。

譬如说 String 类型的变量,我们可以初始化为 '' 空字符串等,也可以初始化为需求文档/接口契约的规定值。

对于对象数据类型,我们可以初始化为特定实例对象,也可以使用 null 清空。

你知道的,大多数情况下我们不需要和 undefined 贴贴,这样代码的可读性和变量的语义是浅显易懂的。

我们知道无论对象类型还是原始类型的值都有对应的值可以初始化/重置,那这样我们不就完全不需要 undefined 原始值了吗?

大多数情况下我们确实不需要 undefined。变量的名字原本就包含特定的编程意图/设计动机,按照需求 pick 类型是正确的选择。

原则上变量的类型就不应该被随意修改。undefined 作为“未定义/无值”的特殊值,只有在编程意图不明确的情况下才可能需要使用祂。

举个粒子,当我们对变量的类型和值不确定时,我们可以考虑使用 undefined 初始化。 《ES6 奇葩说》:我们为什么需要undefined? 猫眼可见,我们并不明确服务端的返回结果是什么,或者我们需要兼容两种类型的数据,这时候我们找不到恰如其分的初始值,此时就可以使用 undefined

BTW,事实上实际开发中服务端的数据也是约定好的,而且接口大多也是对称的,结果更可能被初始化为对应接口的实例,此处只是举个粒子。

若要己无值,除非人不知。undefined 的正确打开方式就好像数学里的“未知数 x”,而不是数学里的 0。

倘若有可能,我们建议尽快、尽可能地确定变量的类型和值,避免万物皆可 undefined,这样的代码语义更清晰,变量名与变量值能名副其实。

私以为 undefined 的正确打开方式是一个“无状态的占位符”,之所以强调“无状态”,是为了区分“无值”的动态编程思维。

此处“无状态”强调的是——也无类型也无值(也无风雨也无晴)。

换而言之,只有当你同时不确定变量的类型和值的时候,你才考虑使用 undefined 初始化,DIY 一个占位符先占着位置,待会再按需赋值。

只要有可能,我们建议大家在 DIY 变量的时候尽快决定变量的类型和初始值,避免“万物皆可 undefined”,编写语义化的可读代码。

综上所述,语冰的个人心证是——

使用 undefined 的最佳方式是不使用祂,因为 undefined 的无能在于祂无所不能。

Before U Go

魔改 form《Before You Go》@Lewis Capaldi

本期的《ES6 奇葩说》就讲到这里了,希望对你有所启发。

感兴趣的同好可以订阅关注和三连催更,也欢迎大家在公屏自由言论。

吾乃前端的虔信徒,传播 BUG 的福音。

我是大家的林语冰,我们一期一会,不散不见~

免责声明——

本文示例 demo 默认均为 ESM(ECMAScript Module)筑基测评,

因为现代化前端开发相对推荐集成 ESM。

edge cases(边界情况)的解释权归语冰所有。

前端禁书目录

魔改 from《魔法禁书目录》@镰池和马