javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JS链式取值和赋值

JS使用链式属性表达式取值和赋值的实现方法

作者:CoderLiu

这篇文章主要给大家详细介绍了JS如何使用链式属性表达式取值和赋值,文章通过代码示例介绍的非常详细,对我们的学习或工作有一定的帮助,感兴趣的同学可以参考一下

什么是链式属性表达式?

例如,有这样一个对象:

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;
}

注意一个细节,数组合并的方法有性能差异,在写工具、框架等对性能要求高的情况下,更推荐使用pushArray.prototype.push.apply(array1, array2)array1.push(…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链式取值和赋值的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文