undefined 的“七大罪”(自律版)

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

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

往期《ES6 奇葩说》系列——

  • undefined 的历史包袱》的关注点在于:undefined 的前世今生
  • 《我们为什么需要 undefined?》的关注点在于: undefined 的设计动机

我们对 undefined 已经有了基本认知,本期的关注点在于 undefined 的正确打开方式。

你知道的,代码千万条,优雅第一条,撸码不鲁棒,同事两行泪。

今天的前端圆桌我们来伪科普一下下 undefined 的代码洁癖(个人向)。

前端禁书目录(省流版)

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

undefined 的“七大罪”,包括但不限于——

  1. 贪婪——重写常量,言行不一
  2. 嫉妒——同名声明,屏蔽全局
  3. 暴怒——一言不合 undefined
  4. 暴食——初始赋值,把饭叫饥
  5. 懒惰——偷懒重置,类型突变
  6. 色欲——宽松比较,一心二意
  7. 傲慢——过度自信,技术负债
  8. 懂得都懂,不懂关注,日后再说~

Rule 1: 禁止重写全局属性

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

“ES3 之前没有 undefined 全局属性,ES5 之前 undefined 属性可重写。”

ES5 之后 undefined 是一个“不可失忆”的全局属性——

  • 不可重写
  • 不可配置
  • 不可枚举

换而言之,undefined 的鲁棒性赋能祂先天免疫若干“阴间操作”。

举个粒子,当我们尝试重写全局属性时,试试就逝世。

try {
  undefined = 'bilibili'
} catch {
  console.log('禁止重写,神说无效!')
}

undefined /* 仍然是 undefined */

猫眼可见,undefined 的内心毫无波动,上述代码在“阉割模式”(严格模式)下会抛出异常。

虽然但是,上述代码在“躺平模式”(草率模式)下会静默失败,运行时没有温馨提示。

换而言之,不同环境测评的行为一毛一样——都是赋值无效、重写失败,但是提示方式一龙一猪。

“躺平模式”下没有报错,其他读码人可能误以为 undefined 被重写了,理解出现歧义。

毕竟不是所有的前端人都像屏幕前的你一样好学,天天来给我彼芯,知道这个 edge case(边界情况)。

综上所述,语冰的代码洁癖是——

原则上我们禁止贪婪地重写全局属性,不作死就不会死,规避代码的二义性。

Rule 2: 禁止屏蔽全局变量

MDN 文档曾经说过——

undefined 是一个见怪不怪的标识符,碰巧成为全局属性。”

你知道的,原则上允许 undefined 作为合法的变量名/函数名等,即使不合理。

举个粒子,当我们尝试重复声明同名变量时,全局变量就会被屏蔽(shadow)。

let undefined = 'bilibili'

undefined /* 'bilibili' 字符串 */
globalThis.undefined /* undefined 原始值 */

猫眼可见,非全局同名变量会屏蔽全局变量,导致期望的 undefined 变量失真。

你知道的,变量读取遵循“就近原则”,优先在当前作用域疯狂试探,先到先得,若求之不得才逆袭作用域链,回溯上游作用域扫描。

换而言之,当前作用域变量优先于上游作用域同名变量,吾愿赐名为“作用域截胡”。

上述代码虽然是“程序正义”的,JS 运行时承认代码的合法性,不会报错,但不是“结果正义”的,因为代码可能产生二义性。

你知道的,“无值”是你的谎言(四月是你的谎言),原则上允许 undefined 是任意值。

综上所述,语冰的代码洁癖是——

原则上我们禁止重复声明屏蔽全局变量,不可嫉妒全局变量,避免指猫为狗。

Rule 3: 禁止一言不合 undefined

我们在《我们为什么需要 undefined?》中曾经说过——

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

你知道的,原则上允许 undefined 作为兼容 JS 所有类型变量的合法初始值。

语冰以前的“思想钢印”是——遇事不决 undefined

举个粒子,因为太麻烦就全写 undefined 了。(因为太麻烦就全点防御力了)

/* 举个反粒子 */
let cat = undefined
let daisy = undefined
/* ...... */

/* 技术性调整 */
let cat = 'Schrodinger'
let daisy = {}

猫眼可见,万物皆可 undefined,但是编程意图并不明确。

你知道的,绝对的光明等价于绝对的黑暗。

举一反一,万能的 undefined 等价于无能的 undefined

变量的命名应该体现值的含义,变量的类型和值应该尽快尘埃落定,明示读码人变量的语意。

undefined 顾名思义指 UZI(Undefined Zone Indeed,此处未定义)——一个无状态的占位符,也无类型也无值(也无风雨也无晴)。

虽然但是,正所谓道可道,非常道。准确而无用的观念,终究还是无用的。

JS 是一门动态类型语言,原则上允许变量的类型和值动态决定,大家基本上也不会去纠结类型。

动态类型并非无类型——当变量的值确定时,祂的类型也确定了。

