《ES6 奇葩说》:undefined 的历史包袱

语言: CN / TW / HK

theme: cyanosis

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

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

今天的前端圆桌我们来讨论一下下 JS(JavaScript)中一个平平无奇的奇葩属性——undefined 以及祂的历史包袱。

某前端的公测挑战(省流版)

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

那我们先带若干脑洞急转弯热热身,包括但不限于——

  1. undefined 是隐式全局变量吗?
  2. undefined 恒等于 undefined 原始值吗?
  3. 为什么要用 void 0 打败 undefined
  4. undefined 为什么不是字面量?
  5. undefined 的本质是什么?
  6. 懂得都懂,不懂关注,日后再说~

免责声明——

大家可以当做面试题去刻意练习/自我增殖,但不要有“挨踢蕉绿”/心智负担,因为语冰并不是再面试你们。

不被定义的 undefined

魔改 from《不被定义的女性》@马晓韵

MDN 文档曾经说过——

undefined 是全局对象的属性——即全局作用域的变量。

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

undefined 是隐式全局变量吗?

正所谓“大胆假射,小心球证”,语冰有一个大胆的假说——

undefined 不是隐式全局变量。

那什么是隐式全局变量呢?

所谓“隐式全局变量(Implicit Globals)”指的是——

在“躺平模式(Sloppy Mode,AKA 草率模式)”下,未声明但被赋值的变量(非限定标识符)会被赋能为全局对象的新生属性,继续依法卡 BUG。

举个粒子,当我们尝试给未声明的变量赋值时,祂就变身成为了隐式全局变量。 隐式全局变量 猫眼可见,隐式全局变量没有通过 var/let 等语法显式声明就能读写,且被赋能为全局对象的新生属性。

不知道大家有没有用过 globalThis.undefined

表面上看,undefined 全局变量和隐式全局变量好像大同小异。

大家可能没有用过 globalThis.undefined,但是你可能没有意识到——其实你的潜意识一直再偷偷白嫖祂。

举个粒子,当我们尝试读写 undefined 时,实际上我们默认读写的是 globalThis.undefinedglobalThis.undefined 猫眼可见,这波操作不能说是一龙一猪,只能说是一毛一样。

这是因为——变量直接赋值为 undefined 时,祂会根据“就近原则”疯狂试探,优先在当前作用域得寸则寸,若求之不得则逆袭上游作用域链上溯扫描,直到作用域的“天花板”——全局作用域,默认读取到 globalThis.undefined,该属性恒等于 undefined 原始值。

虽然但是,undefined 并不是隐式全局变量,而是标配全局属性。

所谓“标配(built-in,AKA“标准内置(的)”)”指的是——JS 运行时标准内置的默认出厂设置。

你知道的,一般变量需要先声明后读写,But 我们不需要手动 DIY undefined 变量,就能免费白嫖,直接读写祂。

标配全局属性就像人先天赋能的超能力,譬如说人都有眼睛作为默认出厂设置。

隐式全局变量则不是,祂是后天赋能的,祂不只是眼镜,更像是隐形眼镜。

就鲁棒性(robustness,AKA“稳健性/健壮性”)而言,隐式全局变量类似于“RAM(随机存取存储器)”,标配全局属性则类似于“ROM(只读存储器)”。

换而言之,隐式全局变量是不鲁棒的“佛系属性”——祂默认是可重写、可枚举、可配置的。

undefined 则是鲁棒的只读属性——祂先天免疫若干“阴间操作”。

举个粒子,当我们尝试重写 undefined 时,程序就会抛出异常。 全局只读属性 猫眼可见,试试就逝世,undefined 的内心毫无波动。

举个粒子,当我们尝试证明 undefined 的鲁棒性时,祂就被实锤了。 undefined的鲁棒性 猫眼可见,undefined 是一个“不可失忆”的属性——不可重写、不可枚举、不可配置。

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

undefined 不是不鲁棒的隐式全局变量,而是不被定义的标配全局属性。

