Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

getter и мемоизация JS: Коллекции

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

Свойства

Начнем с примера, в котором мы видим уже привычные нам конструкции:

// Создаем список машин
const cars = [
  { brand: 'bmw', model: 'm5', year: 2014 },
  { brand: 'bmw', model: 'm4', year: 2013 },
  { brand: 'kia', model: 'sorento', year: 2014 },
  { brand: 'kia', model: 'rio', year: 2010 },
  { brand: 'kia', model: 'sportage', year: 2012 },
];
coll = new Enumerable(cars);

const result = coll.where((car) => car.brand == 'kia')
  .where((car) => car.year > 2011);

result.length; // ?

Предположим, нам нужно узнать количество элементов в результате, который мы получили.

Часто вывод формируется в зависимости от размера выходного массива. Для этого в JavaScript обычно используется свойство length. Но здесь возникает вопрос: откуда оно возьмется там и как оно заполнится.

Обновление во время вычисления

Например, мы можем делать обновление свойства length во время вычисления:

toArray() {
  const result = /* some code here */;
  this.length = result.length;
  return result;
}

Как мы помним, у нас нет вычисленной коллекции. Есть только отложенные вычисления, так как мы используем Lazy Evaluation. Поэтому в toArray, который делает эти вычисления, можно установить length после того, как мы получаем результат.

Но этот способ не работает. В таком случае это будет значить, что length можно использовать только после вызова toArray. То есть до этого момента мы его не можем использовать. А поскольку мы уже вызвали toArray, то length внутри уже не нужен. В этой ситуации длину можно посмотреть у самого массива. При этом до вызова toArray мы не можем узнать длину. Придется произвести вычисления.

В итоге нужен другой способ, который позволяет реализовывать свойства. В JavaScript он называется Getter.

Getter

Чтобы использовать Getter, нужно в классе описать функцию length, при этом слева приписать ключевое слово get с пробелом:

class Enumerable {
  get length() {
    return /* some code here */;
  }
}

В этом случае эта конструкция превращается в динамическое свойство. То есть снаружи оно выглядит как свойство — вызываем coll.length, но в реальности вызывается функция get. При этом она не имеет права принимать параметры. То есть это просто свойство, а не вызов функции.

Далее это свойство работает следующим образом:

const result = coll
  .where((car) => car.brand === 'kia')
  .where((car) => car.year > 2011);

result.length; // 2

Здесь мы делаем выборку и result.length. После этого будет вызвана функция, внутри которой мы и должны будем произвести необходимые нам вычисления. Например, мы можем вызвать toArray. Это зависит от конкретной реализации. Но нам в любом случае придется выполнить эти вычисления, когда вызывается это свойство.

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

Теперь мы можем вызывать все свойства в любом порядке и функцию toArray. При этом всё будет работать, как мы ожидаем. А вычисление будет происходить в тот момент, когда это нужно. Например, при подсчете количества элементов.

Повторные вычисления

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

Когда мы вызываем length или toArray, вычисления производятся каждый раз заново. Это будет замедлять программу. Причем будет видно, что вычислять не нужно, потому что коллекция уже конкретная, и мы знаем, какие операции производятся. Новых операций там не появится, потому что у нас неизменяемая коллекция — каждый раз мы получаем новую.

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

Мемоизация

Мемоизация — это сохранение результата выполнения функций для предотвращения повторных вычислений. Этот паттерн описывается таким шаблоном:

methodName() {
  if (!this.memo) {
    this.memo = /* some code here */;
  }

  return this.memo;
}

methodName();
methodName(); // memо

В этом шаблоне мы проверяем внутри нашей функции или динамического свойства, не установлено ли какое-то свойство. Если нет, то мы его устанавливаем и записываем туда наше вычисление. После этого мы возвращаем значение этого свойства. В нашем случае мы назвали свойство memo

В первый раз, когда мы делаем вызов, у нас производится вычисление и заполнение this.memo. Когда мы делаем вызов второй раз, то нам напрямую возвращается memo.

Когда мы используем нашу библиотеку, это работает идеально. Дело в том, что immutable-коллекции и memo не могут устареть. А когда вызываем новые методы или свойства на нашем объекте, они будут порождать новый объект, который уже содержит копии данных. Поэтому там будет свой memo.

Здесь снова проявляется преимущество immutable-подхода, который при отсутствии изменяемости не заставляет нас синхронизировать состояние.

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

Ниже пример с мемоизацией факториала:

// Это просто функция, которая вычисляет факториал без мемоизации
const factorial = (num) => {
  if (num === 0) {
    return 1;
  }
  return num * factorial(num - 1);
}

// Эта функция выполняет мемоизацию факториала
const generateFactorialWithMemo = () => {
  // Здесь будут храниться вычисленные значения
  // Так как функция факториала применяется к разным аргументам, то для хранения результатов понадобится объект.
  // В более простых случаях достаточно обычного значения (как в практике к этому уроку)
  let memo = {};
  // Эта функция возвращается наружу и будет заниматься вычислением факториала
  const f = (num) => {
    // Если значение не мемоизированно, то мемоизируем
    if (!memo[num]) {
      memo[num] = factorial(num);
    }
    // Возвращаем значение из мемо
    return memo[num];
  }
  return f;
};

const f = generateFactorialWithMemo();
// Значение вычисляется
f(3); // 6

// Значение не вычисляется
f(3); // 6

Мемоизация отличается от кеширования отсутствием устаревания. Данные внутри memo устареть не могут по определению. Если наша функция чистая, то для одного входа она всегда дает один и тот же выход. При этом она мемоизируют только чистые функции, иначе всегда есть риск сохранить результат, который впоследствии может поменяться.

Выводы

В этом уроке мы познакомились с новой возможностью JavaScript, которая позволяет создавать динамические свойства. Еще мы узнали, что такое Getter. Он позволяет реализовывать свойства. А также узнали, что мемоизация — это сохранение результата выполнения функций для предотвращения повторных вычислений. При этом она отличается от кеширования тем, что отсутствует устаревание.


Дополнительные материалы

  1. Геттеры в JS

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»