重铸前端霸权,吾辈义不容辞!
Halo Word!大家好,我是大家的林语冰(挨踢版)~
往期《ES6 奇葩说》系列——
- 《
undefined
的历史包袱》的关注点在于:undefined
的前世今生 - 《我们为什么需要
undefined
?》的关注点在于:undefined
的设计动机
我们对 undefined
已经有了基本认知,本期的关注点在于 undefined
的正确打开方式。
你知道的,代码千万条,优雅第一条,撸码不鲁棒,同事两行泪。
今天的前端圆桌我们来伪科普一下下 undefined
的代码洁癖(个人向)。
前端禁书目录(省流版)
魔改 from《魔法禁书目录》@镰池和马
undefined
的“七大罪”,包括但不限于——
- 贪婪——重写常量,言行不一
- 嫉妒——同名声明,屏蔽全局
- 暴怒——一言不合
undefined
- 暴食——初始赋值,把饭叫饥
- 懒惰——偷懒重置,类型突变
- 色欲——宽松比较,一心二意
- 傲慢——过度自信,技术负债
- 懂得都懂,不懂关注,日后再说~
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《某科学的超电磁炮》@镰池和马
-
undefined
的历史包袱 -
我们为什么需要
undefined
?