可选的链式、符号|TypeScript中的操作符

403 阅读8分钟

TypeScript 3.7增加了对?. 操作符的支持,也被称为可选链操作符。我们可以使用可选的链式来下降到一个对象,其属性可能持有的值是nullundefined ,而不需要为中间属性编写任何空检查。

可选链不是TypeScript特有的功能。?. 操作符被添加到ECMAScript标准中,作为ES2020的一部分。所有现代浏览器都支持可选链(不包括IE11)。

在这篇文章中,我将介绍以下三个可选的链式运算符,并解释为什么我们可能想在TypeScript或JavaScript代码中使用它们。

  • ?.
  • ?.[]
  • ?.()

激励

让我们先看看一个现实世界的例子,在这个例子中,可选的链式运算会派上用场。我定义了一个serializeJSON ,该函数接收任何值并将其序列化为JSON。我将一个带有两个属性的用户对象传递给该函数。

function serializeJSON(value: any) {
  return JSON.stringify(value);
}

const user = {
  name: "Marius Schulz",
  twitter: "mariusschulz",
};

const json = serializeJSON(user);

console.log(json);

该程序在控制台打印出以下输出。

{"name":"Marius Schulz","twitter":"mariusschulz"}

现在我们说,我们想让我们的函数的调用者指定缩进程度。我们将定义一个SerializationOptions 类型,并向serializeJSON 函数添加一个options 参数。我们将从options.formatting.indent 属性中获取缩进程度。

type SerializationOptions = {
  formatting: {
    indent: number;
  };
};

function serializeJSON(value: any, options: SerializationOptions) {
  const indent = options.formatting.indent;
  return JSON.stringify(value, null, indent);
}

现在我们可以在这样调用serializeJSON ,指定缩进程度为两个空格。

const user = {
  name: "Marius Schulz",
  twitter: "mariusschulz",
};

const json = serializeJSON(user, {
  formatting: {
    indent: 2,
  },
});

console.log(json);

正如我们所期望的那样,现在生成的JSON缩进了两个空格,并分成了多行。

{
  "name": "Marius Schulz",
  "twitter": "mariusschulz"
}

通常情况下,像我们在这里介绍的options 参数是可选的。函数的调用者可以指定一个选项对象,但他们不需要这样做。让我们相应地调整我们的函数签名,通过在参数名称后面加上一个问号,使options 参数成为可选项。

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options.formatting.indent;
  return JSON.stringify(value, null, indent);
}

假设我们在TypeScript项目中启用了--strictNullChecks 选项(这是编译器选项--strict 系列的一部分),TypeScript现在应该在我们的options.formatting.indent 表达式中报告以下类型错误。

对象可能是 "未定义"。

options 参数是可选的,因此它可能持有值undefined 。我们应该在访问options.formatting 之前首先检查options 是否持有值undefined ,否则我们有可能在运行时得到一个错误。

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options !== undefined
    ? options.formatting.indent
    : undefined;
  return JSON.stringify(value, null, indent);
}

我们也可以使用一个稍微通用的空值检查来代替,它将同时检查nullundefined - 注意在这种情况下我们故意使用!= 而不是!==

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options != null
    ? options.formatting.indent
    : undefined;
  return JSON.stringify(value, null, indent);
}

现在类型错误消失了。我们可以调用serializeJSON 函数,并将一个带有明确缩进程度的选项对象传递给它。

const json = serializeJSON(user, {
  formatting: {
    indent: 2,
  },
});

或者我们可以在不指定选项对象的情况下调用它,在这种情况下,indent 变量将持有值undefinedJSON.stringify 将使用默认的缩进程度为零。

const json = serializeJSON(user);

上述两个函数调用都是类型正确的。然而,如果我们也希望能够像这样调用我们的serializeJSON 函数呢?

const json = serializeJSON(user, {});

