7 вопросов про замыкания в JavaScript

—

от автора

Привет, Хабр. Для будущих студентов курса «JavaScript Developer. Professional» подготовили перевод интересного материала.

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


7 Вопросов из интервью по замыканиям (closures) в JavaScript. Можете ли вы ответить на них?

Каждый разработчик JavaScript должен знать, что такое замыкание (closure). Во время собеседования по кодированию JavaScript есть большая вероятность, что вас спросят о концепции замыкания.

Я составил список из 7 интересных и наиболее сложных вопросов по замыканиям в JavaScript.

Возьмите карандаш, лист бумаги и постарайтесь ответить на вопросы, не глядя на ответы и не запуская код. По моим подсчетам, вам понадобится около 30 минут.

Развлекайтесь!

Если вам нужно освежить свои знания о замыкании, я рекомендую ознакомится с публикацией A Simple Explanation of JavaScript Closures (Простое объяснение замыканиям в JavaScript).

Содержание

Вопрос 1: Замыкания развязывают твои руки 

Вопрос 2: Утерянные параметры

Вопрос 3: Кто есть кто

Вопрос 4: Хитроумное замыкание

Вопрос 5: Правильное или неправильное сообщение

Вопрос 6: Восстановление инкапсуляции (Restore encapsulation)

Вопрос 7: Умное перемножение

Резюме

Вопрос 1: Замыкания развязывают твои руки 

Рассмотрим следующие функции: clickHandler, immediate, и delayReload:

let countClicks = 0; button.addEventListener('click', function clickHandler() {   countClicks++; });
const result = (function immediate(number) {   const message = `number is: ${number}`;   return message; })(100);
setTimeout(function delayedReload() {   location.reload(); }, 1000);

Какие из этих 3 функций получают доступ к переменным внешней области видимости (outer scope)?

Расширенный ответ

  1. clickHandler  обращается к переменной countClicks из внешней области видимости.

  2. immediate не обращается ни к каким переменным из внешней области видимости.

  3. delayReload обращается к глобальной переменной location  из глобальной области видимости (так же известной как крайняя область видимости (outermost scope)).

Вопрос 2: Утерянные параметры

Что будет записано в консоль для следующего фрагмента кода (code snippet):

