【译】为什么我喜欢JavaScript可选链式调用

3,622 阅读8分钟

原文标题:Why I like JavaScript Optional Chaining

原文链接:dmitripavlutin.com/javascript-…

JavaScript的新特性正在快速改变我们的代码。从ES2015到现在,对我的代码影响最大的新特性包括解构,箭头函数,class类以及模块系统。 在2019年8月份,一个新的提案optional chaining,直译为可选链式调用的新特性到达了stage 3,并且这个新特性将会是一个很棒的改进。可选链式调用改变了对象中深层属性的访问方式。 接下来让我们来看看在深层访问可能不存在的属性时,可选链式调用是如何通过移除模版条件和变量来是你的代码更加简单的。

1. 问题

由于JavaScript的动态特性,一个对象可以具有各种各样的嵌套解构。 通常,你会处理如下几种对象:

  • 获取远端JSON数据
  • 使用配置对象
  • 有可选属性的对象

当我们给予一个对象足够的灵活性以让它可以具有各种各样的嵌套结构的同时,随之而来的代价就是访问对象属性的复杂度的增加。 bigObject对象在运行时会具有不同版本的属性解构:

// 一种版本的bigObject对象
const bigObject = {
    // ...
    prop1:  {
        //...
        prop2: {
            //...
            value: "some value"
        }
    }
};

// 另一个版本的bigObject对象
const bigObject = {
    //...
    prop1: {
        // Nothing here
    }
}

遇到这种情况,之前的做法就是你必须手动判断属性是否存在:

if (bigObject && 
    bigObject.prop1 != null &&
    bigObject.prop1.prop2 != null) {
    let result = bigObject.prop1.prop2.value;
}

这里建议最好不要写这种类型的模版代码。接下来让我们看如何通过可选链式调用解决上述问题,减少模版条件。

2. 易如反掌的深层属性访问

让我们构思一个表示电影信息的对象。这个对象包含一个必须的title属性,以及可选的directoractors属性。 movieSmall对象仅包含必须的title属性,movieFull同时包含必须属性和可选属性:

const movieSmall = {
    title: "Heat"
};

const movieFull = {
    title: "Blade Runner",
    director: {
        name: "Ridley Scott",
    },
    actors: [{ name: "Harrison Ford" }, { name: "Rutger Hauer" }]
}

写一个函数来获取director属性值,需要注意的一点就是director属性可能不存在:

function getDirector (movie) {
    if (movie.director != null) {
        return movie.director.name;
    }
}
get Director(movieSmall);    //=> undefined
get Director(movieFull);    //=>'Ridley Scott'

if (movie.director != null)用于判断director属性是否被定义。这种做法是以防访问一个对象中未被定义的属性的子属性时,JavaScript会抛出一个错误TypeError: Cannot read property 'name' of undefined,例如访问movieSmalldirector属性的name属性就是这种情况。 如下所示是使用JS可选链式调用来移除对movie.director属性是否存在的判断条件的例子,这种方式写出来的getDirector()看起来更加简短:

function getDirector(movie) {
    return movie.director?.name;
}
get Director(movieSmall);    //=> undefined
get Director(movieFull);    //=>'Ridley Scott'

movie.director?.name表达式内部,你能发现?.:这就是可选链式调用。 在movieSmall的例子中,以防movieSmalldirector属性不存在,movie.director?.name给出了undefined值。可选链式调用成功防止了JS抛出TypeError: Cannot read property 'name' of undefined的错误。 相反的,在movieFull的例子中,director属性是存在的。movie.director?.name给出了正常的返回值Ridley Scott。 简单来说,可选链式调用的使用方法如下所示:

let name = movie.director?.name;

等价于:

let name;
if (movie.director != null) {
    name = movie.director.name;
}

?.getDirector()函数简化到了只需要两行代码。这就是我喜欢可选链式调用的原因。

2.1 数组元素

可选链式调用的应用不仅仅之前说到的那些,你可以在一个表达式中使用多个可选链式运算符。你还可以使用它来安全的访问数组元素。 接下来我们写一个函数,用于返回一部电影movie的演员表actors中的的第一个演员名name。 在一个movie object内部,actors数组可能为空数组甚至是没有这个属性,所以之前的做法都需要增加判断条件:

function getLeadingActor (movie) {
    if (movie.actors && movie.actors.length > 0) {
        return movie.actors[0].name;
    }
}

getLeadingActor(movieSmall);    //=> undefined
getLeadingActor(movieFull);    //=>'Harrison Ford'

if (movie.actors && movie.actors.length > 0)被用来确保movie包含actors属性,并且这个属性至少包含一个元素。 使用可选链式调用,这个安全性判断就变得很容易解决了:

function getLeadingActor (movie) {
        return movie.actors?.[0]?.name;
}

getLeadingActor(movieSmall);    //=> undefined
getLeadingActor(movieFull);    //=>'Harrison Ford'

actors?.确保存在actors属性。[0]?.确保列表中存在至少一个元素。