虽然但是,表面鲁棒的 undefined 还有看不见的 BUG 魔鬼在细节(下一节)。

你的 undefined 不是你的 undefined

魔改 from《你的孩子不是你的孩子》@吴晓乐

前端人都知道,globalThis.undefined 是标配全局属性/只读变量。

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

undefined 恒等于 undefined 原始值吗?

正所谓“大胆假射,小心球证”,语冰有一个大胆的假说——

undefined 的语义并不相通,我只觉得祂们双标。

譬如说今天有个“甲方肝虚”,非要让 undefined 变成 bilibili,这个需求能不能实现呢?

举个粒子,当我们尝试重新定义 undefined 时,万物皆可 undefined万物皆可undefined 猫眼可见,我们的 undefined 不是 undefined 原始值,而是“真·un-defined”——我们 define 啥祂就是啥,一时赋值一时爽,一直赋值一直爽。

机智如你可能会灵魂拷问——我们前面不是证明了 undefined 不可重写吗?怎么祂喵地说变就变,来骗、来偷袭,不讲码德!

这种灵异现象其实和前文提及的作用域(链)的扫描机制有关,我们不烦 cos 一下下“教条主义者”,尝试举一反一。

读写变量会遵循“就近原则”浅尝辄止,先找到谁就 pick 谁,就跟结婚一样,于是乎非全局同名变量 undefined 就屏蔽(shadow,AKA“遮蔽/覆盖”)了 globalThis.undefined

关于作用域链这种“远亲不如近邻”的机制,吾愿赐名为“作用域链截胡”。

globalThis.undefined 确实是鲁棒的。虽然但是,你们重写非全局的 undefined,跟我 globalThis.undefined 有什么关系?(你们逮捕周树人,跟我鲁迅有什么关系?)

大家可能不知道,这里还有一个看不见的冷知识魔鬼在细节——cat 变量始终对 undefined 原始值一心一意,就跟主播一样纯情。

这是因为——cat 变量属于解构缺省(default,AKA“默认”)赋值,已声明但未赋值的变量默认自动被初始化为 undefined 原始值,因为祂没有读写 undefined,所以并不需要上溯作用域链。

虽然但是,undefined 好像是“精分属性”,一言不合就“精神分裂”,一会是不变的 undefined 一会是不便的 undefined,前端人也快“解离性读写障碍”了,总之就是非常折磨。

你知道的,当我们重新定义 undefined 时,代码就很容易产生二义性/复合 BUG。

所谓二义性指的是——你的编程意图/代码语义不够简单粗暴,一行代码拥有多种打开方式,解读可能产生歧义。

譬如说 {},写做大括号,读做“对象字面量”呢还是读做“代码块”呢?

所谓复合 BUG 指的是——大家的代码都 BUG free,But 大家的代码混搭就像水邂逅钠一样激情,干柴烈火瞬间爆炸,然后 B 站就崩了。

举个粒子,当我们尝试重新定义 undefined 时,我们的代码就可能产生二义性/复合 BUG。 复合BUG 猫眼可见,写了半天的 undefined,结果全是 bilibili。一顿操作猛如虎,小丑竟是我自己。

虽然这样的代码是“程序正义”的,但是违和感拉满,语义化和可为维护性十分“赶人”,同事读完欲哭无泪。

所谓“程序正义”指的是——JS 运行时没有“阳”,不会抛出异常,直接挂掉,浏览器承认你的代码 Bug free,问题不大。

祂敢报错吗?祂不敢。虽然但是,无症状感染也是感染。你敢这么写吗?你不敢。除非忍不住,或者想跑路。

举个粒子,“离离原上谱”的“饭圈倒牛奶”事件——

  • 有人鞠躬尽瘁粮食安全
  • 有人精神饥荒疯狂倒奶

这种行为未必违法,但是背德,每次看到只能无视,毕竟语冰有“傻叉恐惧症”。

