JS链式属性表达式取值与赋值实现详解

作者:函数茶馆

时间:2026-10-30

浏览:355

本文详解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链式取值和赋值的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章