(function immediateA(a) {   return (function immediateB(b) {     console.log(a); // What is logged?   })(1); })(0);

Расширенный ответ:

0 записывается на консоль. Посмотрите демо.

immediateA вызывается с аргументом 0, таким образом, параметр равен 0.

Функция immediateB, будучи вложенной в функцию immediateA, является замыканием, которое захватывает переменную a из внешней области видимости immediateA, где a равно 0. Таким образом, console.log(a) записывает в журнал 0.

Вопросы 3: Кто есть кто

Что будет записано в консоль для следующего фрагмента кода (code snippet):

let count = 0; (function immediate() {   if (count === 0) {     let count = 1;     console.log(count); // What is logged?   }   console.log(count); // What is logged? })();

Расширенный ответ:

1 и 0 записываются в консоль. Посмотрите демо.

Первое утверждение let count = 0 объявляет переменную count.

immediate() — это замыкание, которое захватывает переменную count из внешней области видимости. Внутри области видимости функции immediate() count равна 0.

Однако внутри этого условия другая команда let count = 1 объявляет count локальной переменной, которая перезаписывает count из внешней области видимости. Первая  console.log(count) записывает 1.

Вторая console.log(count) записывает 0, так как здесь переменная count доступна из внешней области видимости.

Вопрос 4: Хитроумное замыкание

Что будет записано в консоль в следующем фрагменте кода (code snippet):

for (var i = 0; i < 3; i++) {   setTimeout(function log() {     console.log(i); // What is logged?   }, 1000); }

Расширенный ответ:

3, 3, 3 записано на консоль. Посмотрите демо.

Фрагмент кода выполняется в 2 этапа.

Фаза 1

  1. for() выполняет итерацию 3 раза. Во время каждой итерации создается новая функция log(), которая захватывает переменную i. setTimout() планирует исполнение log() через 1000мс.

  2. Когда цикл for() завершается, переменная i имеет значение 3.

Фаза 2

Вторая фаза происходит после 1000 мс:

  1. setTimeout() выполняет запланированные функции log(). log() считывает текущее значение переменной i, которое равно 3, и записывает в консоль 3.

Поэтому 3, 3, 3 записывается в консоль.

Дополнительная задача: как бы вы изменили в этом примере сообщение для консоли со значениями 0, 1, 2 ? Запишите ваше решение в комментариях ниже!

Вопрос 5: Правильное или неправильное сообщение

Что будет записано в консоль в следующем фрагменте кода (code snippet):

function createIncrement() {   let count = 0;   function increment() {      count++;   }    let message = `Count is ${count}`;   function log() {     console.log(message);   }      return [increment, log]; }  const [increment, log] = createIncrement(); increment();  increment();  increment();  log(); // What is logged?

Расширенный ответ:

'Count is 0' записывается в консоль. Посмотрите демо.

Функция increment() вызывалась 3 раза, в итоге увеличивая count до значения 3.

Переменная message существует в рамках функции createIncrement().  Ее начальное значение 'Count is 0'. Однако, даже если переменная count была увеличена несколько раз, переменная message  всегда имеет значение 'Count is 0'.

Функция log() — это закрытие, которое захватывает переменную message из области видимости createIncrement(). console.log(message) записывает 'Count is 0' в консоль.

Дополнительная задача: как бы вы исправили функцию log(), чтобы она возвращала сообщение, имеющее фактическое значение count? Запишите ваше решение в комментариях ниже!

Вопрос 6: Восстановление инкапсуляции (Restore encapsulation)

Следующая функция createStack() создает элементы структуры стековых данных:

function createStack() {   return {     items: [],     push(item) {       this.items.push(item);     },     pop() {       return this.items.pop();     }   }; }  const stack = createStack(); stack.push(10); stack.push(5); stack.pop(); // => 5  stack.items; // => [10] stack.items = [10, 100, 1000]; // Encapsulation broken!

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

Это неприятная деталь, так как она нарушает инкапсуляцию стека: только методы push() и pop() должны быть публичными, а stack.items или любые другие элементы не должны быть доступны.

Рефакторизуйте описанную выше реализацию стека, используя концепцию замыкания, так, чтобы не было возможности доступа к массиву items вне области видимости функции createStack():

function createStack() {   // Write your code here... }  const stack = createStack(); stack.push(10); stack.push(5); stack.pop(); // => 5  stack.items; // => undefined

Расширенный ответ:

Вот возможный рефакторинг (refactoring) функции createStack():

function createStack() {   const items = [];   return {     push(item) {       items.push(item);     },     pop() {       return items.pop();     }   }; }  const stack = createStack(); stack.push(10); stack.push(5); stack.pop(); // => 5  stack.items; // => undefined

→ Посмотрите демо

items был перемещен в переменную внутри области видимости createStack().

Благодаря этому изменению, за пределами области видимости createStack(), теперь нет способа получить доступ к массиву items или изменить его. Элементы сейчас являются приватной переменной, а стек инкапсулирован: только методы push() и pop() являются публичными.

Методы push() и pop(), будучи замкнутыми, захватывают переменную items из области видимости функции createStack().

Вопрос 7: Умное перемножение

Напишите функцию multiply(), которая умножает 2 числа:

function multiply(num1, num2) {   // Write your code here... }

Если multiply(num1, numb2) будет вызвана с 2 аргументами, то она должна вернуть умножение 2 аргументов.

Но в том случае, если вызывается 1 аргумент const anotherFunc = multiply(numb1), то функция должна возвращать другую функцию. Возвращаемая функция при вызове anotherFunc(num2) выполняет умножение num1 * num2.

multiply(4, 5); // => 20 multiply(3, 3); // => 9  const double = multiply(2); double(5);  // => 10 double(11); // => 22

Расширенный ответ:

Вот возможная имплементация функции multiply():

function multiply(number1, number2) {   if (number2 !== undefined) {     return number1 * number2;   }   return function doMultiply(number2) {     return number1 * number2;   }; }  multiply(4, 5); // => 20 multiply(3, 3); // => 9  const double = multiply(2); double(5);  // => 10 double(11); // => 22

→ Посмотрите демо

Если параметр number2 не является undefined, то функция просто возвращает number1*number2.

Но если number2 является undefined, то это означает, что функция multiply() была вызвана с одним аргументом.  В таком случае вернем функцию doMultiply(), которая при последующем вызове выполняет фактическое умножение.

doMultiply() является замыкающей, поскольку она захватывает переменную number1 из области видимости функции multiply().

Резюме

Сравните ваши ответы с ответами в статье:

  • Если вы правильно ответили на 5 или более вопросов, у вас есть хорошее представление о замыканиях.

  • Если вы правильно ответили менее чем на 5 вопросов, вам нужно хорошенько освежить тему замыкания,. Я рекомендую изучить мой пост A Simple Explanation of JavaScript Closures (Простое объяснение замыкания в JavaScript).

Готовы к новому испытанию? Попробуйте ответить на 7 вопросов в интервью по ключевому слову «this» в JavaScript.


Узнать подробнее о курсе «JavaScript Developer. Professional»

Смотреть открытый карьерный вебинар

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/547070/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *