Привет, Хабр. Для будущих студентов курса "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)?
Расширенный ответ
-
clickHandler
обращается к переменнойcountClicks
из внешней области видимости. -
immediate
не обращается ни к каким переменным из внешней области видимости. -
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
-
for()
выполняет итерацию 3 раза. Во время каждой итерации создается новая функцияlog()
, которая захватывает переменнуюi
.setTimout()
планирует исполнениеlog()
через 1000мс. -
Когда цикл
for()
завершается, переменнаяi
имеет значение 3.
Фаза 2
Вторая фаза происходит после 1000 мс:
-
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(); // => 5stack.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(); // => 5stack.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(); // => 5stack.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); // => 20multiply(3, 3); // => 9const double = multiply(2);double(5); // => 10double(11); // => 22
Расширенный ответ:
Вот возможная имплементация функции multiply()
:
function multiply(number1, number2) { if (number2 !== undefined) { return number1 * number2; } return function doMultiply(number2) { return number1 * number2; };}multiply(4, 5); // => 20multiply(3, 3); // => 9const double = multiply(2);double(5); // => 10double(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".
Смотреть открытый карьерный вебинар.