“程序正义”不代表“结果正义”,代码能 run 不代表符合“甲方肝虚”,不讲码德可能造成业务上的技术负债,将来要重构优化来还债。

大家可能不知道,demo 的重点不在于现象和解释,这只是其中一个彩蛋。

举个粒子,当我们尝试不同环境/语法的测评时,我们对 undefined 的洪荒之力一无所知。 测评彩蛋 猫眼可见,一毛一样的代码,一龙一猪的结果,我们的内心是崩溃的。

总之这种代码就是非常离谱,没错,是语冰写的。

大家大约会理直气壮地反驳,我们必不可能写出这样不当人的代码,var 是不可能 var 的,这辈子都不可能 var

虽然但是,请相信我,永远写正确的代码本身就是一件不正确的事情,这篇文章就是因为语冰被坑了气不过,才给祂载入日记。

BUG 的重点不在于测评环境/边界情况(edge cases),BUG 的重点在于——undefined 可以是 undefined 原始值,也可以不是,两者并非“图灵等价”。

这意味着——只要你的代码中出现了 undefined 这个变量,就可能会产生一些看不见的技术负债,导致将来维护代码的铲屎官需要重构来还债。

正所谓“前猫大便,后人铲屎”,大抵不过如此。

换而言之,倘若 undefinednull 一样鲁棒且优秀,倘若粉丝像主播一样富有且慷慨,你根本就没法写出这样的代码,因为你没有卡 BUG 的空间。

归根结底,能解答一切的答案,即“万恶之源”是——undefined 表面是字面量,实质并不是字面量。

那什么是字面量(literal,AKA“直接量”)呢?

MDN 文档曾经说过——

“字面量代表 JS 中的值。这些值是你在脚本中直接提供的固定值,而非变量。”

举个粒子,当我们写下一个字面量时,祂的值就是固定的。 字面量 猫眼可见,字面恒久远,一个永流传。字面量就是字面上直接表示的固定值,一眼万年,此生不换。

数字 9 就是数字 9,祂不会基因突变成 666;字符串 '薛定谔' 就是字符串 '薛定谔',祂不会基因突变成“哆啦 A 梦”/“盯裆猫”,更不会突变成 9。

不幸的是,undefined 既不恒等于 undefined 原始值,又是一个合法的标识符,字面量不喜欢的样子祂都有。

反证法可得——undefined 不是字面量。

语冰以前在和 undefined 贴贴时,有若干根深蒂固的“思想钢印”——

  • undefined 是字面量
  • undefined 恒等于 undefined 原始值

事实证明,祂们都是看不见的刻板印象、视网膜上的谎言。答应我,知错就改,下次还敢。

语冰重新整理自己的偏见后,形成的全新偏见是——

  • undefined 不是字面量,而是一个普普通通的属性名
  • undefined 默认指 globalThis.undefined,后者的值才恒等于 undefined 原始值
  • “作用域链截胡”会屏蔽标配全局属性,导致 undefined 失真
  • undefined 属性和 undefined 原始值是两个一龙一猪的概念
  • “其他三连可观看内容......”

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

