Как реализовать функцию-обертку runOnce в JavaScript, вызывающую функцию только один раз?
Что проверяют
Замыкания и умение написать функцию высшего порядка без потерь: контекста, аргументов, результата. Следом спрашивают, что должен возвращать повторный вызов и что будет, если обёрнутая функция бросит исключение.
Как отвечать
Каркас: runOnce(fn) возвращает новую функцию, а в замыкании хранит флаг и результат первого вызова. Детали, которые отличают хороший ответ:
- результат: повторный вызов возвращает результат первого, а не
undefined. Так ведёт себя_.onceв lodash, и код, который использует возвращаемое значение, не ломается на втором вызове; - контекст: возвращайте обычную функцию, не стрелку, и вызывайте
fn.apply(this, args). Тогда обёртку можно повесить на метод объекта, иthisсохранится; - аргументы: они передаются только при первом вызове, последующие игнорируются. Это стоит сказать вслух, чтобы не было сюрпризов;
- освобождение памяти: после вызова присвойте
fn = null. Замыкание перестанет держать исходную функцию и всё, что захвачено ею; - исключения: решите явно. Если флаг ставится до вызова, упавшая функция больше не повторится. Если после, следующий вызов попробует снова. Оба варианта допустимы, важно, чтобы выбор был осознанным.
Про асинхронные функции: если fn возвращает промис, обёртка закэширует этот промис, и все вызовы получат один и тот же. Для однократной инициализации это ровно то, что нужно: параллельные вызовы не запустят загрузку дважды.
Для обработчиков событий есть встроенная альтернатива: addEventListener(type, handler, { once: true }).
Где спотыкаются
Выносят флаг called во внешнюю область видимости. Тогда все обёртки делят один флаг, и вторая обёрнутая функция не вызовется никогда.
Возвращают стрелочную функцию и вызывают fn(...args). На методах объекта this теряется, и ошибка проявляется только в реальном коде.
В асинхронной версии ставят флаг после await fn(). Пока первый вызов ждёт ответа, второй проходит проверку и запускает функцию повторно.