即使变量具体的初始值尚未尘埃落定,我们也应该优先赋值为具体类型的“特殊值”,提前明示读码人变量的类型约束。

举个粒子,即使不确定变量具体的初始值,我们也可以优先考虑“静态类型初始化”——

  • Number 类型可以初始化为 0/-1
  • String 类型可以初始化为 '' 空字符等
  • Object 类型可以初始化为 {} 空对象等
  • Array 数组(对象子类型)可以初始化为 [] 空数组等
  • 举一反一,其他粉丝可观看内容……
/* 静态类型初始化 */
let like = 0 /* 彼芯数量 */
let name = '' /* 未闻花名 */
let girlFriend = {} /* 正体不明 */
let girlFans = [] /* 未元物质 */
/* ...... */

猫眼可见,值未定义,类型先行,变量语意越具体越 nice。

换而言之,若变量的类型和值已尘埃落定,我们直接给变量初始化具体值,否则退而求其次,优先“静态类型初始化”——将变量赋值为具体类型的“特殊值”。

虽然 JS 是动态弱类型语言,但是我们的静态编程思维可以帮助我们编写可读可维护的代码,优雅得不谈。

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

当且仅当变量的类型和初始值同时不明确时,我们才考虑使用 undefined 作为无状态的占位符。

举个粒子,当我们对变量一无所知时,我们才使用 undefined

let data = {} /* 具体值未知,类型已知 */
let result /* 具体值和类型未知 */

try {
  result = {} /* 可能是 JSON 数据 */
} catch {
  result = new Error() /* 可能是自定义异常 */
}

猫眼可见,当且仅当变量的类型未知/类型需要兼容时,我们才考虑使用 undefined 作为一个提前占位的符号,后续再根据具体业务逻辑按需赋值。

综上所述,语冰的代码洁癖是——

原则上我们禁止一言不合 undefined,因为 undefined 的无能在于祂无所不能。

Rule 4: 禁止冗余初始赋值

前面我们有讲到,undefined 全局变量可能被屏蔽,导致预期的 undefined 失真。

换而言之,直接使用 undefined 变量读取 undefined 原始值并不鲁棒。

举个粒子,当我们使用 undefined 初始化时,这个写法可能是不鲁棒的。

/* 举个反粒子 */
let undefined = 'bilibili'
let cat = undefined
cat /* 'bilibili' 字符串 */

/* 技术性调整 */
let result
result /* undefined 原始值 */

猫眼可见,undefined 不是字面量,所以“光明正大”地使用 undefined 是不鲁棒的。

undefined 变量并不恒等于 undefined 原始值,两者并非“图灵等价”,我们优先使用 undefined 的“隐性性状”。

综上所述,语冰的代码洁癖是——

原则上我们禁止冗余的初始赋值,“Duck 不必”把饭叫饥,矫枉过正。

Rule 5: 禁止重置赋值为 undefined

前端人都知道,undefined 变量的值是不稳定的,一言不合就失真。

换而言之,使用 undefined 赋值初始化是不鲁棒的。

举一反一,使用 undefined 重置赋值也是不鲁棒的。

举个粒子,当我们尝试重置变量时,可能导致意外结果。

let cat = 'bilibili'

cat = 'Schrodinger' /* 重写 */
cat = '' /* 重置,保持类型一致 */

cat = undefined /* bad bad */

猫眼可见,重写/重置变量时优先保持类型一致,同时规避 undefined 的不确定性。

总而言之,使用 undefined 赋值是不鲁棒的——一来值不稳定,二来类型突变。

你知道的,JS(JavaScript)是动态弱类型语言,原则上允许黑客为所欲为地动态魔改变量的类型和值。

虽然但是,绝对的权力导致绝对的腐败,举一反一,绝对的动态导致绝对的变态。

JS 是灵活的动态语言,但不代表前端人也要写测不准的代码,咱又不搞量子力学,这样的代码并不优雅且鲁棒。

保证变量名副其实和类型一致,可读性和可维护性也更好。

综上所述,语冰的代码洁癖是——

原则上我们禁止偷懒使用 undefined 重置变量,优先静态 DIY 变量,保持类型一致。

Rule 6:禁止宽松比较

ES 语言规范(ECMAScript Language Specification)曾经说过——

“Undefined 数据类型有且仅有一个唯一值——undefined 原始值。”

换而言之,undefined 既可以通过类型判断,也可以通过值判断。

相信我,成熟的前端人全都要会,小王子才选择困难。

举个粒子,我们尝试双管齐下判断 undefined

let VOID = void 0

Reflect.apply(toString, VOID, [])
/* '[object Undefined]',类型判断 */
VOID === undefined /* true,值判断 */

VOID === null /* false,严格相等 */
VOID == null /* true,宽松相等 */

猫眼可见,我们可以通过类型判断/值比较来识别 undefined 原始值。

虽然但是,值比较的时候存在一个 edge cases(边界情况)。

MDN 文档曾经说过——

“这里必须使用 === 严格相等操作符而不是 == 宽松相等操作符。”

