空值合併提案 (??
) 新增了一個新的短路運算子,用於處理預設值。
您可能已經熟悉其他短路運算子 &&
和 ||
。這兩個運算子都處理「真值」和「假值」。想像一下程式碼範例 lhs && rhs
。如果 lhs
(讀作「左邊」)為假值,則表達式會評估為 lhs
。否則,會評估為 rhs
(讀作「右邊」)。程式碼範例 lhs || rhs
則相反。如果 lhs
為真值,則表達式會評估為 lhs
。否則,會評估為 rhs
。
但是,「真值」和「假值」到底是什麼意思?根據規範條款,它等於 ToBoolean
抽象運算。對我們這些一般的 JavaScript 開發人員來說,所有東西都是真值,除了假值 undefined
、null
、false
、0
、NaN
和空字串 ''
。(技術上來說,與 document.all
關聯的值也是假值,但我們稍後會討論。)
那麼,&&
和 ||
有什麼問題?為什麼我們需要一個新的空值合併運算子?這是因為真值和假值的這個定義並不適用於所有情況,這會導致錯誤。想像一下以下情況
function Component(props) {
const enable = props.enabled || true;
// …
}
在這個範例中,讓我們將 enabled
屬性視為一個控制元件中某些功能是否啟用的選用布林屬性。意思是,我們可以明確地將 enabled
設定為 true
或 false
。但是,由於它是一個選用屬性,我們可以透過完全不設定它來隱含地將它設定為 undefined
。如果它是 undefined
,我們希望將它視為元件 enabled = true
(其預設值)。
現在,你可能已經發現程式碼範例中的錯誤。如果我們明確地設定 enabled = true
,則 enable
變數為 true
。如果我們隱含地設定 enabled = undefined
,則 enable
變數為 true
。如果我們明確地設定 enabled = false
,則 enable
變數仍然為 true
!我們的目的是將值預設為 true
,但我們實際上強制了值。這種情況下的修正方法是明確指出我們預期的值
function Component(props) {
const enable = props.enabled !== false;
// …
}
我們看到這種錯誤會出現在每個假值中。這很可能是一個選用的字串(其中空字串 ''
被視為有效的輸入),或一個選用的數字(其中 0
被視為有效的輸入)。這是一個如此常見的問題,以至於我們現在引入了空值合併運算子來處理這種預設值指定
function Component(props) {
const enable = props.enabled ?? true;
// …
}
空值合併運算子 (??
) 的作用與 ||
運算子非常相似,只不過在評估運算子時我們不使用「真值」。相反地,我們使用「空值」的定義,意思是「值是否嚴格等於 null
或 undefined
」。因此,想像一下表達式 lhs ?? rhs
:如果 lhs
不是空值,則它會評估為 lhs
。否則,它會評估為 rhs
。
明確地說,這表示值 false
、0
、NaN
和空字串 ''
都是假值,但不是空值。當此類假值但非空值是 lhs ?? rhs
的左側時,表達式會評估為它們,而不是右側。錯誤消失!
false ?? true; // => false
0 ?? 1; // => 0
'' ?? 'default'; // => ''
null ?? []; // => []
undefined ?? []; // => []
解構時如何預設指定? #
你可能已經注意到,最後一個程式碼範例也可以透過在物件解構中使用預設指定來修正
function Component(props) {
const {
enabled: enable = true,
} = props;
// …
}
這有點難以理解,但仍然是完全有效的 JavaScript。不過,它使用稍微不同的語意。物件解構中的預設指定會檢查屬性是否嚴格等於 undefined
,如果是,則預設指定。
但只針對 undefined
的嚴格相等性測試並不總是理想,而且也不總是能取得用於解構的物件。例如,您可能想要預設函式的傳回值(沒有物件可以解構)。或者函式傳回 null
(這是 DOM API 的常見情況)。這些時候您會想要使用空值合併
// Concise nullish coalescing
const link = document.querySelector('link') ?? document.createElement('link');
// Default assignment destructure with boilerplate
const {
link = document.createElement('link'),
} = {
link: document.querySelector('link') || undefined
};
此外,某些新功能(例如 選擇性鏈接)無法與解構完美搭配。由於解構需要一個物件,因此您必須在選擇性鏈接傳回 undefined
(而不是物件)時保護解構。使用空值合併,我們就不會有此問題
// Optional chaining and nullish coalescing in tandem
const link = obj.deep?.container.link ?? document.createElement('link');
// Default assignment destructure with optional chaining
const {
link = document.createElement('link'),
} = (obj.deep?.container || {});
混合和搭配運算子 #
語言設計很困難,而且我們無法在不造成開發人員意圖模糊的情況下建立新的運算子。如果您曾經混合使用 &&
和 ||
運算子,您可能會自己遇到這種模糊性。想像一下表達式 lhs && middle || rhs
。在 JavaScript 中,這實際上與表達式 (lhs && middle) || rhs
的解析方式相同。現在想像一下表達式 lhs || middle && rhs
。這個實際上與 lhs || (middle && rhs)
的解析方式相同。
您可能會看到 &&
運算子對其左右側的優先順序高於 ||
運算子,這表示隱含的括號會包住 &&
而不是 ||
。在設計 ??
運算子時,我們必須決定優先順序為何。它可以
- 優先順序低於
&&
和||
- 低於
&&
但高於||
- 優先順序高於
&&
和||
對於這些優先順序定義中的每一個,我們必須執行四個可能的測試案例
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
在每個測試表達式中,我們必須決定隱含括號屬於何處。如果它們沒有完全按照開發人員的意圖包住表達式,那麼我們就會有寫得很差的程式碼。不幸的是,無論我們選擇哪個優先順序級別,其中一個測試表達式都可能違反開發人員的意圖。
最後,我們決定在混合使用 ??
和 (&&
或 ||
) 時需要明確的括號(請注意我對括號分組的明確說明!這是個元笑話!)。如果您混合使用,您必須用括號包住其中一個運算子組,否則會出現語法錯誤。
// Explicit parentheses groups are required to mix
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);
(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);
(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);
(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);
這樣,語言解析器總是會比對開發人員的意圖。而稍後閱讀程式碼的任何人也能立即理解它。真棒!
告訴我關於 document.all
#
document.all
是您永遠不應該使用的特殊值。但如果您確實使用了它,最好了解它如何與「真值」和「空值」互動。
document.all
是類陣列物件,表示它具有索引屬性(像陣列)和長度。物件通常為真值,但令人驚訝的是,document.all
偽裝成假值!事實上,它與 null
和 undefined
都鬆散相等(這通常表示它根本沒有屬性)。
將 document.all
與 &&
或 ||
一起使用時,它偽裝成假值。但是,它與 null
或 undefined
不同,因此它不是空值。所以當將 document.all
與 ??
一起使用時,它的行為就像任何其他物件一樣。
document.all || true; // => true
document.all ?? true; // => HTMLAllCollection[]