一、前言
业务开发过程中,避免不了操作复杂的嵌套对象,而对于这种场景通常会遇到两个痛点:
- 操作嵌套对象时缺少健壮性处理,从而触发运行时错误,进而导致页面白屏
- 健壮性处理的方式不够优雅,出现大量臃肿的代码块
二、可选链操作符
ECMA 新的可选链操作符规范允许读取位于对象链深处的属性,而不必明确验证链中的每个引用是否有效。
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah'
}
};
adventurer.someNonExistentMethod?.(); // undefined
adventurer.dog?.name; // undefined
而不采用可选链操作符的情况下,浏览器会直接抛出如下异常:
Uncaught TypeError: adventurer.someNonExistentMethod is not a function
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
可选链操作符在引用为 null 或者 undefined 的情况下不会引起错误,该表达式会短路返回 undefined。
let potentiallyNullObj = null;
let x = 0;
let prop = potentiallyNullObj?.[x++];
console.log(x); // x 将不会被递增,依旧输出 0
例如上述代码中,x++ 表达式并不会被执行,这也算是 JavaScript 内部优化的一种方式。
可选链操作符是可以与函数结合的,但是可能会出现这种情况:
const foo = {
customMethod: () => {}
}
foo.customMethod = 1;
foo?.customMethod?.(); // foo?.customMethod is not a function
所以在和函数结合的场景下,要能够确保方法属性不会被篡改,否则还是需要使用 typeof 来保证代码的健壮性。
在前端的开发场景下,大部分情况下都是需要将数据映射到 UI 上,直接显示一个 undefined 肯定是不合适的,所以规范也提供了空值合并操作符。
let customer = {
name: "Carl",
details: { age: 82 }
};
let customerCity = customer?.city ?? "暗之城";
console.log(customerCity); // “暗之城”
而残酷的开发现实中,服务端可能对于没有数据的字段会直接返回 null!!!所以这种不确定的场景下,使用逻辑或操作符(||)会更加稳妥。
三、babel 编译
因为可选链操作符目前处于 Stage 4,所以只能通过 Babel 或者 TypeScript 来编译该特性代码。
const foo = {}
foo?.bar?.name
采用 Babel 编译上述代码,可以得到如下内容:
var _foo$bar;
const foo = {};
foo === null || foo === void 0 ? void 0 : (_foo$bar = foo.bar) === null || _foo$bar === void 0 ? void 0 : _foo$bar.name;
实现的重点如下:
- 利用逻辑或操作符实现可选链操作符的短路计算特性
- 利用临时变量模拟对象链的每个引用
可选链操作符并不会创建临时变量。
四、设置对象链深处的属性值
前文提到可选链操作符只能读取对象链深处的属性,但是在业务开发中,避免不了去设置对象链深处的属性。
首先需要定义一个协议来约定对象链的引用方式:
dotProp.set(object, 'foo.baz', 'x');
接下来只需要针对分隔符来解析出每一级引用的属性名即可:
const getPathSegments = path => path.split('.');
为了增强代码的健壮性,可以针对解析后的路径再进行一次合法性校验:
const disallowedKeys = new Set([
'__proto__',
'prototype',
'constructor'
]);
const isValidPath = pathSegments => !pathSegments.some(segment => disallowedKeys.has(segment));
const getPathSegments = path => {
const paths = path.split('.');
if (!isValidPath(paths)) {
return [];
}
return paths;
};
有了对象链的引用路径之后,只需要找到最终的赋值节点即可,特别需要注意的就是在遍历过程中确保上一级引用为对象,从而避免 TypeError 异常的发生。
set(object, path, value) {
if (!isObject(object) || typeof path !== 'string') {
return object;
}
const root = object;
const pathArray = getPathSegments(path);
for (let i = 0; i < pathArray.length; i++) {
const p = pathArray[i];
if (!isObject(object[p])) {
object[p] = {};
}
if (i === pathArray.length - 1) {
object[p] = value;
}
object = object[p];
}
return root;
}
上述代码虽然实现了设置对象链深处属性值的功能,但是它的缺点也很明显,缺少 IDE 的智能提示。很有可能会出现由于拼写错误导致预期之外的运行结果。
对于对象链协议的约定也可以参考 Ramda 库简单粗暴的方式:
const ramda = require('ramda');
const obj = {
a: {
b: [1, 2, 3]
}
}
ramda.assocPath(['a', 'b', 1], 20, obj); // { a: { b: [ 1, 20, 3 ] } }
五、写在最后
参考文献及相关源码:
最后,如果本文对您有帮助,欢迎点赞、收藏、分享。