3. 配合无效值合并运算符

一个新特性被称作nullish coalescing operator,我自己把它翻译为无效值合并操作符(如果有更好的翻译请指出,感谢),这个操作符专门用来处理undefinednull,把它们转为指定值。 表达式variable ?? defaultValue会在variableundefinednull的时候把defaultValue的值赋给variable

const noValue = undefined;
const value = 'Hello';

noValue ?? 'Nothing';    //=> 'Nothing'
value ?? 'Nothing';    //=> 'Hello'

无效值合并操作符可以改善可选链式调用,做法就是在可选链式调用的解构为undefined时给一个默认值: 例如,我们可以改善getLeadingActor函数,在movie对象没有actors属性时,返回一个Unknown actor

function getLeadingActor (movie) {
        return movie.actors?.[0]?.name ?? 'Unknown actor';
}

getLeadingActor(movieSmall);    //=> undefined
getLeadingActor(movieFull);    //=>'Harrison Ford'

4. 可选链式调用的三类常见用法

常见的可选链式调用的使用方法大致包含三类。 第一种方式是使用object?.property来访问对象的静态属性:

const object = null;
object?.property; //=> undefined

第二种方式是使用object?.[expression]来访问一个数组或对象的动态属性:

const object = null;
const name = 'property';
object?.[name]; //=> undefined
const array = null;
array?.[0];    //=> undefined

第三种方式是使用object?.([arg1, [arg2, ...]])来调用对象的方法:

const object = null;
object?.method('Some value');    //=> undefined

如果有必要的话,可选链式调用是可以组合使用的:

const value = object.maybeUndefinedProp?.maybeNull()?.[propName];

5. 短路特性:遇到undefined或null时终止运算

可选链式调用运算符的有趣之处在于对于表达式leftHandSide?.rightHandSide,如果leftHandSide的值为无效值,那么rightHandSide的运算就不会执行,这种特性被称为短路特性。 看一个例子:

const noting = null;
let index = 0;

noting?.[index++];    //=> undefined;
index;    //=> 0

noting是一个无效值(undefined或null),可选链式调用运算符在左侧遇到无效值的情况下,就会跳过右侧的运算,所以最终的结果就是index的值没有增加。

6. 什么时候使用可选链式调用运算符?

一定要忍住访问任何类型的属性都使用可选链式调用运算符的冲动,否则将会造成滥用。接下来将说明如何正确的使用它。

6.1 访问可能为无效值的属性

?.必须在访问可能为无效值的属性时使用:maybeNullish?.prop。其余的情况,都应该使用之前的属性访问方式:.prop或者[propExpression]。 说回movie对象,在表达式movie.director?.name中,因为director可能为undefined,所以在访问director属性时使用可选链式调用操作符就是正确的。 相反的,在访问电影对象的title属性时使用?.就没什么意义,因为电影对象不会是一个无效值(译注:title在movie对象中是一个required属性)。

// Good
function logMovie (movie) {
    console.log(movie.director?.name);
    console.log(movie.title);
}

// Bad
function logMovie (movie) {
    // director需要使用可选链式调用符
    console.log(movie.director.name);
    // title属性没必要使用可选链式调用
    console.log(movie?.title);
}

6.2 通常有更好的选择

下面的例子中,函数hasPadding()接收一个style对象作为参数,这个参数有一个可选属性padding。这个padding属性又包含可选属性:top、right、bottom、left。 在这个函数中很自然的想到使用可选链式调用符:

function hasPadding ({ padding }) {
    const top = padding?.top ?? 0;
    const right = padding?.right ?? 0;
    const bottom = padding?.bottom ?? 0;
    const left = padding?.left ?? 0;
    return top + left + right + bottom !== 0;
}

hasPadding({ color: 'black' });    //=> false
hasPadding({ padding: {left: 0} });    //=> false
hasPadding({ padding: {right: 10} });    //=>true

虽然上述例子可以正确判断一个元素是否具有padding,但是对于每一个属性都使用可选链式调用仍然不是一种好的选择。 更好的选择是使用展开运算符将padding对象展开并给一个0值的默认值:

function hasPadding ({ padding }) {
    const p = {
        top: 0,
        right: 0,
        bottom: 0,
        left: 0,
        ...padding,
    };
    return p.top + p.left + p.right + p.bottom !== 0;
}

hasPadding({ color: 'black' });    //=> false
hasPadding({ padding: {left: 0} });    //=> false
hasPadding({ padding: {right: 10} });    //=>true

在我看来,使用展开运算符版本的hasPadding()更加的简单易懂。

7. 为什么我喜欢它?

我喜欢可选链式调用符的原因是因为使用它来访问深层嵌套对象的属性非常方便。使用它就可以避免写一长处串的验证条件来校验访问链上的每一个属性。 当可选链式调用需要配合无效值合并运算符使用时,也许有更好的选择来轻松的处理默认值。 如果你还知道别的可选链式调用的好用例,请在评论中给出。