重新審視 undefined 和 null
本文已獲得原作者的獨(dú)家授權(quán),有想轉(zhuǎn)載的朋友們可以在后臺(tái)聯(lián)系我申請(qǐng)開(kāi)白哦! PS:歡迎掘友們向我投稿哦,被采用的文章還可以送你掘金精美周邊!
原文地址:undefined vs. null revisited 原文作者:Dr. Axel Rauschmayer 譯文出自:掘金翻譯計(jì)劃 本文永久鏈接:https://github.com/xitu/gold-miner/blob/master/article/2021/undefined-null-revisited.md 譯者:霜羽 Hoarfroster 校對(duì)者:Moonball、felixliao
重新審視 undefined 和 null
很多的編程語(yǔ)言都有一種表示空值的類型,叫做 null。它指示了一個(gè)變量當(dāng)前并沒(méi)有指向任何對(duì)象 —— 例如,某個(gè)變量還沒(méi)有初始化的時(shí)候。
作為不同,JavaScript 則擁有兩種表示空值的類型,一種是 undefined,另一種則是 null。在這篇文章中,我們將測(cè)試它們的區(qū)別,以及如何去挑選最佳的類型或避免去使用它們。
undefined vs. null
兩個(gè)值都很是相像,并且通常被相互替代著使用,也因此,他們之間的區(qū)別很是細(xì)微。
undefined、null 在ECMAScript 語(yǔ)言標(biāo)準(zhǔn)上的對(duì)比
ECMAScript 語(yǔ)言標(biāo)準(zhǔn)按照如下內(nèi)容描述他們:
undefined是在一個(gè)變量還沒(méi)有被賦值時(shí)候使用的。出處null表示任何有意地缺省對(duì)象值。出處
我們等下就會(huì)探索一下作為程序員,我們應(yīng)該如何去以最佳的方式使用這兩個(gè)值。
兩個(gè)空值 —— 一個(gè)不能彌補(bǔ)的錯(cuò)誤
在 JavaScript 中同時(shí)有兩個(gè)表示空值的值現(xiàn)在被認(rèn)為是一個(gè)設(shè)計(jì)錯(cuò)誤(哪怕是 JavaScript 之父 Brendan Eich 也這么認(rèn)為)。
那么為什么不從 JavaScript 中刪除這兩個(gè)值之一呢?JavaScript 的一項(xiàng)核心原則是永不破壞向后的兼容性。該原則具有好處,但同時(shí)也擁有著最大的缺點(diǎn),即無(wú)法彌補(bǔ)設(shè)計(jì)錯(cuò)誤。
undefined 和 null 的歷史
在 Java(影響了 JavaScript 很多方面的語(yǔ)言)中初始值依賴于一個(gè)變量的靜態(tài)類型:
以對(duì)象值為類型的變量初始化為 null。每個(gè)基本類型都擁有它的初始值,例如 int整型對(duì)應(yīng)0。
在 JavaScript 中,每一個(gè)變量都可以存儲(chǔ)對(duì)象值或原始值,意味著如果 null 表示不是一個(gè)對(duì)象,那么 JavaScript 也同時(shí)需要一個(gè)初始值表示既不是一個(gè)對(duì)象也不擁有原始值,這就是 undefined。
undefined 的出現(xiàn)場(chǎng)合
如果一個(gè)變量 myVar 還沒(méi)有被初始化,那么它的值就是 undefined:
let myVar;
assert.equal(myVar, undefined);
如果一個(gè)屬性 .unknownProp 不存在,訪問(wèn)這個(gè)屬性就會(huì)生成 undefined 值:
const obj = {};
assert.equal(obj.unknownProp, undefined);
如果一個(gè)函數(shù)沒(méi)有明確返回任何內(nèi)容,那么默認(rèn)就會(huì)返回 undefined:
function myFunc() {
}
assert.equal(myFunc(), undefined);
如果一個(gè)函數(shù)擁有一個(gè) return 語(yǔ)句但沒(méi)有指定任何返回值,那么也會(huì)默認(rèn)返回 undefined:
function myFunc() {
return;
}
assert.equal(myFunc(), undefined);
如果一個(gè)參數(shù) x 沒(méi)有傳實(shí)參,那么就會(huì)被初始化為 undefined:
function myFunc(x) {
assert.equal(x, undefined);
}
myFunc();
通過(guò) obj?.someProp 訪問(wèn)的可選鏈在obj 是 undefined 或 null 的時(shí)候返回 undefined:
> undefined?.someProp
undefined
> null?.someProp
undefined
null 的出現(xiàn)場(chǎng)合
一個(gè)對(duì)象的原型要么是另一個(gè)對(duì)象,要么是原型鏈末尾的 null。Object.prototype 沒(méi)有原型:
> Object.getPrototypeOf(Object.prototype)
null
如果我們使用一個(gè)正則表達(dá)式(例如 /a/)匹配一個(gè)字符串(例如 x),我們要么得到一個(gè)存儲(chǔ)著匹配數(shù)據(jù)的對(duì)象(如果匹配成功),要么得到 null(如果匹配失敗)。
> /a/.exec('x')
null
JSON 數(shù)據(jù)格式 不支持 undefined,只支持 null:
> JSON.stringify({a: undefined, b: null})
'{"b":null}'
專門用來(lái)對(duì)付 undefined 和 null 的操作符
undefined 以及默認(rèn)參數(shù)值
一個(gè)參數(shù)的默認(rèn)值會(huì)在以下情況下被使用:
這個(gè)參數(shù)被我們忽略掉了。 這個(gè)參數(shù)被賦予 undefined值。
舉個(gè)例子:
function myFunc(arg = 'abc') {
return arg;
}
assert.equal(myFunc('hello'), 'hello');
assert.equal(myFunc(), 'abc');
assert.equal(myFunc(undefined), 'abc');
當(dāng)指向它的值為一個(gè)元值時(shí),undefined 也會(huì)觸發(fā)默認(rèn)參數(shù)值。
以下的例子示范了這個(gè)特性有用的地方:
function concat(str1 = '', str2 = '') {
return str1 + str2;
}
function twice(str) { // (A)
return concat(str, str);
}
在 A 行,我們并沒(méi)有制定參數(shù) str 的默認(rèn)值,而當(dāng)這個(gè)參數(shù)被忽略掉的時(shí)候,我們將該狀態(tài)轉(zhuǎn)發(fā)到 concat(),讓其選擇默認(rèn)值。
undefined,解構(gòu)默認(rèn)值
解構(gòu)下的默認(rèn)值的工作方式與參數(shù)默認(rèn)值類似 —— 如果變量在數(shù)據(jù)中不匹配或與 undefined 匹配,則使用它們:
const [a = 'a'] = [];
assert.equal(a, 'a');
const [b = 'b'] = [undefined];
assert.equal(b, 'b');
const {prop: c = 'c'} = {};
assert.equal(c, 'c');
const {prop: d = 'd'} = {prop: undefined};
assert.equal(d, 'd');
undefined、null 和可選鏈
如果通過(guò) value?.prop 使用了可選鏈:
如果 value是undefined或null的,將會(huì)返回undefined。也就是說(shuō),如果value.prop拋出錯(cuò)誤,就會(huì)返回undefined。否則會(huì)返回 value.prop.
function getProp(value) {
// 可選的靜態(tài)屬性訪問(wèn)
return value?.prop;
}
assert.equal(
getProp({prop: 123}), 123);
assert.equal(
getProp(undefined), undefined);
assert.equal(
getProp(null), undefined);
以下的兩個(gè)操作也很是類似的工作:
obj?.[?expr?] // 可選的動(dòng)態(tài)屬性訪問(wèn)
func?.(?arg0?, ?arg1?) // 可選的函數(shù)或方法調(diào)用
undefined、null 和空合并
空合并操作符 ?? 可讓我們?cè)谝粋€(gè)值是 undefined 或 null 時(shí),使用默認(rèn)值:
> undefined ?? 'default value'
'default value'
> null ?? 'default value'
'default value'
> 0 ?? 'default value'
0
> 123 ?? 'default value'
123
> '' ?? 'default value'
''
> 'abc' ?? 'default value'
'abc'
空合并賦值操作符 ??= 合并了空合并操作符與賦值操作符:
function setName(obj) {
obj.name ??= '(Unnamed)';
return obj;
}
assert.deepEqual(
setName({}),
{name: '(Unnamed)'}
);
assert.deepEqual(
setName({name: undefined}),
{name: '(Unnamed)'}
);
assert.deepEqual(
setName({name: null}),
{name: '(Unnamed)'}
);
assert.deepEqual(
setName({name: 'Jane'}),
{name: 'Jane'}
);
處理 undefined 與 null
以下的部分解釋了在我們代碼中最常見(jiàn)的處理 undefined 和 null 的方法:
實(shí)際值既不是 undefined 也不是 null
例如,我們可能希望屬性 file.title 始終存在并且始終是字符串,那么有兩種常見(jiàn)的方法可以實(shí)現(xiàn)此目的。
請(qǐng)注意,在此博客文章中,我們僅檢查 undefined 和 null,而不檢查值是否為字符串。你需要自己決定是否要添加檢查器,作為附加的安全保障措施。
同時(shí)禁止 undefined 和 null
例如:
function createFile(title) {
if (title === undefined || title === null) {
throw new Error('`title` must not be nullish');
}
// ···
}
為什么選擇這個(gè)方法?
我們希望以相同的方式處理
undefined和null,因?yàn)?JavaScript 代碼就是經(jīng)常那樣做,例如:// 檢查一個(gè)屬性是否存在
if (!obj.requiredProp) {
obj.requiredProp = 123;
}
// 通過(guò)空合并操作符使用默認(rèn)值
const myValue = myParameter ?? 'some default';如果我們的代碼中出現(xiàn)了問(wèn)題,讓
undefined或null出現(xiàn)了,我們需要讓它盡早結(jié)束執(zhí)行并拋出錯(cuò)誤。
同時(shí)對(duì) undefined 和 null 使用默認(rèn)值
例如:
function createFile(title) {
title ??= '(Untitled)';
// ···
}
我們不能使用參數(shù)默認(rèn)值,因?yàn)樗粫?huì)被 undefined 觸發(fā)。在這里,我們依賴于空合并賦值運(yùn)算符 ??=。
為什么選擇這個(gè)方法?
我們希望以相同方式對(duì)待 undefined和null(見(jiàn)上文)。我們希望我們的代碼無(wú)聲但有力地對(duì)待 undefined和null。
undefined 或 null 是一個(gè)被忽略的值
例如,我們可能希望屬性 file.title 是字符串或是被忽略的值(即 file 沒(méi)有標(biāo)題),那么有幾種方法可以實(shí)現(xiàn)此目的。
null 是被忽略值
例如:
function createFile(title) {
if (title === undefined) {
throw new Error('`title` 不應(yīng)該是 undefined');
}
return {title};
}
或者,undefined 也可以觸發(fā)默認(rèn)值:
function createFile(title = '(Untitled)') {
return {title};
}
為什么要選擇這個(gè)方法?
我們需要一個(gè)空值來(lái)表示被忽略。 我們不希望空值觸發(fā)參數(shù)默認(rèn)值并破壞默認(rèn)值。 我們想將空值字符串化為 JSON(這是我們無(wú)法對(duì) undefined進(jìn)行的處理)。
undefined 是被忽略的值
例如:
function createFile(title) {
if (title === null) {
throw new Error('`title` 不應(yīng)該是 null');
}
return {title};
}
為什么選擇這種方法?
我們需要一個(gè)空值來(lái)表示被忽略。 我們確實(shí)希望空值觸發(fā)參數(shù)或解構(gòu)默認(rèn)值。
undefined 的一個(gè)缺點(diǎn)是它通常是在 JavaScript 中意外賦予的 —— 在未初始化的變量,屬性名稱中的錯(cuò)字,忘記從函數(shù)中返回內(nèi)容等。
為什么不同時(shí)將 undefined 和 null 看作是被忽略的值?
當(dāng)接收到一個(gè)值時(shí),將 undefined 和 null 都視為 “空值” 是有意義的。但是,當(dāng)我們創(chuàng)建值時(shí),我們不希望模棱兩可,以避免不必要的麻煩。
這指向了另一種角度:如果我們需要一個(gè)被忽略的值,但又不想使用 undefined 或 null 作為被忽略值時(shí)該怎么辦?看看下文吧:
其他處理被忽略值的方法
特殊值
我們可以創(chuàng)建一個(gè)特殊值,每當(dāng)屬性被忽略時(shí) .title 時(shí)就使用該值:
const UNTITLED = Symbol('UNTITLED');
const file = {
title: UNTITLED,
};
Null 對(duì)象模式
Null 對(duì)象模式 來(lái)自 OOP(面對(duì)對(duì)象編程):
一個(gè)公共超類的所有子類都具有相同的接口。 每個(gè)子類實(shí)現(xiàn)一種不同的模式供其實(shí)例使用。 這些模式之一是 null。
在下文中,UntitledFile 繼承了 “null” 模式。
// Abstract superclass
class File {
constructor(content) {
if (new.target === File) {
throw new Error('Can’t instantiate this class');
}
this.content = content;
}
}
class TitledFile extends File {
constructor(content, title) {
super(content);
this.title = title;
}
getTitle() {
return this.title;
}
}
class UntitledFile extends File {
constructor(content) {
super(content);
}
getTitle() {
return '(Untitled)';
}
}
const files = [
new TitledFile('Dear diary!', 'My Diary'),
new UntitledFile('Reminder: pick a title!'),
];
assert.deepEqual(
files.map(f => f.getTitle()),
[
'My Diary',
'(Untitled)',
]);
我們也可以只為標(biāo)題(而不是整個(gè)文件對(duì)象)使用空對(duì)象模式。
“也許”類型
“也許”類型是一種函數(shù)編程技術(shù):
function getTitle(file) {
switch (file.title.kind) {
case 'just':
return file.title.value;
case 'nothing':
return '(Untitled)';
default:
throw new Error();
}
}
const files = [
{
title: {kind: 'just', value: 'My Diary'},
content: 'Dear diary!',
},
{
title: {kind: 'nothing'},
content: 'Reminder: pick a title!',
},
];
assert.deepEqual(
files.map(f => getTitle(f)),
[
'My Diary',
'(Untitled)',
]);
我們本可以通過(guò)數(shù)組對(duì) "just" 和 "nothing" 進(jìn)行編碼,但我們的方法的好處是 TypeScript 對(duì)其有很好的支持(通過(guò)可辨識(shí)聯(lián)合)。
我的方法
我不喜歡將 undefined 用作被忽略的值的原因有三個(gè):
undefined通常是在 JavaScript 中意外出現(xiàn)的。undefined會(huì)觸發(fā)參數(shù)和解構(gòu)的默認(rèn)值(出于某些原因,某些人更喜歡undefined)。
因此,如果需要特殊值,可以使用以下兩種方法之一:
我將 null用作被忽略的值。(順便說(shuō)一句,TypeScript 相對(duì)較好地支持了這種方法。)我通過(guò)上述的其中一種技術(shù)避免了同時(shí)出現(xiàn) undefined和null的情況,優(yōu)點(diǎn)在乎讓代碼更干凈,而缺點(diǎn)在于需要做出更多的工作。
本文正在參與「掘金 2021 春招闖關(guān)活動(dòng)」, 點(diǎn)擊查看活動(dòng)詳情
如果發(fā)現(xiàn)譯文存在錯(cuò)誤或其他需要改進(jìn)的地方,歡迎到 掘金翻譯計(jì)劃 對(duì)譯文進(jìn)行修改并 PR,也可獲得相應(yīng)獎(jiǎng)勵(lì)積分。文章開(kāi)頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。
掘金翻譯計(jì)劃 是一個(gè)翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū),文章來(lái)源為 掘金 上的英文分享文章。內(nèi)容覆蓋 Android、iOS、前端、后端、區(qū)塊鏈、產(chǎn)品、設(shè)計(jì)、人工智能等領(lǐng)域,想要查看更多優(yōu)質(zhì)譯文請(qǐng)持續(xù)關(guān)注 掘金翻譯計(jì)劃、官方微博、知乎專欄。
最后
如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「huab119」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

