Реализовать цепочку в JavaScript

JavaScript

Сама реализация цепного вызова относительно проста, и существует множество статей, подробно описывающих ее реализацию. В этой статье сделан еще один шаг вперед, чтобы объяснить, как реализовать связанные вызовы с точки зрения возвращаемых значений метода связанного вызова.

что цепочка

связанные звонки вJavaScriptЭто распространено в языковом мире, напримерjQuery,Promiseи т. д., используются ли все связанные вызовы. Цепочка вызовов позволяет нам писать более лаконичный код при выполнении последовательных операций.

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

Пошаговая реализация объединения вызовов

Предположим, мы хотим реализоватьmathмодуль, позволяющий ему поддерживать цепные вызовы:

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}

базовая цепочка

Обычный способ реализации связанных вызовов — вернуть результат вызова функции самому модулю. ТакmathКод модуля должен примерно выглядеть так:

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

Как методы возвращают значения

Вышеприведенный код реализует цепочку вызовов, но также есть проблема, что невозможно получить результат вычисления. Поэтому нам нужно изменить модуль, чтобы использовать внутреннюю переменную для хранения результата расчета.

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

Таким образом, на последнем шаге мы проходим.valueВы можете получить окончательный результат расчета.

Проблема действительно решена?

Выше мы, кажется, проходимvalueПеременная решает проблему сохранения результата вычисления, но когда происходит второй вызов цепочки,valueЗначение , поскольку у нас уже есть начальное значение, мы получим неправильный результат расчета!

const a = math.add(5, 6).value; // 11
const b = math.add(5, 7).value; // 23 而非 12

так как это потому чтоvalueС начальным значением вы можете получитьvalueсбросить значение? Ответ отрицательный, потому что мы не можем быть уверены, когда пользователь получит значение.

Другая идея заключается в создании нового экземпляра перед каждым связанным вызовом, чтобы гарантировать независимость экземпляров друг от друга.

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;

Но это не решает проблему полностью, если предположить, что мы вызываем следующим образом:

const m = math().add(5, 6);
const c = m.add(5).value; // 16
const d = m.add(5).value; // 21 而非 16

Итак, в конце концов, для решения этой проблемы каждый метод может возвращать только новый экземпляр, что гарантирует, что как бы он ни был вызван, он не будет мешать друг другу.

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

Как поддержать неудачу.valueвыполнять обычные операции над результатом

путем модернизацииvalueOfметод илиSymbol.toPrimitiveметод. вSymbol.toPrimitiveметод первыйvalueOfМетод вызывается, если среда ES не поддерживает его.

Как поддержатьJSON.stringifyСериализация результатов расчета

путем настройкиtoJSONметод.JSON.stringifyПри преобразовании значения в соответствующий формат JSON, если преобразованное значение имеетtoJSONметод, значение, возвращаемое методом, является предпочтительным.

Окончательный полный код реализации

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();