前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【译】通过可选链操作符重构大型代码库的经验教训

【译】通过可选链操作符重构大型代码库的经验教训

作者头像
腾讯IVWEB团队
发布2020-07-14 11:10:24
7960
发布2020-07-14 11:10:24
举报

原文地址:Refactoring optional chaining into a large codebase: lessons learned 作者:Lea Verou

如今,可选链操作符已经被支持了。我决定用其来重构Mavo(当然了,还需要提供一个转译版本来适配不支持该特性的浏览器)。我等这一刻已经很久了,这是我认为自箭头函数和模板字符串以来最重要的一个语法改进,甚至比async/await还要重要。因为属性访问操作遍地都是,可选链操作符能够改进大量的代码。

首先,让我们来了解一下什么是可选链操作符。

我们知道,若不依次检查foofoo.barfoo.bar.baz是否存在就直接读取使用foo.bar.baz(),就可能会抛出错误。因此,我们通常会采用如下笨拙的方式进行判断:

代码语言:javascript
复制
if (foo && foo.bar && foo.bar.baz) {
    foo.bar.baz();
}

或者:

代码语言:javascript
复制
foo && foo.bar && foo.bar.baz && foo.bar.baz();

甚至有人通过对象解构来解决这个问题,而通过使用可选链操作符,就可以如下简写为:

代码语言:javascript
复制
foo?.bar?.baz?.()

其支持普通的属性访问、括号式访问(foo?.[bar]),甚至函数调用式(foo?.())。大多数场景下,这可以简化很多代码,但也有一些注意事项。

需要寻找的场景

如果决定重构代码,那么,需要针对哪些场景呢?

最简单明显的就是将foo && foo.bar优化成foo?.bar。另外还有其它条件判断的场景,譬如开头提到的通过if()来对调用链进行检查。除此之外,还有其它一些场景。

三元运算

代码语言:javascript
复制
foo? foo.bar : defaultValue

现在可改写成:

代码语言:javascript
复制
foo?.bar || defaultValue

或者采用另一个新的操作符——空值合并操作符:

代码语言:javascript
复制
foo?.bar ?? defaultValue

数组校验

代码语言:javascript
复制
if (foo.length > 3) {
	foo[2]
}

现在可改写成:

代码语言:javascript
复制
foo?.[2]

要注意,这不能替代真正的数组类型校验,如Array.isArray(foo)。不要因为这样比较简短,就采用鸭子类型的方式将真正的数组类型校验给替换了,我们十年前就不这样做了。

正则匹配

请忘记如下写法:

