空值合併

發佈於 · 標籤為 ECMAScript ES2020

空值合併提案 (??) 新增了一個新的短路運算子,用於處理預設值。

您可能已經熟悉其他短路運算子 &&||。這兩個運算子都處理「真值」和「假值」。想像一下程式碼範例 lhs && rhs。如果 lhs(讀作「左邊」)為假值,則表達式會評估為 lhs。否則,會評估為 rhs(讀作「右邊」)。程式碼範例 lhs || rhs 則相反。如果 lhs 為真值,則表達式會評估為 lhs。否則,會評估為 rhs

但是,「真值」和「假值」到底是什麼意思?根據規範條款,它等於 ToBoolean 抽象運算。對我們這些一般的 JavaScript 開發人員來說,所有東西都是真值,除了假值 undefinednullfalse0NaN 和空字串 ''。(技術上來說,與 document.all 關聯的值也是假值,但我們稍後會討論。)

那麼,&&|| 有什麼問題?為什麼我們需要一個新的空值合併運算子?這是因為真值和假值的這個定義並不適用於所有情況,這會導致錯誤。想像一下以下情況

function Component(props) {
const enable = props.enabled || true;
// …
}

在這個範例中,讓我們將 enabled 屬性視為一個控制元件中某些功能是否啟用的選用布林屬性。意思是,我們可以明確地將 enabled 設定為 truefalse。但是,由於它是一個選用屬性,我們可以透過完全不設定它來隱含地將它設定為 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;
// …
}

空值合併運算子 (??) 的作用與 || 運算子非常相似,只不過在評估運算子時我們不使用「真值」。相反地,我們使用「空值」的定義,意思是「值是否嚴格等於 nullundefined」。因此,想像一下表達式 lhs ?? rhs:如果 lhs 不是空值,則它會評估為 lhs。否則,它會評估為 rhs

明確地說,這表示值 false0NaN 和空字串 '' 都是假值,但不是空值。當此類假值但非空值是 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) 的解析方式相同。

您可能會看到 && 運算子對其左右側的優先順序高於 || 運算子,這表示隱含的括號會包住 && 而不是 ||。在設計 ?? 運算子時,我們必須決定優先順序為何。它可以

  1. 優先順序低於 &&||
  2. 低於 && 但高於 ||
  3. 優先順序高於 &&||

對於這些優先順序定義中的每一個,我們必須執行四個可能的測試案例

  1. lhs && middle ?? rhs
  2. lhs ?? middle && rhs
  3. lhs || middle ?? rhs
  4. 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 偽裝成假值!事實上,它與 nullundefined 都鬆散相等(這通常表示它根本沒有屬性)。

document.all&&|| 一起使用時,它偽裝成假值。但是,它與 nullundefined 不同,因此它不是空值。所以當將 document.all?? 一起使用時,它的行為就像任何其他物件一樣。

document.all || true; // => true
document.all ?? true; // => HTMLAllCollection[]

支援空值合併 #