One, foreword

In business development, it is inevitable to operate on complex nested objects, and two pain points are commonly encountered in this scenario:

  • A lack of robustness handling when working with nested objects triggers a runtime error that leads to a blank page
  • The way the robustness is handled is not elegant, with lots of bloated code blocks

Optional chain operators

ECMA’s new optional chain operator specification allows properties deep in the chain of objects to be read without explicitly validating that each reference in the chain is valid.

const adventurer = {
  name: 'Alice'.cat: {
    name: 'Dinah'}}; adventurer.someNonExistentMethod? . ();// undefinedadventurer.dog? .name;// undefined
Copy the code

Instead of using the optional chain operator, the browser throws the following exception:

Uncaught TypeError: adventurer.someNonExistentMethod is not a function

Uncaught TypeError: Cannot read properties of undefined (reading 'name')
Copy the code

The optional chain operator will not cause an error if the reference is null or undefined, and will short-circuit the expression to return undefined.

let potentiallyNullObj = null;
let x = 0;
letprop = potentiallyNullObj? .[x++];console.log(x); // x will not be incremented and will still print 0
Copy the code

For example, in the code above, the x++ expression is not executed, which is a form of internal optimization in JavaScript.

Optional chain operators can be combined with functions, but this can happen:

const foo = {
    customMethod: () = > {}
} 

foo.customMethod = 1; foo? .customMethod? . ();// foo? .customMethod is not a function
Copy the code

So in the case of functions, you need to be able to ensure that the method properties won’t be tampered with, otherwise you still need to use Typeof to keep your code robust.

In front-end development scenarios, where data needs to be mapped to the UI in most cases, displaying undefined is definitely not appropriate, so the specification also provides a null value merge operator.

let customer = {
  name: "Carl".details: { age: 82}};letcustomerCity = customer? .city ??Dark City;
console.log(customerCity); // Dark City
Copy the code

In the harsh reality of development, the server may return NULL for fields with no data. So this kind of uncertain scenarios, using the Boolean or operator (| |) will be more reliable.

Babel compiler

Because the optional chain operator is currently in Stage 4, this feature code can only be compiled using Babel or TypeScript.

constfoo = {} foo? .bar? .nameCopy the code

Compiling the above code with Babel yields the following:

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;
Copy the code

The key points of implementation are as follows:

  • Implement the short-circuit calculation feature of optional chain operators using logic or operators
  • Simulate each reference to the object chain with temporary variables

The optional chain operator does not create temporary variables.

4. Set the property value at the bottom of the object chain

As mentioned earlier, the optional chain operator can only read properties deep in the object chain, but in business development, you cannot avoid setting properties deep in the object chain.

First we need to define a protocol for how the chain of objects should be referenced:

dotProp.set(object, 'foo.baz'.'x');
Copy the code

The next step is to parse out the attribute name referenced at each level for the delimiter:

const getPathSegments = path= > path.split('. ');
Copy the code

To enhance the robustness of the code, the parsed path can be validated again:

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;
};
Copy the code

Once you have a reference path for the chain of objects, you only need to find the final assignment node, especially to ensure that the upper level is referenced as an object during traversal to avoid TypeError exceptions.

set(object, path, value) {
  if(! isObject(object) ||typeofpath ! = ='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;
}
Copy the code

Although the above code implements the function of setting property values deep in the object chain, its disadvantages are also obvious, lacking the intelligent hints of the IDE. It is possible to have unexpected results due to spelling errors.

For the object chain protocol convention can also refer to the Ramda library simple and crude way:

const ramda = require('ramda');

const obj = {
  a: {
    b: [1.2.3]
  }
}

ramda.assocPath(['a'.'b'.1].20, obj); // { a: { b: [ 1, 20, 3 ] } }
Copy the code

Write at the end

References and related source code:

  • Developer.mozilla.org/zh-CN/docs/…
  • Github.com/sindresorhu…

Finally, if this article is helpful to you, please like, bookmark and share.