Сегодня мы его разберём и поговорим о подходах к поиску ответа. Задавая вопрос, о котором идёт речь, интервьюер предлагает рассказать о том, что выведет примерно такой код:
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}, 3000);
}
А вы знаете, что появится в консоли?
Сразу хочется сказать, что этот вопрос направлен на понимание таких механизмов JS, как замыкания, области видимости и функция setTimeout. Правильный ответ выглядит так:
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
Если вы ожидали чего-то другого, надеемся, в этом материале мы сможем рассказать о том, почему вывод этого фрагмента кода оказался именно таким, и о том, как привести его в более приличный вид.
Почему этот вопрос так популярен?
Один пользователь Reddit рассказал о том, что ему задавали такой вопрос на собеседовании в Amazon. Я и сам сталкивался с подобными вопросами, направленными на понимание циклов и замыканий в JS, даже на собеседовании в Google.
Этот вопрос позволяет проверить владение некоторыми важными концепциями JavaScript. Учитывая особенности работы JS, ситуация, которая смоделирована в представленном фрагменте кода, нередко может возникать и в ходе реальной работы. В частности, это касается использования
setTimeout
или какой-нибудь другой асинхронной функции в цикле.Хорошее понимание функциональных и блочных областей видимости в JavaScript, особенностей устройства анонимных функций, замыканий и IIFE, поможет вашему профессиональному росту и позволит показать себя с хорошей стороны на собеседованиях.
Подходы к ответу на вопрос и к избавлению от undefined
На самом деле, я уже писал о возможных подходах к ответу на этот вопрос в некоторых моих предыдущих материалах. В частности, в этом и этом. Позволю себе процитировать кое-что из этих публикаций:
Причина подобного заключается в том, что функция
setTimeout
создаёт функцию (замыкание), у которой есть доступ к внешней по отношению к ней области видимости, представленной в данном случае циклом, в котором объявляется и используется переменная i
. После того, как пройдут 3 секунды, функция выполняется и выводит значение i
, которое, после окончания работы цикла, остаётся доступным и равняется 4-м. Переменная, в ходе работы цикла, последовательно принимает значения 0, 1, 2, 3, 4, причём, последнее значение оказывается сохранённым в ней и после выхода из цикла. В массиве имеется четыре элемента, с индексами от 0 до 3, поэтому, попытавшись обратиться к arr[4]
, мы и получаем undefined
. Как избавиться от undefined
и сделать так, чтобы код выводил то, чего от него и ждут, то есть — значения элементов массива?Вот пара распространённых подходов к решению подобной задачи, а конкретно — к тому, чтобы организовать доступ к нужному значению переменной цикла внутри функции, вызываемой
setTimeout
.Первый предусматривает передачу необходимого параметра во внутреннюю функцию, второй основан на использовании возможностей ES6.
Итак, вот первый вариант:
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
// передадим функции переменную i, в результате
// у каждой функции будет доступ к правильному значению индекса
setTimeout(function(i_local) {
return function() {
console.log('The index of this number is: ' + i_local);
}
}(i), 3000);
}
Вот второй вариант:
const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
// использование ключевого слова let, которое появилось в ES6,
// позволяет создавать новую привязку при каждом вызове функции
// подробности смотрите здесь: http://exploringjs.com/es6/ch_variables.html#sec_let-const-loop-heads
setTimeout(function() {
console.log('The index of this number is: ' + i);
}, 3000);
}
На Reddit мне удалось найти похожий ответ на этот вопрос. Вот — хорошее разъяснение особенностей замыканий на StackOverflow.
Итоги
Можно отметить, что вопрос, с которого мы начали этот материал, часто сбивает с толку людей, обладающих небольшим опытом в области JavaScript или в функциональном программировании. Причина заключается в непонимании сущности замыканий. При формировании замыкания не выполняет передача значения переменной или ссылки на неё. Замыкание захватывает саму переменную.
Уважаемые читатели! Знаете ли вы интересные вопросы, которые задают на собеседованиях по JavaScript? Если да — просим поделиться.
Комментарии (45)
n0wheremany
16.10.2017 14:54Доки:
var timerId = setTimeout(func / code, delay[, arg1, arg2...])
Правда не сработает на <IE9
n0wheremany
16.10.2017 15:00-1И касаемо 1 варианта — почему автор не сделал так? Есть какие то ограничения?
const arr = [10, 12, 15, 21]; for (var i = 0; i < arr.length; i++) { (function(i){ setTimeout(function() { console.log('Index: ' + i + ', element: ' + arr[i]); }, 3000); })(i) }
mayorovp
16.10.2017 15:03Это же то же самое, вид сбоку.
Cryvage
16.10.2017 15:18Нет, это совсем не то же самое. Тут в коллюэке setTimeout будет замыкаться не «i», объявленный в цикле for, а «i», являющийся параметром функции-обёртки. В итоге выведутся индексы от 0 до 3 и соответствующие им элементы.
mayorovp
16.10.2017 15:28И касаемо 1 варианта — почему автор не сделал так? Есть какие то ограничения?
Напомню, первый вариант — это то где
setTimeout(function(i_local) { ... }(i), 3000)
Cryvage
16.10.2017 15:12Да потому что пример специально написан с ошибкой, чтобы выяснить, понимает ли человек, как работают замыкания, или нет.
n0wheremany
16.10.2017 15:24Вопрос то мой в другом — зачем в результате выполнения функция, а не выполнение функции в результате :)
stardust_kid
16.10.2017 15:18Этому вопросу уже лет 15 как минимум. Авторам блога можно было и посвежее найти.
И про замыкания не объяснили как следует.justhabrauser
16.10.2017 21:13Если авторам блога вчера исполнилось 14 или менее — то они могли что-то пропустить.
ermolaevalexey
17.10.2017 08:58-1Вы таки не поверите, но до сих пор многие «сеньеры-помидоры», разглагольствующие про graphql, на этом вопросе сыпятся
igormich88
16.10.2017 15:40В Java подобный код вообще не скомпилируется — потребует явно копировать в локальную переменную, по моему это правильно.
serf
16.10.2017 16:06Вопрос был бы чуть более хитрым если бы в setTimeout таймаут было не 3000, а 0 (значение по умолчанию). Хитрость ведь не только в области видимости и замыканиях, а еще в понимании того что JS однопоточный и event loop блокировать очень нежелательно.
rualekseev
16.10.2017 17:11Я совсем не программирую на js, но мои познания в других языках позволили мне правильно ответить про замыкание (все вопросы про замыкания, сводятся к подобной формулировке).
А вот ваше уточнение про таймаут 0 и однопоточность не очень понятно, что мы получим?SuperPaintman
16.10.2017 20:08Да тоже самое получаем, timeout работает по принципу "когда нибудь, но только не сейчас", т.е. если вы даже напишите
-500
, он сработает не раньше чем через один тик (зависит от браузера, но если не изменяет память минимальный таймаут 5-10мс).
Aquahawk
16.10.2017 17:22так ничего же с 0 не изменится. А перенос времени вычисления вычислительно сложной задачи в рамках этого потока ничего не даст, всё равно луп залочится. Можно порезать задачку на куски и через performance.now отъедать не больше например 10 ms на итерацию, но это изврат. А вообще воркеры же есть. Но на самом деле не все задачи подходят.
serf
16.10.2017 18:07Да ничего не измениться по сути, просто не все понимают как происходит планировка подобных «отложенных» задач в JS, а там тоже есть что обсудить. И вот как раз воркеры были бы к месту в обсуждении.
Aquahawk
16.10.2017 16:25а мне вот такое решение кажется интересным.
const arr = [10, 12, 15, 21]; for (let i = 0; i < arr.length; i++) { setTimeout(function() { console.log('Index: ' + i + ', element: ' + arr[i]); }, 3000); }
Вообще это не хитрый а самый что ни есть базовый вопрос по js на любую позицию кроме может совсем зелёного джуна. Если человек утверждает что знает JS и работает на нём профессионально, то незнание этих вещей означает полную профнепригодность в принципе. Скоупы, что их создаёт и как работает замыкание являются базовыми знаниями для языка который на этом построен чуть менее чем полностью.vanxant
17.10.2017 18:47Зашел написать этот же комментарий.
Еще можно спрашивать, чему равно 2+3*4, уровень сложности примерно такой же.
Только при чем тут гугль?
ameli_anna_kate
16.10.2017 16:38Что-то я разочарована, вполне рядовой вопрос на собесах в московских компаниях в течение уже нескольких лет. Сталкивалась и с такой формулировкой: «Как можно исправить данный пример? Напишите все способы, какие знаете»
К тому же кандидат мог почитать статьи о часто задаваемых вопросах на собеседованиях и тупо выучить как правильно ответить, все же не мешало бы просто отдельно спросить стандартные вопросы:
«Какие типы функций вы знаете и какие особенности у каждого?
Что такое замыкания и область видимости переменной?
Что такое setTimeout/setInterval, чем отличаются?»
… и тд.
haoNoQ
16.10.2017 18:44Хмм. Следует ли из вышесказанного что в циклах, в которых мы не хотим создавать такие замыкания,
var i
будет работать чуть быстрее, чемlet i
, ведь интерпретатору не надо создавать новую переменнуюi
на каждой итерации?
Производительность JS это, конечно, мутно, но я не настоящий сварщик.
kahi4
16.10.2017 19:29+1Возьмем код
const arr = [10, 12, 15, 21]; for (let i = 0; i < arr.length; i++) { setTimeout(function() { console.log('The index of this number is: ' + i); }, 3000); } for (let i = 0; i < arr.length; i++) { console.log(i); }
Вставим сюда и будет результат:
'use strict'; var arr = [10, 12, 15, 21]; var _loop = function _loop(i) { setTimeout(function () { console.log('The index of this number is: ' + i); }, 3000); }; for (var i = 0; i < arr.length; i++) { _loop(i); } for (var i = 0; i < arr.length; i++) { console.log(i); }
Видно, что когда лишние телодвижения не нужны, babel транслирует в код 1-в-1. В случае если нужно делать замыкание на внутреннюю переменную (как в задаче в примере) — ее создание неизбежно, поэтому как не крутите, память будет выделена. Думаю, движок хрома делает еще более оптимальный байткод, так что на производительность влиять не будет.
kahi4
16.10.2017 19:23+1Хитрый вопрос? Хитрее только "чему равен typeof null".
Вот вам еще хитрый вопрос для написания статьи на знание основ js:
function foo() { 'use strict'; console.log(bar()); function bar() { return 'bar'}; }
Будет undefined, reference error или 'bar'?
Как вариант решения задачки из топика — даешь больше es6:
const arr = [10, 12, 15, 21]; arr.forEach((item, i) => setTimeout(function() { console.log('Index: ' + i + ', element: ' + item); }, 3000));
А вообще
Rx.Observable.from([10, 12, 15, 21]).delay(3000).do(console.log);
SagePtr
17.10.2017 08:31-2const arr = [10, 12, 15, 21]; arr.forEach((item, i) => setTimeout(_ => console.log('Index: ' + i + ', element: ' + item), 3000));
oleg_gf
16.10.2017 20:55Я только изучаю JavaScript, ещё не дошёл до асинхронности, но уже подзабыл синтаксис for'а.
Вот такой код нормальный результат выдаёт:
const arr = [10, 12, 15, 21]; const iter = (i) => { if (i >= arr.length) {return ;} setTimeout(function() { console.log('Index: ' + i + ', element: ' + arr[i]); }, 3000); return iter(i + 1); }; iter(0);
Antelle
16.10.2017 23:14omg, «хитрый» вопрос из google и amazon… Не задают его нигде уже, то есть, задают, но в каком-нибудь первом тесте для отсева неадеквата.
kuraga333
17.10.2017 09:58-1(к первому листингу и пояснению к нему)
Во-первых, почему последнее значение i — 4, а не 3?
Во-вторых, де-факто в консоли выводится иное…mayorovp
17.10.2017 10:06В консоли выводится то же самое, просто некоторые консоли умеют отслеживать дублирующиеся сообщения и оставлять только одно.
А 4 выводится потому что после окончания цикла переменная i принимает именно это значение. На значении 3 цикл закончиться не может, потому что 3 < arr.length. Цикл заканчивается когда нарушается его условие — а оно нарушается когда i >= arr.length.
kuraga333
17.10.2017 10:14Да, сорри, какой-то не тот код выполнил. Своими глазами видел элементы массива в выводе.
Сам удивился. Спросоня. Эх, жаль карму…kuraga333
17.10.2017 10:19Там var на let заменили, в комментарии выше. Не заметил :-) Ну про 4 тупанул, еще раз сорри :-) Чувствую себя первоклашкой :-(
igrishaev
17.10.2017 11:53У меня на собеседовании по Питону похожее спрашивали. В цикле создается список лямбд, каждая из которых ссылается на счетчик. Функции выполнятся уже когда счетчик будет максимальным, из-за лексической видимости будет напечатано последнее значение.
roman_gemini
17.10.2017 13:59Был уверен что напечатается 4 раза последний элемент массива. Но после того как увидел правильный ответ, первая мысль — точно, это же for! Не знаю теперь кто я с точки зрения Amazon или Microsoft… for ведь такой же как в большинстве Си-подобных языков. Это не знание основ javascript или же не знание основ Си?)
mayorovp
Третий вариант:
PS если уж и оставляли ссылку на SO — можно было бы и на русскоязычное объяснение сослаться. Например, на вот это: https://ru.stackoverflow.com/a/433888/178779
timfcsm
ещё покороче
mayorovp
Вы цикл забыли, вот у вас и вышло "покороче". И, раз уж вы решили так делать — проще пойти через дополнительные параметры setTimeout:
timfcsm
я его не забыл, а просто не стал писать, для наглядности — где я что поменял
timfcsm
извиняюсь, что-то я затупил и не увидел что у вас forEach, а не просто две обертки в цикле)
iShatokhin
Более современный вариант:
mayorovp
Если уж использовать for-of и деструктуризацию, то и стрелочные функции тоже использовать можно :-)
iShatokhin
На самом деле, даже стрелочные не нужны.