代码语言:javascript
复制
let match = "#C0FFEE".match(/#([A-Z]+)/i);
let hex = match && match[1];

还有这些:

代码语言:javascript
复制
let hex = ("#C0FFEE".match(/#([A-Z]+)/i) || [,])[1];

现在仅用即可:

代码语言:javascript
复制
let hex = "#C0FFEE".match(/#([A-Z]+)/i)?.[1];

在这种情况下,甚至可以删除两个实用函数并通过可选链操作符替换对应的引用。

特性检测

在简单的场景下,可选链操作符可用来替代特性检测,例如:

代码语言:javascript
复制
if (element.prepend) element.prepend(otherElement);

改为:

代码语言:javascript
复制
element.prepend?.(otherElement);

避免矫枉过正

虽然以下改写看起来很诱人:

代码语言:javascript
复制
if (foo) {
	something(foo.bar);
	somethingElse(foo.baz);
	andOneLastThing(foo.yolo);
}

改为:

代码语言:javascript
复制
something(foo?.bar);
somethingElse(foo?.baz);
andOneLastThing(foo?.yolo);

请不要这么做,这样会导致JS运行时对foo的检查从一次增加到三次。也许会有人说,这对性能的影响并不大。但是否考虑到,这对阅读该代码的人来说,同样会在头脑中进行三次重复的检查;另外,若想对foo添加其它属性的访问,就需要进行同样的检查,而不是仅仅使用已经存在的条件即可。

注意事项

赋值前的检查

兴许有人会想像以下这样转换:

代码语言:javascript
复制
if (foo && foo.bar) {
	foo.bar.baz = someValue;
}

改为:

代码语言:javascript
复制
foo?.bar?.baz = someValue;

很遗憾,这样会抛出错误。以下是我们代码库中的一个片段:

代码语言:javascript
复制
if (this.bar && this.bar.edit) {
	this.bar.edit.textContent = this._("edit");
}

我开心得将其改为:

代码语言:javascript
复制
if (this.bar?.edit) {
	this.bar.edit.textContent = this._("edit");
}

目前为止,这样能够正常运行。但转念一想,我为什么还需要判断条件呢,或许可以将其改写为:

代码语言:javascript
复制
this.bar?.edit?.textContent = this._("edit");

于是便抛出了Uncaught SyntaxError: Invalid left-hand side in assignment的错误。我们仍然需要判断条件,事实上,我也一直在这么做。很高兴在编辑器中可以通过ESLint进行及时的提醒,而不必等待实际运行代码的时候才发现错误。

错误或忘记使用可选链操作符

要注意,若通过可选链操作符重构一条很长的链,就需要给每个可能不存在的属性插入?.,否则一旦返回undefined就会抛出错误了。

亦或者,将?.插入到错误的地方。以下是一个真实例子,起初我是这样重构的:

代码语言:javascript
复制
this.children[index] ? this.children[index].element : this.marker

改为:

代码语言:javascript
复制
this.children?.[index].element ?? this.marker

然后就抛出了TypeError: Cannot read property 'element' of undefined的错误。然后,我通过多插入一个?.解决了该问题:

代码语言:javascript
复制
this.children?.[index]?.element ?? this.marker

虽然,这能正常运行,但评论中有人指出引入了多余的步骤。其实,只需要针对三元运算的判断条件移动?.即可:

代码语言:javascript
复制
this.children.[index]?.element ?? this.marker

正如评论中指出的,要小心使用通过可选链操作符来替代数组长度检查,进而进行索引访问,这可能会有损性能。因为对于数组越界访问,在V8引擎中会对代码进行反优化(其会去检查原型链是否也具有该属性,而不仅仅是确定数组中有没有某个索引)。

不细心带来的BUG

如果像我一样对项目进行如此重构,就很容易在某个点引入可选链操作符后,不经意间改变了代码功能并引入了难以察觉的BUG。

null vs undefined

通常来说,将foo && foo.bar替换为foo?.bar可能是最常见的场景。大多数情况下,这种方式的结果是一致的,但并不代表所有的情况。当foonull时,前者返回null,而后者返回undefined。在需要进行区分的场景下,就会引入BUG,这可能是通过这种方式进行重构时引入的最常见问题。

等值判断

请小心转换如下代码:

代码语言:javascript
复制
if (foo && bar && foo.prop1 === bar.prop2) { /* ... */ }

改为:

代码语言:javascript
复制
if (foo?.prop1 === bar?.prop2) { /* ... */ }

在第一种情况下,只有当foobar都为真的情况下,整个判断条件才有可能为真。然而在第二种情况下,如果当foobar都为空值,整个判断条件也将会是真,因为两边都返回了undefined。第二个值不使用可选链操作符,也可能出现该BUG,只要返回undefined就有意外相等的可能性。

运算符优先级

还有一件需要注意的事情就是可选链操作符的优先级高于&&,而相等/不等操作符的优先级低于?.而高于&&。当通过使用来?.替换&&时,若还涉及到相等检查,这点就变得十分重要。例如:

代码语言:javascript
复制
if (foo && foo.bar === baz) { /* ... */ }

请问这里的baz在和什么进行比较,foo.bar还是 foo && foo.bar?由于&&的优先级低于===,因此其等价于:

代码语言:javascript
复制
if (foo && (foo.bar === baz)) { /* ... */ }

如果这里foo为假,则整个条件判断内的语句将不会被执行。当我们通过可选链操作符进行重构后,就成为(foo && foo.bar)和baz进行比较。当baz为undefined时,就能看到不同语义下会得出不同的结果。例如:

代码语言:javascript
复制
if (foo?.bar === baz) { /* ... */ }

此时,若foo为空值时,可选链操作符语句将返回undefined,即整个判断条件为真,这基本上就和上边例子的结果一致。在其它大多数情况,这个场景也不会有太多不同。然而,当使用不等运算时,这就会变得十分糟糕。例如:

代码语言:javascript
复制
if (foo && foo.bar !== baz) { /* ... */ }

改为:

代码语言:javascript
复制
if (foo?.bar !== baz) { /* ... */ }

此时,当foo为空值、baz不为undefined时,整个判断条件就为真,和重构之前的表现结果并不一致。这种差异在边界情况下都不容易被察觉到,更别说一般情况了。

返回语句

当我们仔细考虑了很多种情况时,也会很容易忘记返回语句。譬如,我们不能进行如下的替换:

代码语言:javascript
复制
if (foo && foo.bar) {
	return foo.bar();
}

改为:

代码语言:javascript
复制
return foo?.bar?.();

在第一种情况下,返回值是有条件的;而在第二种情况下,任何时候都会有返回值。如果该逻辑是函数中的最后一段语句,将不会引入任何问题。若不是,则将改变代码执行的流程。

有时也能解决BUG

来看一看我在重构中遇到的这段代码:

代码语言:javascript
复制
/**
 * 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,因为空字符串将隐式转换为假值。而通过可选链操作符重写就能够解决这个问题:

代码语言:javascript
复制
if (element) {
	var value = getComputedStyle(element).getPropertyValue(property);

	return value?.trim();
}

现在,如果value是一个空字符串,该函数也会返回一个空字符串。其只会在value为空值时,才会返回undefined

后记

重构后,Mavo的资源大小轻便了2KB并减少了37行代码。然而,转译版本多了79行代码并加重了9KB大小。

这里是可供参考的相关提交记录。在此次提交中,我尽可能只引入了跟可选链操作符相关的代码。因此,显示的diff部分就可当做可选链操作符的示例。其中,有104行添加项和141行删除项,大约有100个可选链操作符的实践示例。

希望对大家有所帮助。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 需要寻找的场景
    • 三元运算
      • 数组校验
        • 正则匹配
          • 特性检测
            • 避免矫枉过正
            • 注意事项
              • 赋值前的检查
                • 错误或忘记使用可选链操作符
                  • 不细心带来的BUG
                    • null vs undefined
                    • 等值判断
                    • 运算符优先级
                    • 返回语句
                    • 有时也能解决BUG
                • 后记
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档