Analysis of issues at the hh.ru stand at # HolyJS18
We tried to do something interesting and unusual for you. I really hope that we have succeeded. We did not want to leave you without answers and explanations why this is so. Let's figure it out.
First I want to remind you how the competition took place, there were 4 rounds with 15 questions about JS, 1 out-of-competition round with 15 questions about React and a final with 10 questions.
Under the cut - analysis of the tasks of the first 4 rounds.
This is the second part of our analysis.
Analysis of questions about React here
How did we all do it? We decided that we need to generate about 80-90 questions so that there is a stock to choose from. After that, we divided everything into topics:
- browser events
- various APIs (Array, Set, defineProperty, etc.),
- attentiveness
- working with fractional numbers
- hoisting
- event loop
- cast
- typeof
- logical (with logical AND and OR)
After that distributed questions on 4 rounds. We tried to make all the tours the same in complexity, for this we made several visits by passing these tests and determining where the questions were simpler, where it was more difficult and replaced the outstanding questions with more appropriate ones. And we did in each round about the same number of questions on a particular topic. As a result, it turned out that in different tours there were similar, but not identical questions.
Because of this, sorting out the tours seems not very convenient because there will be a lot of duplicate explanations, I suggest looking at them by topic. Let's start with the simplest.
Questions on attentiveness:
What will be displayed in the console?
console.log(0,1 + 0,2);
a) 0.30000000000000004
b) 0.3
c) 2
d) 0 1 2
d) 0 1 2
There is between the numbers ,
, and not .
if you format the question like this: console.log(0, 1 + 0, 2);
everything will become clear
What will be displayed in the console?
(() => {
'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
в строгом режиме такое запрещено.
What will be displayed in the console?
let foo = function bar() { return 123; };
console.log( typeof bar() );
a) 'function'
b) 'number'
c) 'undefined'
d) ошибка
d) ошибка
Это функциональное выражение (expression) — имя функции в данном случае является локальным для функции. Для вызова функции надо вызывать foo
, а не bar
. Если бы это было объявление (declaration) ответ был бы number
.
Questions about working with fractional numbers:
What will be displayed in the console?
console.log(0.1 ** 2);
a) 0.2
b) 0.01
c) 0.010000000000000002
d) NaN
c) 0.010000000000000002
What will be displayed in the console?
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 бита — все числа всегда двойной точности с плавающей запятой. Тоже самое произойдет при сложении.
We turn to the questions a little more difficult.
Events in the browser:
There is an event handler on the element. What values inside this handler will always be the same?
elem.onclick = function(event) { }
a) event.target и event.currentTarget
b) event.target и this
c) event.currentTarget и this
d) все варианты не верны
c) event.currentTarget и thisthis
— всегда будет указывать на элементcurrentTarget
— тот элемент на котором висит событиеtarget
— элемент на котором событие произошло
What will this code get by clicking on a 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
то сработал бы каждый из них, т.к. хендлеры добавляют события в очередь.
Questions section about various APIs
defineProperty:
What will this code output?
(() => {
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
What will this code output?
(() => {
'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, ошибка
What will this code output?
(() => {
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
движок притворится что все хорошо, но значение не поменяет, а в в строгом режиме будет ошибка.
increment:
What will this code output?
let x = 5;
console.log(x++);
a) 5
b) 6
c) '5++'
d) ошибка
a) 5
What will this code output?
const a = 5;
console.log(a++);
a) 5
b) 6
c) '5++'
d) ошибка
d) ошибка
При использовании постфиксной формы инкримента возвращается значение до увеличения.
А при префиксной после, т.е. console.log(++5)
распечатало бы 6
const
нельзя перезаписывать, а т.к. Number — это примитив то при его увеличении переменную перезапишет новым значением и будет ошибка.
Set:
What will this code output?
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]
What will this code output?
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
What will this code output?
let obj = {};
let set = new Set([obj, obj, {}, {}, {...{}}, {...obj}]);
console.log(set.size);
a) 6
b) 5
c) 2
d) 1
b) 5Set
— это множество, в нем по определению не может быть одинаковых значений. Вопрос в том как эти значения сравниваются. Примитивы сравниваются по значению, а объекты по ссылке.
Он сам по себе не приводит типы данных и может хранить значения любого типа 1e1
и 0xA
— будут преобразованы в десятичную систему и получится 10
.
А новые объекты всегда не равны: console.log({} == {})
выдаст false
т.к. объекты будут созданы по новой в разных местах памяти и их ссылки будут не равны.
What will this code output?
console.log(Infinity / Infinity);
a) NaN
b) 1
c) Error
d) Infinity
a) NaN
Делить бесконечность на бесконечность и вычесть бесконечность из бесконечности нельзя т.к. с математической точки зрения получается неопределенность, то же самое произойдет при умножении Infinity
и 0
ошибок математические операции не вызывают — будет NaN
Questions about Spread:
What will this code output?
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 }
What will this code output?
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 })
Все значения по умолчанию будут перекрыты переданными значениями, если они есть.
What will this code output?
console.log(parseInt(' -10,3 миллиона рублей '));
a) -10,3
b) -10
c) TypeError
d) NaN
b) -10
Исчерпывающее описание с MDN:
Если функция parseInt встречает символ, не являющийся числом в указанной системе счисления, она пропускает этот и все последующие символы (даже, если они подходящие) и возвращает целое число, преобразованное из части строки, предшествовавшей этому символу. parseInt отсекает дробную часть числа. Пробелы в начале и конце строки разрешены.
What will this code output?
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 18Proxy
перехватывает все обращения к объекту. В данном случае мы проксируем только get
метод и всегда возвращаем 12
независимо от того, к какому полю объекта мы обращаемся. При этом set
мы не трогаем, и при обращении к прокси значение в объекте будет заменено.
arrays:
What will this code output?
let arr = [];
arr[1] = 1;
arr[5] = 10;
console.log(arr.length);
a) 1
b) 5
c) 6
d) 10
c) 6
What will this code output?
let arr = new Array(3);
console.log(arr[1]);
a) undefined
b) 1
c) 3
d) ошибка
a) undefined
Когда мы создаем Array
с одним числовым аргументом — он означает длину массива. Массив при этом создается пустой, все значения undefined
. То же самое произойдет если создать обратиться к несуществующему полю массива. Стоит отметить, что если передать в Array
не число, вернется массив с этим элементом т.е. Array('a')
вернет ['a']
logical operations &&
, ||
, ==
etc .:
What will this code output?
console.log([] && 'foo' && undefined && true && false);
a) []
b) 'foo'
c) undefined
d) true
c) undefined
What will this code output?
console.log(0 || 1 && 2 || 3);
a) 0
b) 1
c) 2
d) 3
c) 2
What will this code output?
console.log(0 || '' || 2 || undefined || true || false);
a) 0
b) false
c) 2
d) true
c) 2
What will this code output?
console.log(2 && '1' && null && undefined && true && false);
a) 2
b) false
c) undefined
d) null
d) null
What will this code output?
console.log([] && {} || null && 100 || '');
a) true
b) 100
c) ''
d) {}
d) {}
Пустой масcив []
— это true
как и пустой объект {}
.
Пустая строка ''
, null
и undefined
— это false
Логическое или ||
— возвращает левый операнд, если он истинен, в остальных случаях возвращает правый.
Логическое и &&
— возвращает левый операнд, если он ложен, в остальных случаях возвращает правый.
Это можно иногда встретить в коде, до появления параметров по умолчанию часто писали так — если в функции нет параметров, то берем параметры по умолчанию:
functionf(userParams) {
var params = userParams || defaultParams;
}
Сейчас в React’е часто проверяют, если условие истинно, то рендерим что-то:
{ isDivVisible && <div>bla-bla</div> }
array comparison:
What will this code output?
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
What will this code output?
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]
What will this code output?
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
What will this code output?
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
What will this code output?
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
не равным самому себе.
Questions about hoisting:
What will this code output?
console.log(str);
const str = 'HeadHunter';
a) 'HeadHunter'
b) undefined
c) ошибка
c) ошибка
What will this code output?
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
What will this code output?
console.log(str);
var str = 'HeadHunter';
a) 'HeadHunter'
b) undefined
c) null
c) будет ошибка
b) undefined
What will this code output?
console.log(foo);
var foo;
foo = foo ? 1 : 0;
console.log(foo);
a) ошибка
b) undefined 0
c) '' 1
d) 0 0
b) undefined 0
Does the function call work?
getCompanyName();
function getCompanyName() {
return 'HeadHunter';
}
a) да
b) нет, вызов должен стоять после объявления.
c) ошибка
a) да
What will this code output?
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]());
What will this code output?
console.log(true + false);
a) true
b) false
c) 1
d) 0
c) 1
Ни один из операторов строкой не является, +
приводит к числу. Получается 1 + 0
What will this code output?
console.log([] - 100 + ![]);
a) false
b) '-100'
c) -100
d) NaN
c) -100
Массив приводится к строке, после этого из-за -
приводим к числу, получается -100
, далее приводим массив к false
, а это 0
What will this code output?
console.log([[], []] + 1);
a) 1
b) '1'
c) ',1'
d) NaN
c) ',1'
Вызываем toString
на объекте, при этом toString
будет так же вызван на всех элементах массива. [].toString
вернет пустую строку ''
. Получается , + 1
— ответ ,1
.
What will this code output?
console.log([] + 100 + 5);
a) 105
b) '1005'
c) 1005
d) NaN
b) '1005'
Массив приводим к строке, и далее уже происходит конкатенация.
What will this code output?
console.log(1 + { a: 3 } + '2');
a) 6
b) '1[object Object]2'
c) 3
d) NaN
b) '1[object Object]2'
Преобразовываем к строке — тут просто конкатенация.
What will this code output?
console.log(10.toString() + 10 + 0x1);
a) '10101'
b) 21
c) '10100x1'
d) ошибка
d) ошибка
Для числа точка .
означает начало дробной части, мы ожидаем там число — будет ошибка.
Чтобы этот пример заработал нормально надо писать 10..toString()
What will this code output?
console.log(5 + false - null + true);
a) '0true'
b) NaN
c) 6
d) будет ошибка
c) 6
Тут все приводим к числу, получается 5 + 0 - 0 + 1
What will this code output?
console.log(true + NaN + false);
a) true
b) NaN
c) false
d) 1
b) NaN
Приводим все к числу, при сложении чисел с NaN
— получаем NaN
What will this code output?
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:
What will this code output?
let foo = () => { return null; };
console.log( typeof typeof foo );
a) 'function'
b) 'string'
c) 'null'
d) ошибка
b) 'string'
What will this code output?
typeof function() {}.prototype;
a) 'function'
b) 'object'
c) 'undefined'
d) ошибка
b) 'object'typeof
всегда возвращает строку, имеет меньший приоритет чем вызов функции, поэтому сначала выполняется функция, а typeof
применяется к возвращаемому ей результату. Объекты Function наследуются от Function.prototype. Спека явно определяет что это объект.
event loop:
Let's start with 2 questions about promises.
What will this code output?
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
What will this code output?
Promise.reject('foo')
.then(() => Promise.resolve('bar'), () => {})
.then((a) => {console.log(a)})
a) foo
b) bar
c) undefined
d) ошибка
c) undefinedPromise.reject
— возвращает промис в rejected состоянии.
Надо вспомнить что then
принимает 2 параметра, onFulfill
и onReject
колбеки. Если происходит ошибка до этого then
, то мы попадаем в onReject
колбек. Если в нем не происходит ошибки то дальше мы попадает в onFulfill
следующего then
. И еще не забываем что () => {}
возвращает не пустой объект, а undefined
, чтобы вернуть пустой объект надо писать так: () => ({})
order of tasks.
What will this code output?
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
What will this code output?
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
What will this code output?
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
What will this code output?
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
What will this code output?
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, после того как таска завершена в браузере происходит рендеринг. После этого выполняются макро таски — таймауты.
Это очень упрощенный пример, более подробно я бы советовал посмотреть выступление Михаила Башурова
And the last question promise against await
What will this code output?
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