如何用可选链重构你的代码 | ES2020

目录

    • 重构哪里?
        • 三元运算符
        • 数组检查
        • 正则匹配
        • 属性检测
        • 不要滥用
    • 注意事项
        • 需要在声明前检查的情况
        • 把`?.`放错位置,或者忘记`?.`
        • 要小心可能会引起bug的情况
          • null vs. undefined
          • 全等检测
          • 操作符优先级
          • 返回语句
        • 同样,可选链也能修复一些bug!

首先,什么是可选链(optional-chaining),这是在ES2020中引入的新特性,在typescript v3.7中也已经支持。

要知道,你不能只在代码中写foo.bar.baz(),而不检查foo是否存在,然后检查foo.bar是否存在,再检查foo.bar.baz是否存在,否则你可能会收到一个异常。所以你的代码必须类似于:

if (foo && foo.bar && foo.bar.baz) {
	foo.bar.baz();
}

或者:

foo && foo.bar && foo.bar.baz && foo.bar.baz();

但如果有了可选链,你就可以直接这样做:

foo?.bar?.baz?.()

它支持正常的属性访问、括号(foo?.[bar]),甚至是函数调用(foo?.());

可选链还可以简化其他很多代码,同时也有一些需要注意的地方。

重构哪里?

假设你决定要重构你的代码,要找什么呢?

当然明显的有foo && foo.bar,转为foo?.bar

还有上文所述if语句中的代码。

另外还有一些情况:

三元运算符

foo ? foo.bar : defaultValue

重构后:

foo?.bar || defaultValue

或者用ES2020中另一个新的运算符,空值合并运算符(Nullish Coalescing Operator):

foo?.bar ?? defaultValue

数组检查

if (foo.length > 3) {
	foo[2]
}

重构后:

foo?.[2]

请注意,这并不能替代真正的数组检查,比如Array.isArray(foo)

正则匹配