==undefined 并非一心一意,可能出轨 null,导致判断出错。

相信我,使用“三长”才能避免“两短”——=== 严格相等优于 == 宽松相等,一寸长一寸强。

你知道的,我们也可以使用 typeof 操作符进行类型判断。

MDN 文档曾经说过——

“使用 typeof 操作符的原因在于——即使变量未声明,祂也不会抛出错误。”

举个粒子,Vue 源码的最佳实践。

/* 类型判断 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    if (isIOS) setTimeout(noop)
  }
}

/* 值判断 */
export function isUndef(v: any): v is undefined | null {
  return v === undefined || v === null
}

猫眼可见,typeof 很适合“优雅降级”的兼容性类型检测。

倘若不明确变量是否已被创建,可以优先使用 typeof 操作符进行类型比较,否则可以使用 === 严格相等操作符进行值比较。

综上所述,语冰的代码洁癖是——

原则上允许我们通过类型/值比较判断 undefined,但是值比较时禁止宽松比较,避免一心二意。

Rule 7: DIY 鲁棒的共享常量

《ECMAScript6 标准入门教程》曾经说过——

“魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。”

“风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。”

举个粒子,使用魔术字符串/魔术数字代码可读性不好。

const cat = 'bilibili'

/* 魔术字符串 */
if (cat === 'bilibili');
if (cat === 'bbibbi'); /* 拼写错误 */
if (cat === 'bilibili');

/* 魔术数字 */
if (cat === 9); /* 9 语意不明 */
if (cat === 9);
if (cat === 9);

猫眼可见,魔术字面量的问题在于重复使用可能拼写错误,其次一处修改,处处修改/排错。

这种技术负债不利于维护代码,且可读性较差,导致维护代码的人需要重构来还债。

读码人并不能直观地 get 到魔术字面量的语意,我们需要重构更优雅且鲁棒的代码。

举个粒子,消除魔术字面量。

const cat = 'bilibili'
const bilibili = 'bilibili' /* 统一管理,拼写提示 */
const age = 9 /* 语意明确 */

if (cat === bilibili);
if (cat === age);

猫眼可见,我们统一维护字面量,按需一处修改即可。

其次编程意图明确,变量名浅显易懂,拼写也有 IED 的提示不容易犯错。

举一反一,我们不烦 cos 一下下“教条主义者”,故技重施消除 undefined 的副作用。

举个粒子,我们可以 DIY 鲁棒的共享常量来编写可维护的代码。

const UNDEFINED = 'undefined' /* 消除魔术字符串 */
const VOID = void 0 /* DIY 鲁棒的共享常量 */

typeof undeclared === UNDEFINED
const isUndefined = value => value === VOID

猫眼可见,我们封装了更加通用的方法、共享常量和鲁棒的 undefined 原始值。

这样做的好处是代码可读性更佳,同时字符串统一管理,不用到处排错或修改,也有安全的 undefined 原始值可以使用。

综上所述,语冰的代码洁癖是——

原则上我们禁止傲慢地拼写魔术字面量,推荐 DIY 共享常量来消除魔术字符串的技术负债。

Before U Go

魔改 form《Before You Go》@Lewis Capaldi

前端人都知道,坏的制度会让好人作恶,好的制度能让坏人从良。

举一反一,坏的代码会让黑客破防,好的代码能让码农内卷。

你永远可以相信 undefined,只要你不使用祂。

相信我,undefined 的正确打开方式是——无为,使用 undefined 的最佳方式就是不使用祂。

换而言之,任何时候、任何情况下,我们承诺不首先使用 undefined(核武器)。

薛定谔(语冰家的猫)曾经说过关于 undefined 的“撸码十诫”——

第一诫——除了我之外,你不可有别的值。(禁止重写全局属性)

第二诫——不可为自己初始赋值。(禁止冗余初始赋值)

第三诫——不可妄称 undefined 的名。(禁止屏蔽全局变量)

第四诫——当且仅当变量的类型和具体值同时不明确时,我们才使用 undefined。(无状态占位符)

第五诫——当知书达礼,仅仅知道本文的知识是不行的,还要懂得给语冰彼芯送礼。

第六诫——不可变型。(禁止重置赋值为 undefined

第七诫——不可 ==。(禁止宽松比较)

第八诫——不可傲慢。(消除魔术字符串)

第九诫——不可一言不合 undefined

第十诫——不可白嫖。

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

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

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

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

免责声明——

大家已经是成熟的前端人了,要学会技术自信/保留偏见,此处“墙裂推荐 ”仅供参考,总之就是非常主观。

大家可以自由言论,自助集成,但禁止共享 BUG,因为语冰并不是再 PUA 你们。

某前端的知识图谱(粉丝福利)

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

原文链接:https://juejin.cn/post/7218099499193335864 作者:大家的林语冰

(0)
上一篇 2023年4月5日 上午10:26
下一篇 2023年4月5日 上午10:36

相关推荐

发表回复

登录后才能评论