SoFunction
Updated on 2025-02-28

JS implementation method of using chain attribute expressions to obtain values ​​and assignments

What is a chain property expression?

For example, there is an object like this:

const obj = {a: {b: {c: 3} } };

We want to get the value of c. Under normal circumstances, we have many ways to get the value, such as direct points or using deconstruction, etc.

const c = ;
const {a: {b: {c} } } = obj;

However, there are some situations where we do not take the initiative to get the value, but a method or class will get the value when it is executed. For example:Vue2The responsive principle is to hijack the object's getters and setters, collect dependencies in getters, and trigger dependencies in setters. So how to determine which attributes are dependent?WatcherThis class accepts expressions as parameters, which gets the property value. After the property is accessed, the property's dependencies are collected and a callback will be executed when it is updated.

const viewModel = {a: {n: { m } } };
new Watcher(viewModel, "", (newVal, oldVal) => {
  ("New Value--"", newVal);
  ("Old value--"", oldVa1l);
});

Then in the above exampleIt is a chain attribute expression, which tells the method which attribute of the object to obtain through the chain attribute expression.

There is also a WeChat mini programobserverListener,setDataMethods, etc., are applied to chain attribute expressions, and the principles are the same, and values ​​are obtained or assigned through chain attribute expressions.

Chain value

Let’s take a look at the data types to support:

// Object, use . Access
// Array, accessed using subscript, and must support continuous accessarr[0][1][2]
// Object array nesting mix[0].b

First parse the chain attribute expression (hereinafter referred to as path) into a field array to facilitate subsequent operations, such as:[0].Analyze it into['obj', 'arr', 0, 'a', 'b']

// parse the path as a field arrayfunction parsePath(path: string) {
  const segments: string[] = ('.'); // Split field fragment  let fileds: Array<number | string> = []; // Save the field name  if (('[')) { // If the array subscript is included, collect the array index. Formats like arr[0] are similar to arr[0]    for (const segment of segments) {
      if (/^(\w+)(\[(\w+)\])+/.test(segment)) { // Match formats like arr[0][1]        const arrIndexs: number[] = [];
        for (const item of (/(\w*)\[(\w+)\]/g)) {
          if (item[1]) (item[1]); // The first matching bracket is the array field name          (~~item[2]); // The second matching bracket, that is, the array index        }
        (...arrIndexs);
      } else { // If the field is divided by '.', it is pushed directly        (segment);
      }
    }
  } else { // No traversal is required when there are no arrays of values, improving performance    fileds = segments;
  }
  return fileds;
}

Pay attention to a detail. There are performance differences in the method of combining and combining the numbers. If the performance requirements of writing tools, frameworks, etc. are highly recommended.push(array1, array2)and(…array2)All is OK, the difference between these two is very small.

  • Performance comparison of array elements when the order of magnitude is large and the number of merges is small:
    concat() > push() > […array1,…array2]

  • Performance comparison when there are fewer array elements but many merges:
    push() > concat() > […array1,…array2]

  • push()The method is suitable for combining elements below 100,000. The more times the better it is, the better it is. However, push() is afraid that there are many elements in the array, and an error will be reported after more than 120,000, which will lead to the inability to merge the array.

  • concat()The method is suitable for situations where array elements are of large order but few merges, and performance is slightly worse when the number combination is frequently combined;

  • […array1, …array2]Whether it is a combination of large series and frequent array merging, the method does not have an advantage. In terms of performance alone, it is the worst one. Could it be because it creates an array with a large overhead.

To sum up:push() > concat() > […array1,…array2]

Generally speaking, using push() method to merge arrays is the fastest method. The concat() method can support the combination of a large number of series, while the […array1,…array2] extension operator is readable and can be used without considering performance;

Insert it()Methods, you may not understand:

matchAll()The method returns an iterator containing all the results matching regular expressions and group capture groups.

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]

After parsing the path into a field array, you can use itreduceThe method is to get the value in chain form:

// Chain valuefunction getValByPath(target: object, path: string): any {
  if (!(/[\\.\\[]/.test(path))) return target[path]; // If there is no . or [ symbol description is non-chained, directly return the attribute value  const fileds = getPathFileds(path);
  const val = ((pre, cur) => pre?.[cur], target); // Return undefined when it cannot be retrieved  return val;
}

The above method does not perform type checks and default values, etc., just do it according to your own needs.

At this point, we can use itgetValByPathThe method obtains the object's attribute value based on the chain attribute expression.

Chain assignment

Assigning values ​​is relatively more troublesome

// Chain assignmentfunction updateValByPath(target: object, path: string, value): void {
  if (!(/[\\.\\[]/.test(path))) return target[path] = value; // If there is no . or [ symbol description is non-chained, directly assign value  const fileds = getPathFileds(path);
  // cosnt obj = {a: {b: {c: 6}}};
  // Get a value reference, for example, to update the c value of the obj object, you need to get a reference to the {c: 6} object, that is, = {c: 6}. After getting the reference = 8, {c: 6} is updated to {c: 8}  const ref = (0, -1).reduce((pre, cur) => pre[cur], target); // Only traverse to the penultimate field, because this field is a reference to the modified object  if (ref) return ref[`${(-1)}`] = value; // After getting the reference, update the last field  // If the reference object does not exist, remind the developer not to update the non-existent attributes  (`updated property "${path}" is not registered in data, you will not be able to get the value synchronously through ""`);
}

You have already discovered that the above method can only update the situation where the reference exists, that is, the parent object of the updated data exists. If you want to support more complex situations, you need to help you create a parent object when the updated attribute has no parent attribute. It may be an object type or an array type, which will consume a lot of memory and performance. Moreover, it is not conducive to rigorous code specifications by randomly operating an attribute that an object does not have, which is not conducive to maintenance. If you are interested, you can expand it on this basis and support setting non-existent attributes.

The above is a detailed explanation of how JS uses chain attribute expressions to obtain values ​​and assignments. For more information about JS chain value and assignments, please pay attention to my other related articles!