Мы старались сделать для вас что-то интересное и необычное. Очень надеюсь что у нас получилось. Нам не хотелось оставлять вас без ответов и объяснений почему именно так. Давайте разбираться.
Для начала хочу напомнить как проходил конкурс, было 4 тура по 15 вопросов про JS, 1 внеконкурсный тур на 15 вопросов про React и финал на 10 вопросов.
Под катом — разбор задач первых 4 туров.
Это вторая часть наших разборов.
Разбор вопросов про React тут
Как мы все это делали? Мы решили что нам нужно нагенерировать порядка 80-90 вопросов, чтобы был запас из чего выбирать. После этого мы поделили все на темы:
- события в браузере
- различные API (Array, Set, defineProperty и т.д.),
- внимательность
- работа с дробными числами
- поднятие (hoisting)
- event loop
- приведение типов
- typeof
- логичиские (с логическим И и ИЛИ)
После этого распределили вопросы по 4 турам. Все туры мы старались сделать одинаковыми по сложности, для этого мы делали несколько заходов проходя эти тесты и определяя где вопросы проще, где сложнее и заменяли выделяющиеся вопросы на более подходящие. И мы сделали в каждом туре примерно одинаковое количество вопросов на определенную тему. В итоге получилось, что в разных турах были похожие, но не одинаковые вопросы.
Из-за этого разбирать по турам кажется не очень удобно потому-что будет очень много дублирующихся объяснений, предлагаю смотреть на них по темам. Давайте начинать с самой простой.
Вопросы на внимательность:
Что будет выведено в консоль?
console.log(0,1 + 0,2);
a) 0.30000000000000004
b) 0.3
c) 2
d) 0 1 2
d) 0 1 2
Тут между числами стоит ,
, а не .
если отформатировать вопрос так:
console.log(0, 1 + 0, 2);
все станет понятно
Что будет выведено в консоль?
(() => {
'use strict';
a = null + undefined;
console.log(a);
})();
a) 0
b) NaN
c) null
d) ошибка
d) ошибка
a
создается не как переменная (не Variable Declaration), тут происходит неявное присваивание (Assignment Expression) к this.a
что очень часто может оказаться не тем чего вы ожидаете, т.к. будет создана глобальная переменная window.a
в строгом режиме такое запрещено.
Что будет выведено в консоль?
let foo = function bar() { return 123; };
console.log( typeof bar() );
a) 'function'
b) 'number'
c) 'undefined'
d) ошибка
d) ошибка
Это функциональное выражение (expression) — имя функции в данном случае является локальным для функции. Для вызова функции надо вызывать foo
, а не bar
. Если бы это было объявление (declaration) ответ был бы number
.
Вопросы про работу с дробными числами:
Что будет выведено в консоль?
console.log(0.1 ** 2);
a) 0.2
b) 0.01
c) 0.010000000000000002
d) NaN
c) 0.010000000000000002
Что будет выведено в консоль?
console.log(0.1 + 0.2);
a) 0.30000000000000004
b) 0.3
c) 2
d) NaN
a) 0.30000000000000004
**
— это аналог Math.pow возводим 0.1
в квадрат — должно получиться 0.01
, но в JS (как и во многих других языках) есть известная проблема с точностью операций при работе с числами с плавающей запятой. Будет 0.010000000000000002
Связано это с тем что в двоичной системе получается бесконечная дробь т.к. под число в JS всегда выделяется ровно 64 бита — все числа всегда двойной точности с плавающей запятой. Тоже самое произойдет при сложении.
Переходим к вопросам чуть сложнее.
События в браузере:
Есть обработчик события на элементе, Какие значения внутри этого обработчика будут всегда одинаковы?
elem.onclick = function(event) { }
a) event.target и event.currentTarget
b) event.target и this
c) event.currentTarget и this
d) все варианты не верны
c) event.currentTarget и this
this
— всегда будет указывать на элемент
currentTarget
— тот элемент на котором висит событие
target
— элемент на котором событие произошло
Что выведет этот код по клику на div?
div.onclick = function() { console.log(1) };
div.onclick = function() { console.log(2) };
div.addEventListener('click', function() { console.log(3) });
a) 1
b) 1 3
c) 2 3
d) 3
c) 2 3
onclick добавит обработчик console.log(1)
, но в следующей строчке мы перетираем его новой функцией и остается только console.log(2)
. onclick
— это DOM свойство оно всегда одно
События сработают в том порядке в котором навешены, сначала будет выведено 2 потом 3.
Если бы мы несколько раз делали addEventListener
то сработал бы каждый из них, т.к. хендлеры добавляют события в очередь.
Секция вопросов про различные API
defineProperty:
Что выведет этот код?
(() => {
const obj = { key: 1 };
Object.defineProperty(obj, 'key', {
enumerable: false,
configurable: false,
writable: false,
value: 2
});
console.log(obj.key);
obj.key = 3;
console.log(obj.key);
})();
a) 1, 2
b) 2, 2
c) 2, 3
d) ошибка
b) 2, 2
Что выведет этот код?
(() => {
'use strict';
const obj = { key: 1 };
Object.defineProperty(obj, 'key', {
enumerable: false,
configurable: false,
writable: false,
value: 2
});
console.log(obj.key);
obj.key = 3;
console.log(obj.key);
})();
a) 1, 2
b) 2, 2
c) 2, 3
d) 2, ошибка
d) 2, ошибка
Что выведет этот код?
(() => {
const obj = { key: 1 };
Object.defineProperty(obj, 'key', {
enumerable: false,
configurable: false,
writable: true,
value: 2
});
console.log(obj.key);
obj.key = 3;
console.log(obj.key);
})();
a) 1, 2
b) 2, 2
c) 2, 3
d) ошибка
c) 2, 3
Во всех вопросах выше проверяется знание метода defineProperty
а конкретнее настройки writable
. Если она установлена в false
то запрещается менять значения ключу переданному вторым параметром в defineProperty
. Разница только в том что без строгого режима — use strict
движок притворится что все хорошо, но значение не поменяет, а в в строгом режиме будет ошибка.
инкремент:
Что выведет этот код?
let x = 5;
console.log(x++);
a) 5
b) 6
c) '5++'
d) ошибка
a) 5
Что выведет этот код?
const a = 5;
console.log(a++);
a) 5
b) 6
c) '5++'
d) ошибка
d) ошибка
При использовании постфиксной формы инкримента возвращается значение до увеличения.
А при префиксной после, т.е. console.log(++5)
распечатало бы 6
const
нельзя перезаписывать, а т.к. Number — это примитив то при его увеличении переменную перезапишет новым значением и будет ошибка.
Set:
Что выведет этот код?
const a = [...new Set([1, 1, 2, , 3, , 4, 5, 5])];
console.log(a);
a) [1, 1, 2, , 3, , 4, 5, 5]
b) [1, 2, undefined, 3, 4, 5]
c) [1, 1, 2, undefined, 3, undefined, 4, 5, 5]
d) ошибка
b) [1, 2, undefined, 3, 4, 5]
Что выведет этот код?
let set = new Set([10, '10', new Number(10), 1e1, 0xA]);
console.log(set.size);
a) 5
b) 3
c) 2
d) 1
b) 3
Что выведет этот код?
let obj = {};
let set = new Set([obj, obj, {}, {}, {...{}}, {...obj}]);
console.log(set.size);
a) 6
b) 5
c) 2
d) 1
b) 5
Set
— это множество, в нем по определению не может быть одинаковых значений. Вопрос в том как эти значения сравниваются. Примитивы сравниваются по значению, а объекты по ссылке.
Он сам по себе не приводит типы данных и может хранить значения любого типа 1e1
и 0xA
— будут преобразованы в десятичную систему и получится 10
.
А новые объекты всегда не равны: console.log({} == {})
выдаст false
т.к. объекты будут созданы по новой в разных местах памяти и их ссылки будут не равны.
Что выведет этот код?
console.log(Infinity / Infinity);
a) NaN
b) 1
c) Error
d) Infinity
a) NaN
Делить бесконечность на бесконечность и вычесть бесконечность из бесконечности нельзя т.к. с математической точки зрения получается неопределенность, то же самое произойдет при умножении Infinity
и 0
ошибок математические операции не вызывают — будет NaN
Вопросы про Spread:
Что выведет этот код?
const a = { ...{ a: 1, b: 2, c: 3 }, ...{ a: 2, c: 4, d: 8 } };
console.log(a);
a) { a: 2, b: 2, c: 4, d: 8 }
c) { a: 1, b: 2, c: 3, d: 8 }
c) { a: 1, b: 2, c: 3, a: 2, c: 4, d: 8 }
d) ошибка
a) { a: 2, b: 2, c: 4, d: 8 }
Что выведет этот код?
const a = [...[1, 2], ...[[3, 4]], ...[5, 6]];
console.log(a);
a) [1, 2, 3, 4, 5, 6]
b) [1, 2, [3, 4], 5, 6]
c) [[1, 2], [[3, 4]], 5, 6]
e) ошибка
b) [1, 2, [3, 4], 5, 6]
Spread
оператор служит для разбора объекта или массива на части. Берет значения из сущности после ...
и копирует их в создаваемую. Стоит отметить что для массива и объекта раскрывается на 1 уровень т.е. ...[[1]]
вернет массив с одним элементом, а не сам элемент. В объектах же дублирующихся значений быть не может, поэтому значения, раскрываемые после, перезапишут те, что были раскрыты раньше. Это можно использовать для указания параметров по умолчанию.
const fn = (actualProps) => ({ ...defaultProps, ...actualProps })
Все значения по умолчанию будут перекрыты переданными значениями, если они есть.
Что выведет этот код?
console.log(parseInt(' -10,3 миллиона рублей '));
a) -10,3
b) -10
c) TypeError
d) NaN
b) -10
Исчерпывающее описание с MDN:
Если функция parseInt встречает символ, не являющийся числом в указанной системе счисления, она пропускает этот и все последующие символы (даже, если они подходящие) и возвращает целое число, преобразованное из части строки, предшествовавшей этому символу. parseInt отсекает дробную часть числа. Пробелы в начале и конце строки разрешены.
Что выведет этот код?
const t = { a: 6, b: 7 };
const p = new Proxy(t, {
get() {
return 12;
},
});
console.log(p.a);
p.a = 18;
console.log(p.a);
console.log(t.a);
a) ошибка
b) 12 18 18
c) 12 18 6
d) 12 12 18
e) 6 18 6
d) 12 12 18
Proxy
перехватывает все обращения к объекту. В данном случае мы проксируем только get
метод и всегда возвращаем 12
независимо от того, к какому полю объекта мы обращаемся. При этом set
мы не трогаем, и при обращении к прокси значение в объекте будет заменено.
массивы:
Что выведет этот код?
let arr = [];
arr[1] = 1;
arr[5] = 10;
console.log(arr.length);
a) 1
b) 5
c) 6
d) 10
c) 6
Что выведет этот код?
let arr = new Array(3);
console.log(arr[1]);
a) undefined
b) 1
c) 3
d) ошибка
a) undefined
Когда мы создаем Array
с одним числовым аргументом — он означает длину массива. Массив при этом создается пустой, все значения undefined
. То же самое произойдет если создать обратиться к несуществующему полю массива. Стоит отметить, что если передать в Array
не число, вернется массив с этим элементом т.е. Array('a')
вернет ['a']
логические операции &&
, ||
, ==
и т.д.:
Что выведет этот код?
console.log([] && 'foo' && undefined && true && false);
a) []
b) 'foo'
c) undefined
d) true
c) undefined
Что выведет этот код?
console.log(0 || 1 && 2 || 3);
a) 0
b) 1
c) 2
d) 3
c) 2
Что выведет этот код?
console.log(0 || '' || 2 || undefined || true || false);
a) 0
b) false
c) 2
d) true
c) 2
Что выведет этот код?
console.log(2 && '1' && null && undefined && true && false);
a) 2
b) false
c) undefined
d) null
d) null
Что выведет этот код?
console.log([] && {} || null && 100 || '');
a) true
b) 100
c) ''
d) {}
d) {}
Пустой масcив []
— это true
как и пустой объект {}
.
Пустая строка ''
, null
и undefined
— это false
Логическое или ||
— возвращает левый операнд, если он истинен, в остальных случаях возвращает правый.
Логическое и &&
— возвращает левый операнд, если он ложен, в остальных случаях возвращает правый.
Это можно иногда встретить в коде, до появления параметров по умолчанию часто писали так — если в функции нет параметров, то берем параметры по умолчанию:
function f(userParams) {
var params = userParams || defaultParams;
}
Сейчас в React’е часто проверяют, если условие истинно, то рендерим что-то:
{ isDivVisible && <div>bla-bla</div> }
сравнение массивов:
Что выведет этот код?
const arrayFoo = [1, 2, 3, 4];
const arrayBaz = [1, 2, 3, 4];
console.log(arrayFoo == arrayBaz && arrayFoo == arrayBaz);
a) false
b) true
c) undefined
d) ошибка
a) false
Что выведет этот код?
console.log([null, 0, -0].map(x => 0 <= x));
a) [false, true, false]
b) [false, true, true]
c) [false, false, false]
d) [true, true, true]
d) [true, true, true]
Что выведет этот код?
const arrayFoo = [1, 2, 3, 4];
const arrayBaz = [1, 2, 3, 4];
console.log(arrayFoo >= arrayBaz && arrayFoo <= arrayBaz);
a) true
b) false
c) undefined
d) ошибка
a) true
Что выведет этот код?
const foo = [1, 2, 3, 4];
const baz = '1,2,3,4';
console.log(foo >= baz && foo <= baz);
a) false
b) true
c) undefined
d) будет ошибка
b) true
При ==
сравниваем по ссылке.
при операции >, >=, <, <=
операнды преобразуются к примитивам и у arrayFoo
вызывается метод valueOf, который должен вернуть примитивное значение arrayFoo
, но он возвращает ссылку на этот же массив. Далее происходит преобразование в примитивное значение вызовом метода toString
, который в свою очередь вернет строковое представление массива в виде "1,2,3,4" сравнит лексикографически два массива и вернет true
Что выведет этот код?
console.log(+0 == -0);
console.log(+0 === -0);
console.log(Object.is(+0, -0));
a) true, false, false
b) true, true, false
c) false, true, true
d) false, false. false
b) true, true, false
Исчерпывающее объяснение с MDN:
Поведение этого метода (речь об Object.is
) не аналогично оператору ===
. Оператор ===
(также как и оператор ==
) считает числовые значения -0
и +0
равными, а значение Number.NaN
не равным самому себе.
Вопросы про hoisting:
Что выведет этот код?
console.log(str);
const str = 'HeadHunter';
a) 'HeadHunter'
b) undefined
c) ошибка
c) ошибка
Что выведет этот код?
var arrayFunction = [];
for (let i = 0; i <= 10; i++) {
arrayFunction.push(() => i);
}
console.log(arrayFunction[3]());
a) 4
b) 0
c) 11
d) 3
d) 3
Что выведет этот код?
console.log(str);
var str = 'HeadHunter';
a) 'HeadHunter'
b) undefined
c) null
c) будет ошибка
b) undefined
Что выведет этот код?
console.log(foo);
var foo;
foo = foo ? 1 : 0;
console.log(foo);
a) ошибка
b) undefined 0
c) '' 1
d) 0 0
b) undefined 0
Сработает ли вызов функции?
getCompanyName();
function getCompanyName() {
return 'HeadHunter';
}
a) да
b) нет, вызов должен стоять после объявления.
c) ошибка
a) да
Что выведет этот код?
var arrayFunction = [];
for (var i = 0; i <= 10; i++) {
arrayFunction.push(() => i);
}
console.log(arrayFunction[3]());
a) 4
b) 0
c) 11
d) 3
c) 11
Объявления функции (declaration) всплывают, а выражения (expression) нет.
var
всплывает, но до момента инициализация равен undefined
.
let
и const
не всплывают и имеют область видимость в блок т.е. ограничены {}
.
Чтобы правильно работал цикл с var
надо использовать замыкание, в нем сохранится значение.
(раньше это было классической задачей для собеседований, а сейчас у нас есть let)
var arrayFunction = [];
for (var i = 0; i <= 10; i++) {
(function(i) {
arrayFunction.push(() => i);
})(i);
}
console.log(arrayFunction[3]());
Что выведет этот код?
console.log(true + false);
a) true
b) false
c) 1
d) 0
c) 1
Ни один из операторов строкой не является, +
приводит к числу. Получается 1 + 0
Что выведет этот код?
console.log([] - 100 + ![]);
a) false
b) '-100'
c) -100
d) NaN
c) -100
Массив приводится к строке, после этого из-за -
приводим к числу, получается -100
, далее приводим массив к false
, а это 0
Что выведет этот код?
console.log([[], []] + 1);
a) 1
b) '1'
c) ',1'
d) NaN
c) ',1'
Вызываем toString
на объекте, при этом toString
будет так же вызван на всех элементах массива. [].toString
вернет пустую строку ''
. Получается , + 1
— ответ ,1
.
Что выведет этот код?
console.log([] + 100 + 5);
a) 105
b) '1005'
c) 1005
d) NaN
b) '1005'
Массив приводим к строке, и далее уже происходит конкатенация.
Что выведет этот код?
console.log(1 + { a: 3 } + '2');
a) 6
b) '1[object Object]2'
c) 3
d) NaN
b) '1[object Object]2'
Преобразовываем к строке — тут просто конкатенация.
Что выведет этот код?
console.log(10.toString() + 10 + 0x1);
a) '10101'
b) 21
c) '10100x1'
d) ошибка
d) ошибка
Для числа точка .
означает начало дробной части, мы ожидаем там число — будет ошибка.
Чтобы этот пример заработал нормально надо писать 10..toString()
Что выведет этот код?
console.log(5 + false - null + true);
a) '0true'
b) NaN
c) 6
d) будет ошибка
c) 6
Тут все приводим к числу, получается 5 + 0 - 0 + 1
Что выведет этот код?
console.log(true + NaN + false);
a) true
b) NaN
c) false
d) 1
b) NaN
Приводим все к числу, при сложении чисел с NaN
— получаем NaN
Что выведет этот код?
console.log('0x1' + '1' - '1e1');
a) 17
b) 7
c) '0x111e1'
d) NaN
b) 7
Тут уже строки после первой конкатенации получаем: '0x11' - '1e1'
. Из-за знака -
приводим все к числу.
0x11
— шестнадцатеричная запись числа в десятичной это 17
.
1e1
— экспоненциальная форма тоже самое что 1 * 10 ** 1
— т.е. просто 10
.
typeof:
Что выведет этот код?
let foo = () => { return null; };
console.log( typeof typeof foo );
a) 'function'
b) 'string'
c) 'null'
d) ошибка
b) 'string'
Что выведет этот код?
typeof function() {}.prototype;
a) 'function'
b) 'object'
c) 'undefined'
d) ошибка
b) 'object'
typeof
всегда возвращает строку, имеет меньший приоритет чем вызов функции, поэтому сначала выполняется функция, а typeof
применяется к возвращаемому ей результату. Объекты Function наследуются от Function.prototype. Спека явно определяет что это объект.
event loop:
Начнем с 2 вопросов про промисы.
Что выведет этот код?
Promise.reject()
.then(() => console.log(1), () => console.log(2))
.then(() => console.log(3), () => console.log(4));
a) 1 4
b) 1 3
c) 2 3
d) 2 4
c) 2 3
Что выведет этот код?
Promise.reject('foo')
.then(() => Promise.resolve('bar'), () => {})
.then((a) => {console.log(a)})
a) foo
b) bar
c) undefined
d) ошибка
c) undefined
Promise.reject
— возвращает промис в rejected состоянии.
Надо вспомнить что then
принимает 2 параметра, onFulfill
и onReject
колбеки. Если происходит ошибка до этого then
, то мы попадаем в onReject
колбек. Если в нем не происходит ошибки то дальше мы попадает в onFulfill
следующего then
. И еще не забываем что () => {}
возвращает не пустой объект, а undefined
, чтобы вернуть пустой объект надо писать так: () => ({})
порядок выполнения задач.
Что выведет этот код?
async function get1() {
return 1;
}
function get2() {
return 2;
}
(async () => {
console.log(await get1());
})();
console.log(get2());
a) 1,2
b) 2,1
c) 1
d) 2
b) 2,1
Что выведет этот код?
setTimeout(() => {console.log('in timeout')});
Promise.resolve()
.then(() => {console.log('in promise')});
console.log('after');
a) in timeout, in promise, after
b) after, in promise, in timeout
c) after, in timeout, in promise
d) in timeout, after, in promise
b) after, in promise, in timeout
Что выведет этот код?
let __promise = new Promise((res, rej) => {
setTimeout(res, 1000);
});
async function test(i) {
await __promise;
console.log(i);
}
test(1);
test(2);
a) 1, 2
b) 2, 1
c) 1
d) 2
a) 1, 2
Что выведет этот код?
console.log('FUS');
setTimeout(() => {console.log('RO')})
Promise.resolve('DAH!').then(x => console.log(x));
a FUS RO DAH!
b) FUS DAH! RO
c) RO FUS DAH!
d) DAH! RO FUS
b) FUS DAH! RO
Что выведет этот код?
console.log(1);
setTimeout(() => console.log('setTimeout'), 0);
console.log(2);
Promise.resolve().then(() => console.log('promise1 resolved'));
console.log(3);
a) 1, 2, 3, 'setTimeout', 'promise1 resolved'
b) 1, 'setTimeout', 2, 'promise1 resolved', 3
c) 1, 2, 3, 'promise1 resolved', 'setTimeout'
d) 1, 2, 'promise1 resolved', 3, 'setTimeout'
c) 1, 2, 3, 'promise1 resolved', 'setTimeout'
Сначала срабатывают все синхронные вызовы, после этого, когда call stack пуст, вызывается то что попало в очередь (асинхронные задачи). Сначала выполняются микротаски — промисы и mutation observer
. В конце текущей таски выполняются все микротаски, в связи с этим микротасками можно заблокировать event loop, после того как таска завершена в браузере происходит рендеринг. После этого выполняются макро таски — таймауты.
Это очень упрощенный пример, более подробно я бы советовал посмотреть выступление Михаила Башурова
И последний вопрос promise против await
Что выведет этот код?
const p = Promise.resolve();
(async () => {
await p;
console.log('1');
})();
p.then(() => console.log('2'))
.then(() => console.log('3'));
a) 1 2 3
b) 2 1 3
c) 2 3 1
d) 3 2 1
c) 2 3 1
Согласно спеке сначала должны выполнится промисы добавленные через then
и только после этого нужно продолжить
выполнение асинхронной функции. Спека. Для более подробного понимания, почему это так, советую почитать отличную статью на v8.dev