TypeScript 3.7增加了对?. 操作符的支持,也被称为可选链操作符。我们可以使用可选的链式来下降到一个对象,其属性可能持有的值是null 或undefined ,而不需要为中间属性编写任何空检查。
可选链不是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);
}
我们也可以使用一个稍微通用的空值检查来代替,它将同时检查null 和undefined - 注意在这种情况下我们故意使用!= 而不是!== 。
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 变量将持有值undefined ,JSON.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持有值null或undefined,产生值undefined。 - 否则,如果
options.formatting持有的值是null或undefined,产生的值是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 分别改名为formatting 和getIndent 。
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);
}
最后,让我们把使用=== 对值null 和undefined 的检查合并成一个使用== 操作符的单一检查。除非我们在空值检查中处理特殊的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正在发出空值检查,如果我们不能使用可选的链式运算符,我们就会自己写。