let match = "#C0FFEE".match(/#([A-Z]+)/i);
let hex = match && match[1];

或者

let hex = ("#C0FFEE".match(/#([A-Z]+)/i) || [,])[1];

重构后:

let hex = "#C0FFEE".match(/#([A-Z]+)/i)?.[1];

属性检测

if (element.prepend) element.prepend(otherElement);

重构后:

element.prepend?.(otherElement);

不要滥用

虽然你可能很想重构这样一段代码:

if (foo) {
	something(foo.bar);
	somethingElse(foo.baz);
	andOneLastThing(foo.yolo);
}

转为:

something(foo?.bar);
somethingElse(foo?.baz);
andOneLastThing(foo?.yolo);

不要这样做。因为这样会让JS在运行时检查foo三次而不是一次。你可能觉得这在性能上并不那么重要,但对于阅读代码的人来说,也会存在同样的重复:他们必须在心理上处理三次对foo的检查,而不是一次。如果他们要在另一个语句上使用foo的属性,他们需要另外添加一个检查,而不是使用已有的条件。

注意事项

需要在声明前检查的情况

你可能会将下面的代码:

if (foo && foo.bar) {
	foo.bar.baz = someValue;
}

重构为:

foo?.bar?.baz = someValue;

然而,这样做会出错。比如,将下面的代码:

if (this.bar && this.bar.edit) {
	this.bar.edit.textContent = this._("edit");
}

重构为:

if (this.bar?.edit) {
	this.bar.edit.textContent = this._("edit");
}

目前为止一切都还好,直到代码进一步重构成这样:

this.bar?.edit?.textContent = this._("edit");

这会抛出这样的异常:Uncaught SyntaxError: Invalid left-hand side in assignment。所以你仍然需要条件,你可以使用ESLint来防止这样的错误在运行代码后产生。

?.放错位置,或者忘记?.

值得注意的是,当你重构一个带有可选链的长链时,你往往需要在第一个链之后插入多个 ?.,否则一旦可选链返回undefined,你的代码就会报错。

或者,有时是因为你把?.放错了位置。

比如下面这段代码:

this.children[index]? this.children[index].element : this.marker

重构为:

this.children?.[index].element ?? this.marker

那么你会得到这样一个报错:TypeError: Cannot read property 'element' of undefined.这样因为忘记了一个?.导致的:

this.children?.[index]?.element ?? this.marker

这样是可行的,不过回顾下重构前的代码,有一个?.是多余的:

this.children[index]?.element ?? this.marker

请注意,当用可选链代替数组长度检查时,可能会对性能造成影响,因为数组的越界访问会影响V8执行代码的效率(因为它也要检查像原型链的这种属性,而不仅仅是判断数组中有没有指定的索引)。

要小心可能会引起bug的情况

当你大肆进行重构到了一定程度后,可能很容易在一些地方只引入可选链,而实际上它将改变你代码的作用,引发一些难以察觉的bug。

null vs. undefined

最常见的情况可能是将foo && foo.bar替换为foo?.bar。虽然在大多数情况下,这两个函数的工作原理是相同的,但并不是每一种情况都是如此。当foonull时,前者会返回null,而后者返回undefined。这可能会导致在精准区分的情况下出现bug,这也是这种情况下重构最常见的引起的bug。

全等检测

比如将下面这段代码:

if (foo && bar && foo.prop1 === bar.prop2) { /* ... */ }

转为:

if (foo?.prop1 === bar?.prop2) { /* ... */ }

在第一种情况下,只有foo和bar都为真时条件才为真。但在第二种情况下,如果foobar都为undefined,那么条件也会为真。

操作符优先级

需要注意的是,可选链的优先级高于&&。尤其当你重构一个包含&&和平等运算符的表达式时,需要额外注意,因为平等运算符相对于?.&&而言,其优先级低于前者,高于后者。

if (foo && foo.bar === baz) { /* ... */ }

这里baz在和谁比较? foo.bar还是foo && foo.bar?因为&&的优先级比===低,所以是这样:

if (foo && (foo.bar === baz)) { /* ... */ }

在上面的代码中,如果foo是假值,条件就永远不能被执行。然而,一旦我们将其重构为可选链的形式,就会变成比较foo && foo.barbaz

if (foo?.bar === baz) { /* ... */ }

一个明显的问题是,当 bazundefined时,我们可以在foonullish的时候进入条件语句,这和我们的初衷相违和。

另一种更糟糕的情况是出现不等式操作符时,因为它和平等操作符具有相同的优先级。比如:

if (foo && foo.bar !== baz) { /* ... */ }

转为:

if (foo?.bar !== baz) { /* ... */ }

现在,当baz不是undefined,只要foonullish时就会进入条件语句!

返回语句

在重构返回语句时,你要当心这样的替换:

if (foo && foo.bar) {
	return foo.bar();
}

转为:

return foo?.bar?.();

在第一种情况下,代码会有条件地返回,但在第二种情况下,代码总是会返回。如果条件是函数中的最后一条语句,这不会有任何问题,但如果不是,就会改变控制流程。

同样,可选链也能修复一些bug!

来看看这段代码:

/**
 * Get the current value of a CSS property on an element
 */
getStyle: (element, property) => {
	if (element) {
		var value = getComputedStyle(element).getPropertyValue(property);

		if (value) {
			return value.trim();
		}
	}
},

发现这段代码的错误了吗?如果value是一个空字符串(根据上下文,它很可能是),函数将返回undefined,因为空字符串是个假值。我们使用可选链来解决这个问题:

if (element) {
	var value = getComputedStyle(element).getPropertyValue(property);

	return value?.trim();
}

现在,如果value是空字符串,它就会支持返回一个空字符串,而且只有当valuenullish时,它才会返回undefined

注:这里记录着更多的重构实例

原文-refactoring-optional-chaining-into-a-large-codebase-lessons-learned

你可能感兴趣的:(javascript)