Realization of chain call in JavaScript

Time:2021-2-18

Chain call implementation itself is relatively simple, there are many articles on its implementation. This article further explains how to realize chain call from the perspective of return value of chain call method.

What is chain call

Chain call is very common in JavaScript language field, such as jQuery, promise and so on. Chain call allows us to write more concise code when continuous operation.


new Promise((resolve, reject) => {
 resolve();
})
.then(() => {
 throw new Error('Something failed');
})
.then(() => {
 console.log('Do this whatever happened before');
})
.catch(() => {
 console.log('Do that');
})

Realize chain call step by step

Suppose we want to implement a “math” module to support chained calls


const math = require('math');
const a = math.add(2, 4).minus(3).times(2);
const b = math.add(2, 4).times(3).divide(2);
const c = { a, b };

console.log(a.times(2) + b + 1); // 22
console.log(a.times(2) + b + 2); // 23
console.log(JSON.stringify(c)); // {"a":6,"b":9}

Basic chain call

The usual implementation of chain call is to return the result of function call to the module itself. So the code of the math module should look like this:


export default {
 add(...args) {
  // add
  return this;
 },
 minus(...args) {
  // minus
  return this;
 },
 times(...args) {
  // times
  return this;
 },
 divide(...args) {
  // divide
  return this;
 },
}

How do methods return values

The above code implements the chain call, but there is also a problem, that is, it is unable to obtain the calculation results. So we need to modify the module and use an internal variable to store the calculation results.


export default {
 value: NaN,
 add(...args) {
  this.value = args.reduce((pv, cv) => pv + cv, this.value || 0);
  return this;
 },
}

In this way, we can get the final calculation result through. Value in the last step.

Is the problem really solved

Above, we seem to solve the problem of storing the calculation results by using a value variable, but when the second chain call occurs, because the value already has the initial value, we will get the wrong calculation results!

const a = math.add(5, 6).value; // 11
const b =  math.add (5, 7). Value; // 23 instead of 12

Since it is because of the initial value of ﹣ value, can we reset it when we get the value of ﹣ value? The answer is no, because we are not sure when the user will take the value.

Another idea is to generate a new instance before each chain call, so as to ensure that the instances are independent of each other.


const math = function() {
 if (!(this instanceof math)) return new math();
};

math.prototype.value = NaN;

math.prototype.add = function(...args) {
 this.value = args.reduce((pv, cv) => pv + cv, this.value || 0);
 return this;
};

const a = math().add(5, 6).value;
const b = math().add(5, 7).value;

However, this can not completely solve the problem. Suppose we call as follows:

const m = math().add(5, 6);
const c = m.add(5).value; // 16
Const d = m.add (5). Value; // 21 instead of 16

Therefore, to solve this problem, only each method can return a new instance, which can ensure that no matter how it is called, they will not be interfered with each other.


math.prototype.add = function(...args) {
 const instance = math();
 instance.value = args.reduce((pv, cv) => pv + cv, this.value || 0);
 return instance;
};

How to support ordinary operation on results without. Value

By modifying the “valueof” method or Symbol.toPrimitive method. Among them Symbol.toPrimitive Method priority: the ﹣ valueof method is called unless it is not supported by the ES environment.

How to support JSON.stringify Serialize calculation results

By customizing the ﹣ tojson method.   JSON.stringify When converting a value to the corresponding JSON format, if the converted value has a ﹣ tojson method, the value returned by this method will be used first.

The final complete implementation code


class Math {
 constructor(value) {
  let hasInitValue = true;
  if (value === undefined) {
   value = NaN;
   hasInitValue = false;
  }
  Object.defineProperties(this, {
   value: {
    enumerable: true,
    value: value,
   },
   hasInitValue: {
    enumerable: false,
    value: hasInitValue,
   },
  });
 }

 add(...args) {
  const init = this.hasInitValue ? this.value : args.shift();
  const value = args.reduce((pv, cv) => pv + cv, init);
  return new Math(value);
 }

 minus(...args) {
  const init = this.hasInitValue ? this.value : args.shift();
  const value = args.reduce((pv, cv) => pv - cv, init);
  return new Math(value);
 }

 times(...args) {
  const init = this.hasInitValue ? this.value : args.shift();
  const value = args.reduce((pv, cv) => pv * cv, init);
  return new Math(value);
 }

 divide(...args) {
  const init = this.hasInitValue ? this.value : args.shift();
  const value = args.reduce((pv, cv) => pv / cv, init);
  return new Math(value);
 }

 toJSON() {
  return this.valueOf();
 }

 toString() {
  return String(this.valueOf());
 }

 valueOf() {
  return this.value;
 }

 [Symbol.toPrimitive](hint) {
  const value = this.value;
  if (hint === 'string') {
   return String(value);
  } else {
   return value;
  }
 }
}

export default new Math();

The above is the whole content of this article, I hope to help you learn, and I hope you can support developer more.