问题还原

这是最近 CR 的时候在业务代码中发现了一个问题,先来看一下问题代码:

// data 为接口返回的数据
const { bizObject = {}, total = 0 } = data.result || {};
const list = bizObject.list || [];

// 其他逻辑,比如把 list 更新到 state 中,等等
  • A 接口正常的情况:data 中有 result 属性,且 result 对象中 bizObject 返回了一个数—— ✅
  • B 接口异常:data 对象中没有 result 属性 —— ✅
  • C 接口异常:data 中有 result 属性,result 对象中也有 bizObject 属性,但是,bizObject 属性的值是 null,然后呢?

从上下文来看,这位同学应该是期望解构赋值按以下方式执行:

const result = data.result || {};
const bizObject = result.bizObject || {};
// ...

但是,C 情形抛异常了:

Uncaught TypeError: Cannot read property 'list' of null

也就是 bizObject 的值是 null 而不是期望的 {}。为什么呢?

解构赋值中的默认值

A variable can be assigned a default, in the case that the value unpacked from the object/array is undefined.

数组、对象解构赋值时,只有当属性(数组索引对应的值)值为 undefined 时,才会使用默认值。

问题代码中,当 bizObjectnull 时,解构出来的就是 null,读取 nulllist 属性,不报错才怪。

函数默认参数

再来看看函数的默认参数是不是同样的逻辑:

function doSomething(options = { foo: 'bar' }) {
  console.log(options);
}

doSomething(); // { foo: "bar" }
doSomething(undefined); // { foo: "bar" }
doSomething(null); // null

不传参数(隐式的 undefined)或者显示地传递 undefined,使用了默认参数,传 null 的时候没有使用默认值,和解构赋值的默认值同样的逻辑。

其实把上面的函数转成 ES5,就能直观地了解其逻辑:

function doSomething() {
  var options =
    arguments.length > 0 && arguments[0] !== undefined
      ? arguments[0]
      : {
          foo: "bar"
        };
  console.log(options);
}

解构赋值的默认值也一样:

// ES6
const { a = 1 } = { };

// 转换成 ES5
var _ref = {},
  _ref$a = _ref.a,
  a = _ref$a === void 0 ? 1 : _ref$a;

写在最后

之所以会有同学把解构赋值默认值等同于 const bizObject = result.bizObject || {},可能是对 ES6 的一些细节了解得不够透彻,可以多翻翻文档:

还有一个可能,前端同学并没有误解解构赋值默认值的工作原理,只是接口不规范引发了异常。一般而言,接口约定好字段、类型后,就应该始终按约定的类型返回数据,约定的是对象,那没有数据的时候也应该返回一个空对象,即使不返回这个字段,前端也已经判断了,莫名其妙地返回一个 null 算哪门子事

接口写得有问题,有的人沟通一下还是会调整,有的人就始终一副「放荡不羁」样子,通了就行,才不管你什么规范、约定……对于不讲究的人,还是自己多写两行代码判断一下,说多了也是浪费,你懂的。

很多事情都是 100% 的期望,然后妥协,接受一个差不多的结果。(BGM 差不多先生 - MC Hot Dog