这是你会看到的另一个常见模式。选项对象倾向于将其部分或全部属性声明为可选的,这样函数的调用者可以根据需要指定尽可能多(或尽可能少)的选项。为了支持这种模式,我们需要在我们的SerializationOptions 类型中使formatting 属性成为可选项。

type SerializationOptions = {
  formatting?: {
    indent: number;
  };
};

注意formatting 属性名称后面的问号。现在,serializeJSON(user, {}) 的调用是类型正确的,但 TypeScript 在访问options.formatting.indent 时报告了另一个类型错误。

对象可能是 "未定义"。

我们需要在这里添加另一个null检查,因为options.formatting 现在可以持有值undefined

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options != null
    ? options.formatting != null
      ? options.formatting.indent
      : undefined
    : undefined;
  return JSON.stringify(value, null, indent);
}

这段代码现在是类型正确的,它安全地访问了options.formatting.indent 属性。不过这些嵌套的空值检查已经变得很不方便了,所以让我们看看如何使用可选的链式操作符来简化这个属性访问。

?. Operator:点状符号

我们可以使用?. 操作符来访问options.formatting.indent ,在这个属性链的每一层都要检查空值。

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options?.formatting?.indent;
  return JSON.stringify(value, null, indent);
}

ECMAScript规范对可选链的描述如下。

可选链[是]一个属性访问和函数调用操作符,如果要访问/调用的值是空的,就会出现短路。

JavaScript运行时对options?.formatting?.indent 表达式的评估如下。

  • 如果options 持有值nullundefined ,产生值undefined
  • 否则,如果options.formatting 持有的值是nullundefined ,产生的值是undefined
  • 否则,产生值options.formatting.indent

请注意,当?. 操作符停止下降到一个属性链时,总是产生值undefined ,即使它遇到了值null 。TypeScript在其类型系统中模拟了这种行为。在下面的例子中,TypeScript推断出indent 这个局部变量的类型是number | undefined

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options?.formatting?.indent;
  return JSON.stringify(value, null, indent);
}

多亏了可选链,这段代码更加简洁,而且和以前一样类型安全。

The ?.[] Operator:方括号符号

接下来,让我们来看看?.[] 操作符,这是可选链中的另一个操作符。

假设我们在SerializationOptions 类型上的indent 属性被称为indent-level 。我们需要使用引号来定义一个名称中带有连字符的属性。

type SerializationOptions = {
  formatting?: {
    "indent-level": number;
  };
};

现在我们可以在调用serializeJSON 函数时,像这样为indent-level 属性指定一个值。

const json = serializeJSON(user, {
  formatting: {
    "indent-level": 2,
  },
});

然而,下面试图使用可选的链式访问indent-level 属性是一个语法错误。

const indent = options?.formatting?."indent-level";

我们不能直接使用?. 操作符,后面跟着一个字符串字面,这将是无效的语法。相反,我们可以使用可选链的括号符号,使用?.[] 操作符访问indent-level 属性。

const indent = options?.formatting?.["indent-level"];

下面是我们完整的serializeJSON 函数。

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options?.formatting?.["indent-level"];
  return JSON.stringify(value, null, indent);
}

除了为最后的属性访问添加方括号外,它和之前的内容基本相同。

?.() operator:方法调用

可选链系列中的第三个也是最后一个操作符是?.() 。我们可以使用?.() 操作符来调用一个可能不存在的方法。

为了了解这个操作符何时有用,让我们再一次改变我们的SerializationOptions 类型。我们将用一个getIndent 属性(类型为返回数字的无参数函数)取代indent 属性(类型为数字)。

type SerializationOptions = {
  formatting?: {
    getIndent?: () => number;
  };
};

我们可以调用我们的serializeJSON 函数并指定缩进程度为2,如下所示。

const json = serializeJSON(user, {
  formatting: {
    getIndent: () => 2,
  },
});

