optional-chaining-loader

1.可选链是什么?解决了什么问题?

根据 MDN 文档

可选链操作符?.能够去读取一个被连接对象的深层次的属性的值而无需明确校验链条上每一个引用的有效性。?.运算符功能类似于.运算符,不同之处在于如果链条上的一个引用是nullish (nullundefined),.操作符会引起一个错误,?.操作符取而代之的是会按照短路计算的方式返回一个 undefined。当?.操作符用于函数调用时,如果该函数不存在也将会返回 undefined。

当访问链条上可能存在的属性却不存在时,?.操作符将会使表达式更短和更简单。当不能保证哪些属性是必需的时,?.操作符对于探索一个对象的内容是很有帮助的。

例子

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah'
  }
}

const dogName = adventurer.dog?.name
console.log(dogName)
// expected output: undefined

console.log(adventurer.someNonExistentMethod?.())
// expected output: undefined

更多语法可以参见 TC39

2.思路

const obj = { foo: { bar: { baz: 2 } } }
console.log(obj.foo.bar?.baz)
// 需要转换成 obj && obj.foo && obj.foo.bar && obj.foo.bar.baz

观察上面两个例子,可以发现三点

(1) 对于 x?.y,相当于 x && x.y

(2) 对于 x.y?.z,则相当于 x?.y?.z,也就是x && x.y && x.y.z,这是相比 TC39 不一样的地方

In a deeply nested chain like a?.b?.c, why should I write ?. at each level? Should I not be able to write the operator only once for the whole chain?

By design, we want the developer to be able to mark each place that they expect to be null/undefined, and only those. Indeed, we believe that an unexpected null/undefined value, being a symptom of a probable bug, should be reported as a TypeError rather than swept under the rug.

(3) 对于函数,fn?.() 相当于 fn && fn()

webpack loader

本质还是字符串的修改,只要拿到代码字符串就可以做,那么写一个 webpack-loader 做正则匹配也可行。

babel loader

可选链是 ES-next 的内容,容易想到用 babel。因此写一个 babel loader 操作 AST 应该也可以做。

@babel/plugin-proposal-optional-chaining

3.webpack loader 实现

正则匹配

之后,我们本质上修改字符串就可以达到这个效果,请出正则表达式就完事了。

匹配变量和.号,变量可以是字母、数字、美元符号($)和下划线

[\w\$_\.]

匹配.和?

\?\.

匹配函数后跟的( , [

判断是否为函数 fn?.(), 数组 arr?.[1] 或对象属性 obj?.['name']

[(\(\[]?

最终得到

/([\w\$_\?\.]+\?\.)[(\(\[]?/g

image-20200313101052251

根据 . 或者 ?. 去拆开变量,最后把他们套娃一样套起来。

x.y.z?. => x && x.y && x.y.z

再判断一下结尾,如果有 ([ 则需要补上括号,否则补个 .

代码实现

/**
 * webpack loader
 * Optional Chain Transformer
 *
 * @param {string} source 待处理代码字符串
 * @author Husiyuan
 */
function optionalChain(source) {
  const replacer = str => {
    // 判断是否为函数 fn?.(), 数组arr?.[1] 或对象属性, obj?.['name']
    const endBrackets = str[str.length - 1]
    const haveEndBrackets = endBrackets === '(' || endBrackets === '['

    // 去除末尾 ( 或 [,切分变量
    const varList = str.replace(/[\[\(]/g, '').split(/\.|\?\./)
    // 去除末尾空字符
    varList.pop()

    const defaultListIndex = 0
    let ret = varList[defaultListIndex]
    let pre = ret
    // 开始拼接操作
    for (let i = 1; i < varList.length; i++) {
      pre = pre + '.' + varList[i]
      ret += ' && ' + pre
    }
    ret += ' && ' + pre

    return ret + (haveEndBrackets ? endBrackets : '.')
  }

  return source.replace(/([\w\$_\?\.]+\?\.)[(\(\[]?/g, replacer)
}