空值合并运算符

发布日期 · 标签: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[]

对空值合并运算符的支持 #