为了在我们的serializeJSON 函数中获得缩进级别,我们可以使用?.() 操作符来有条件地调用getIndent 方法,如果(并且仅当)它被定义。

const indent = options?.formatting?.getIndent?.();

如果getIndent 方法没有被定义,就不会试图调用它。在这种情况下,整个属性链将评估为undefined ,避免了臭名昭著的 "getIndent不是一个函数 "错误。

下面是我们的完整的serializeJSON 函数,再一次。

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options?.formatting?.getIndent?.();
  return JSON.stringify(value, null, indent);
}

将可选链编译成较旧的JavaScript

现在我们已经看到了可选链式运算符是如何工作的,以及它们是如何被类型检查的,让我们看看TypeScript编译器在针对旧的JavaScript版本时发出的编译JavaScript。

下面是TypeScript编译器发出的JavaScript代码,为了便于阅读,调整了空白部分。

function serializeJSON(value, options) {
  var _a, _b;
  var indent =
    (_b =
      (_a =
        options === null || options === void 0
          ? void 0
          : options.formatting) === null || _a === void 0
        ? void 0
        : _a.getIndent) === null || _b === void 0
      ? void 0
      : _b.call(_a);
  return JSON.stringify(value, null, indent);
}

在对indent 变量的赋值中,有很多事情要做。让我们一步一步地简化代码。我们首先将局部变量_a_b 分别改名为formattinggetIndent

function serializeJSON(value, options) {
  var formatting, getIndent;
  var indent =
    (getIndent =
      (formatting =
        options === null || options === void 0
          ? void 0
          : options.formatting) === null || formatting === void 0
        ? void 0
        : formatting.getIndent) === null || getIndent === void 0
      ? void 0
      : getIndent.call(formatting);
  return JSON.stringify(value, null, indent);
}

接下来,让我们处理一下void 0 的表达式。void 操作符总是产生值undefined ,不管它应用于什么值。我们可以直接用值undefined 来替换void 0 表达式。

function serializeJSON(value, options) {
  var formatting, getIndent;
  var indent =
    (getIndent =
      (formatting =
        options === null || options === undefined
          ? undefined
          : options.formatting) === null || formatting === undefined
        ? undefined
        : formatting.getIndent) === null || getIndent === undefined
      ? undefined
      : getIndent.call(formatting);
  return JSON.stringify(value, null, indent);
}

接下来,让我们把对formatting 变量的赋值提取到一个单独的语句中。

function serializeJSON(value, options) {
  var formatting =
    options === null || options === undefined
      ? undefined
      : options.formatting;

  var getIndent;
  var indent =
    (getIndent =
      formatting === null || formatting === undefined
        ? undefined
        : formatting.getIndent) === null || getIndent === undefined
      ? undefined
      : getIndent.call(formatting);
  return JSON.stringify(value, null, indent);
}

让我们用同样的方法处理对getIndent 的赋值,并添加一些空白。

function serializeJSON(value, options) {
  var formatting =
    options === null || options === undefined
      ? undefined
      : options.formatting;

  var getIndent =
    formatting === null || formatting === undefined
      ? undefined
      : formatting.getIndent;

  var indent =
    getIndent === null || getIndent === undefined
      ? undefined
      : getIndent.call(formatting);

  return JSON.stringify(value, null, indent);
}

最后,让我们把使用=== 对值nullundefined 的检查合并成一个使用== 操作符的单一检查。除非我们在空值检查中处理特殊的document.all,否则这两者是等价的。

function serializeJSON(value, options) {
  var formatting = options == null
    ? undefined
    : options.formatting;

  var getIndent = formatting == null
    ? undefined
    : formatting.getIndent;

  var indent = getIndent == null
    ? undefined
    : getIndent.call(formatting);

  return JSON.stringify(value, null, indent);
}

现在,代码的结构就更明显了。你可以看到,TypeScript正在发出空值检查,如果我们不能使用可选的链式运算符,我们就会自己写。