undefined` 的语义并不相通,我只觉得祂们双标。

虽然但是,有时我们确实需要 DIY 一个鲁棒的“共享常量”,我们期望祂有一个超能力——祂的值恒等于 undefined 原始值。

请问主播有什么奇伎淫巧可以安利吗?

并没有。虽然但是,我们可以用 void 9 来打败 undefinedundefined 不够鲁棒,芜所谓,void 9 会出手的。

未有 undefined 之前

魔改 form《未有天才之前》@鲁迅

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

void 0 读写 undefined 原始值是最佳实践。

虽然但是,为什么会出现这种不是特别“KISS(Keep it Simple & Stupid)”的黑科技呢?

void 是一个存在感低下的操作符,语冰以前甚至不知 void 为何物,而且直接写做 undefined 不是很简单粗暴吗?

BTW,大家不烦想入非非——什么情况下是 void 操作符是充分且必要的?请举个粒子,语冰就不举了。

关于 void 的求生欲,倘若大家感兴趣的话,可以投币打 call/留言许愿,只要大家献祭富有且慷慨的一键三连,语冰就会回馈硬核且有趣的下次丕定,总之我们日后再说。

正所谓“大胆假射,小心球证”,语冰有一个大胆的假说——

首先是犯下“摆烂之罪”的 undefined 自己。

在尚未对 undefined “解离性读写障碍”之前,估计大家已经和语冰一样,对 undefinednull 选择困难了。

为什么会出现两个不能说是一毛一样、只能说是一龙一猪的特殊值呢?

“JS 之父” B.E. 大约会理直气壮地教你做人——成年人全都要,小王子才选择困难。

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

undefined 表示 no value(无值)——也无对象也无值。

null 表示 no object(无对象)。”

举个粒子,当我们尝试在 JSON 中测评祂们时,祂们就表现得一龙一猪。 null vs undefined 猫眼可见,经过 JSON 的序列化和反序列化后,undefined 人间蒸发了,null 还是不忘初心,方得始终。

虽然但是,这里只是举个粒子辅助理解,对象属性不存在和对象属性值为 undefined 原始值是两个一龙一猪的概念,对象属性为空和对象属性未定义也是两个一龙一猪的概念。

私以为 undefined 的意思是不知道薛定谔(语冰家的猫)有没有 girlFans,正体不明不好评价;null 的意思是明确知道薛定谔没有 girlFriend(已绝育),公开处刑单身猫,但是“真·阉割模式”。

B.E. 设计 undefined 还有部分原因是为了异常处理考虑,需要一个更加广义的 metavalue(元值),这跟动态弱类型语言的类型转换有关。

关于 nullundefined 的神仙打架和相爱相杀,倘若大家感兴趣的话,可以投币打 call/留言许愿,只要大家献祭富有且慷慨的一键三连,语冰就会回馈硬核且有趣的下次丕定,总之我们日后再说。

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

在 JS 1.0 中尚未赋能的一些特性,包括但不限于——

  • undefined 的全局绑定
  • void 操作符
  • “其他三连可观看内容......”

“在 JS 1.0 中,undefined 原始值可通过声明和读写未初始化变量的方式来读取。”

猫眼可见,在 JS 横空出世之时,当时尚未集成 undefined 全局属性,也没有 void 操作符,但是已经存在 undefined 原始值了。

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

“在 JS 1.1 中添加了 void 操作符。

void 运算符仅求值其操作数,然后返回 undefined(原始值)。void 0 是读写 undefined(原始值)的最佳实践。”

举个粒子,当我们尝试写下 void 9 时,我们就得到了 undefined 原始值。

void 操作符 猫眼可见,void 操作符永远返回鲁棒的 undefined 原始值,与 0 并没有一刀乐关系。

该书作者之一“JS 之父”——B.E. 实锤了一些 ES3 之前没有集成的特性。

1998 年 11 月 19 日,加入 undefined 的全局绑定。

换而言之,直到 ES3 JS 才赋能 undefined 的全局绑定,这时候才可以在全局对象上读写到 undefined 属性。

而且 ES5 之前,undefined 全局属性是不鲁棒的“RAP”,ES5 之后才将其赋能为鲁棒的只读属性。

你知道的,从此 undefined 生而背锅,负重前行,开始了罪恶的一生。

大家可能会讶异于 undefined 的进化史如此曲折。

其实挨踢领域日新月异,前端轮子滚滚向前,根本停不下来。

今天 Vue2 明天 Vue3,今年 Webpack 明年 Vite,今年 Alpha 狗明年 CatCAT......唯一不变的就是变化本身。

世上唯一不变 是人都善变

——《路过人间》@郁可唯

一个神头鬼脸的 undefined,实在不值得大精小怪。

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

首先是犯下“摆烂之罪”的 undefined 自己,未有 undefined 属性之前,undefined 原始值已经存在。

虽然但是,大家可能会问,void 0 只是一种鲁棒的 fallback(备胎方案),我们直接将 undefined 赋能为字面量不香吗?

这是一个好问题,前端人都沉默了,大约 “JS 之父” B.E. 都觉得这是个 BE(Bad Ending)。

undefined 的诞生

魔改 from《悲剧的诞生》@尼采

前端人都知道,undefined 不是字面量,由此赋能了前端可以承受之历史包袱。

可是啊可是,为什么不直接把 undefined 赋能为字面量呢?undefined 到底是怎样炼成的呢?

正所谓“大胆假射,小心球证”,语冰有一个大胆的假说——

首先是犯下摆烂之罪的 undefined 自己,其次是犯下傲娇之罪的保留字,最后是犯下内卷之罪的前端人。

机智如你大约会灵魂拷问,undefined 不是字面量,跟我保留字有什么关系?

那么什么是保留字呢?

ES 语言规范曾经说过——

“保留字是禁止作为标识符的标识符名。”

保留字是由字面量和其他东西(关键字/未来保留字等)组成的——即字面量是保留字的子集。

所以 undefined 既不是字面量,也不是保留字。

保留字本身一言难尽,即使是在后 ES6 时代的现代化开发中,“阉割模式”也会影响保留字的界定范围。

举个粒子,一键三连,但是灵魂拷问限定版。

  • let 是保留字吗?
  • let 在“阉割模式”下还是保留字吗?
  • let 在“躺平模式”下还是保留字吗?

我可太谢谢你了,本来还挺喜欢 let 的,别问了,再问炸毛。

猫眼可见,保留字自身难保,但 undefined 好像不至于绝体绝命,譬如说我们不妨让 undefined 在“阉割模式”下作为字面量。

不幸的是,最后是犯下“内卷之罪”的前端人。

前端已死莫衷一是,前端已卷俯拾皆是。

举个粒子,当我们 DIY 一个名为 undefined 变量时,万恶之源就开始了。 历史包袱 猫眼可见,在没有 undefined 的时代,原则上我们认为当时这并不是一种技术负债。

但是假设 undefined 被设计为字面量,我们知道字面量是不能作为变量名(标识符)的,一旦落地就意味着以前的代码全部作废。undefined 一句,可怜前端。(楚人一炬,可怜焦土。)

这种 breaking changes(非兼容的破坏性更新)就好像今天我们要把 let 改成 bilibili 一样勇。

即使是 IE 的巅峰时期,祂都不敢这么嚣张,搞破而后立的大一统,当然这跟 IE 其实没有关系,我们只是举个粒子,绝对不是反复鞭尸。

(IE:“还好老子退休的早!你祂喵的在暗示些什么!”)

你知道的,原则上即使是 ES6 筑基的现代化前端开发,ES 语言规范也不会去做“零和博弈”的 breaking changes,而是既要“渐进增强”又要“优雅降级”,优先考虑向下兼容的增量更新。

冰冻三尺非一日之寒,undefined 也不是一日赋能的。

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

首先是犯下摆烂之罪的 undefined 自己,其次是犯下傲娇之罪的保留字,最后是犯下内卷之罪的前端人。

undefined 的本质

魔改 from《偏见的本质》@戈登·奥尔波特

前端人都知道,作为前端在逃字面量,万物皆可 undefined,这个主播有点东西的。

那么“正体不明”的 undefined 的本质到底是什么鬼物呢?

一谈到本质总感觉会被贴上“还原主义者”的标签,不过今天粉丝护体,语冰不烦 cos 一波“还原主义者”。

正所谓“大胆假射,小心球证”,语冰有一个大胆的假说——

undefined 是一个长得像字面量的标识符——写做 undefined,读做 undefined 标识符。

那什么是标识符呢?

MDN 文档曾经说过——

“标识符是代码中用来标识变量、函数或属性的字符序列。标识符用于链接值和名字。”

举个粒子,变量名、属性名、函数名等都是标识符(名)。

举个反粒子,字面量、关键字等保留字都禁止作为标识符。

所以 undefined 作为一个全局属性,其本质就是一个属性名,甚至可以理解为一个字符串,但不完全是。

面试时为了更好地 sell yourself(贩卖自我),我们不妨“烦耳塞”地使用“标识符”这个术语,以彰显我们的编程素养和专业品质。

当我们谈论 undefined 时,我们在谈论什么?

一般而言,包括但不限于一下解读——

  • 默认等价于 globalThis.undefined——一个全局对象的只读属性
  • Undefined 原始类型有且仅有的唯一值——undefined 原始值

举个粒子,当我们尝试谈论 undefined 时,祂就被以下的打开方式解读了。 undefined 猫眼可见,undefined 这两种无关本质的解读并非“图灵等价”,所以才会产生前端在逃字面量的精分割裂感。

大家可能不知道,当我们读写 undefined 时,我们默认是想要表达 undefined 原始值,但是实际上我们读写的 undefined 只是全局对象的一个属性名。

这其实是字面量一种视觉上的“思想钢印”。倘若 undefined 是大写的 Undefined,或许大家会比较容易 get 到,就像 Promise 一样,不会有人觉得 Promise 是一个字面量吧。

当然,这是由 undefined 的本质和我们的认知偏见决定的,和大小写本身无关,譬如 NaN 看起来就像字面量。

那如何证明 undefined 就是一个标识符呢?这与 JS 引擎如何解读 undefined 有关。

我们可以借助一些工具人帮我们可视化 undefined 的本质,这些与 AST(抽象语法树)相关的工具人需要有编译原理的基础,不过我们不必“挨踢蕉绿”,只是看看就好。

抽象语法树

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

undefined 是一个长得像字面量的标识符——写做 undefined,读做 undefined 标识符。

Before U Go

魔改 form《Before You Go》@Lewis Capaldi

林语冰曾经说过——

纸上得来终觉浅,节制撸码要宫刑。

前端人都知道,“编程”AKA“程序设计”——“编”即“设计”,“程”即“程序”,“编程”即“程序设计”。

编程是技能型知识,就像开车一样,只有科目一的情绪记忆是完全达咩的,想进化为车震老司机还得知行合一,刻意练习固化肌肉记忆。

爱因斯坦是伟大的理论物理学家,但相对论再牛叉也需要实验物理学家亲测有效。你不投币,你怎么能说重力是存在的?

“程序”是看得见的“物理硬件”,倘若我们只懂程序的语法和语义,那我们和 CatCAT 就没什么区别了,我们甚至不如 Alpha 狗。

人族的优势在于创造性思维,在于编程的“编”,也就是“设计”的部分。

“设计”是看不见的“魔法软件”,想要提升编程天花板,我们需要升级一下 CPU。剑与魔法缺一不可。我们不仅要学会亮剑,还要懂得补魔。

今天我们的关注点主要在于 undefined 的“硬件”,我们简单了解了一下下 undefined 的前世今生,但是我们还缺少 undefined 的正确打开方式。

undefined 普遍认知大约是“未定义/无值”,这种说法是正确的,虽然但是,私以为没有完全正确。

undefined 是一个“不为谁而作的值”,与其说是“未定义”不如视为“未赋值”,吾愿赐名为“上下文无关的无状态占位符”。

这里语冰特别使用了“无状态占位符”的说法,就是为了区分彻底清除以前“无值”的“思想钢印”,植入一种全新的“思想钢印”。

希望各位有彼芯、有硬币、有关注、有收藏的四有青年能够冬日可爱,知书达礼,仅仅知道书上的理论是不行的,还要懂得送礼。

三连大数据突破 2 的 9 次方,马上安排下一期《ES6 奇葩说》——undefined 的代码洁癖(自律版),一起测评一下关于 undefined 的“软件”。

语冰会根据大宝贝们反馈的大数据及时调整热点内容,共享 BUG。

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

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

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

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

前端禁书目录

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

undefined的历史包袱(视频版)