JS链式属性表达式取值与赋值实现详解
本文详解JavaScript中链式属性表达式的取值与赋值实现。通过解析路径字符串为字段数组,利用reduce方法进行链式访问,并探讨push、concat及扩展运算符在数组合并时的性能差异。
何谓链式属性表达式?
举例,存在如下对象:
const obj = {a: {b: {c: 3} } };若需获取其中的c值,常规手段多样,如直接点号访问或解构赋值:
const c = obj.a.b.c;
const {a: {b: {c} } } = obj;然部分场景非由开发者主动取值,而是特定方法或类在执行过程中自动获取。试举一例:Vue2的响应式机制旨在拦截对象的getter与setter,于getter内收集依赖,在setter中触发依赖。关键在于如何判定哪些属性受依赖,Watcher类即接收表达式作为参数以获取属性值,属性访问后触发getter,进而收集该属性依赖,更新时执行相应回调。
const viewModel = {a: {n: { m } } };
new Watcher(viewModel, "a.n.m", (newVal, oldVal) => {
console.log("新的值--》", newVal);
console.log("老的值--》", oldVa1l);
});故前述示例中的 a.n.m 即为链式属性表达式,借由该表达式向方法指明需获取对象的特定属性。
此外,微信小程序的observer监听器及setData方法等均应用了链式属性表达式,其原理一致,皆通过链式属性表达式实现取值或赋值。
链式取值
首先明确需支持的数据类型:
// 对象,使用 . 访问
obj.a.b.c
// 数组,使用下标访问,要支持连续访问
arr[0][1][2]
// 对象数组嵌套混合
obj.a[0].b先将链式属性表达式(下文简称路径)解析为字段数组,以利于后续操作,例如:将路径obj.arr[0].a.b 解析为 ['obj', 'arr', 0, 'a', 'b']。
// 解析路径为字段数组
function parsePath(path: string) {
const segments: string[] = path.split('.'); // 分割字段片段
let fileds: Array<number | string> = []; // 保存字段名
if (path.includes('[')) { // 如果包含数组下标,收集数组索引 类似arr[0]这样的格式
for (const segment of segments) {
if (/^(\w+)(\[(\w+)\])+/.test(segment)) { // 匹配 类似 arr[0][1] 这种格式
const arrIndexs: number[] = [];
for (const item of segment.matchAll(/(\w*)\[(\w+)\]/g)) {
if (item[1]) fileds.push(item[1]); // 第一个匹配的括号,即数组字段名
arrIndexs.push(~~item[2]); // 第二个匹配的括号,即数组索引
}
fileds.push(...arrIndexs);
} else { // 如果是被'.'分割完的字段直接push
fileds.push(segment);
}
}
} else { // 无数组值时无需遍历,提高性能
fileds = segments;
}
return fileds;
}须注意一细节,数组合并方法存在性能差异。在编写工具、框架等对性能要求严苛的场景下,更推荐使用push,Array.prototype.push.apply(array1, array2)与array1.push(…array2)亦可,二者差距极小。
数组元素量级大且合并次数少时,性能对比: concat() > push() > […array1,…array2]
数组元素少但合并次数多时,性能对比: push() > concat() > […array1,…array2]
push()方法适用于10万级以下元素的数组合并,次数越多优势越明显;但push()惧怕数组元素过多,超过12万左右即报错,导致无法合并数组。concat()方法适用于数组元素量级大但合并次数少的情况,当数组合并频繁时性能表现略逊;[…array1, …array2]方法在大量级数组合并或数组频繁合并场景下均不占优,单从性能角度而言,此为最差方案,或许因其创建数组产生了较大开销。
综合对比结果: push() > concat() > […array1,…array2]
通常情况下,采用push()方法合并数组速度最快;concat()方法可支撑大量级数组合并;而[…array1,…array2]扩展运算符可读性更佳,若不考量性能因素可选用;
此处插播 String.prototype.matchAll()方法,部分读者可能存有疑惑:
matchAll() 方法返回一个包含所有匹配正则表达式结果及分组捕获组的迭代器。
const arr = [...'arr[0][1]'.matchAll(/(\w*)\[(\w+)\]/g)];
// arr[0]: ["arr[0]", "arr", "0", index: 0, input: "arr[0][1]", groups: undefined]
// arr[1]: ["[1]", "", "1", index: 6, input: "arr[0][1]", groups: undefined]将路径解析为字段数组后,即可利用 reduce 方法实现链式取值:
// 链式取值
function getValByPath(target: object, path: string): any {
if (!(/[\\.\\[]/.test(path))) return target[path]; // 如果没有 . 或 [ 符号说明非链式,直接返回属性值
const fileds = getPathFileds(path);
const val = fileds.reduce((pre, cur) => pre?.[cur], target); // 取不到的时候返回undefined
return val;
}上述方法未进行类型检查及默认值处理,请依实际需求调整。
至此,便可利用getValByPath方法依据链式属性表达式获取对象属性值。
链式赋值
赋值过程相对更为复杂。
// 链式赋值
function updateValByPath(target: object, path: string, value): void {
if (!(/[\\.\\[]/.test(path))) return target[path] = value; // 如果没有 . 或 [ 符号说明非链式,直接赋值
const fileds = getPathFileds(path);
// cosnt obj = {a: {b: {c: 6}}};
// 获取值引用 ,例如更新obj对象的c值,需要获取{c: 6}对象的引用,即obj.a.d = {c: 6},拿到引用后 ref.c = 8,就 {c: 6} 更新成 {c: 8} 了
const ref = fileds.slice(0, -1).reduce((pre, cur) => pre[cur], target); // 只遍历到倒数第二个字段,因为这个字段就是被修改对象的引用
if (ref) return ref[`${fileds.at(-1)}`] = value; // 拿到引用后,更新最后一个字段
// 如果引用对象不存在,提醒开发者不要更新不存在的属性
console.warn(`updated property "${path}" is not registered in data, you will not be able to get the value synchronously through "this.data"`);
}读者已察觉,前述方法仅能更新引用存在的情形,即被更新数据的父级对象必须存在。若需支持更复杂情况,当被更新属性无父级属性时,需为其创建父级对象(可能为对象类型或数组类型),这将额外消耗大量内存与性能。且随意操作对象不存在的属性不符合严谨的代码规范,不利于维护。感兴趣者可在此基础之上扩展,支持设置不存在的属性。
以上即为详解JS如何使用链式属性表达式取值和赋值的详细内容,更多关于JS链式取值和赋值的资料请关注本站其它相关文章!
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
JS使用constructor判断数据类型可靠吗
2026-11-05 13:49:41
