Разбор вопросов на стенде hh.ru на #HolyJS18

Мы старались сделать для вас что-то интересное и необычное. Очень надеюсь что у нас получилось. Нам не хотелось оставлять вас без ответов и объяснений почему именно так. Давайте разбираться.

Для начала хочу напомнить как проходил конкурс, было 4 тура по 15 вопросов про JS, 1 внеконкурсный тур на 15 вопросов про React и финал на 10 вопросов.

image
Под катом — разбор задач первых 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


Что выведет этот код?

(() => {  
    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) ошибка  


Ответ


Что выведет этот код?

(() => {
    '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) ошибка 


Ответ


Что выведет этот код?

const a = 5;
console.log(a++);

a) 5  
b) 6  
c) '5++'  
d) ошибка  


Ответ

d) ошибка
При использовании постфиксной формы инкримента возвращается значение до увеличения.
А при префиксной после, т.е. console.log(++5) распечатало бы 6
const нельзя перезаписывать, а т.к. Number — это примитив то при его увеличении переменную перезапишет новым значением и будет ошибка.


Что выведет этот код?

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  


Ответ


Что выведет этот код?

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


Что выведет этот код?

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 


Ответ


Что выведет этот код?

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  


Ответ


Что выведет этот код?

console.log(0 || '' || 2 || undefined || true || false);

a) 0  
b) false  
c) 2   
d) true


Ответ


Что выведет этот код?

console.log(2 && '1' && null && undefined && true && false);

a) 2  
b) false  
c) undefined  
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 && 
bla-bla
}


Что выведет этот код?

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) ошибка  


Ответ


Что выведет этот код?

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) ошибка  


Ответ


Что выведет этот код?

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 


Ответ


Что выведет этот код?

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) ошибка 


Ответ


Что выведет этот код?

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.


Что выведет этот код?

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. Спека явно определяет что это объект.


Начнем с 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  


Ответ


Что выведет этот код?

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  


Ответ


Что выведет этот код?

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  


Ответ


Что выведет этот код?

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, после того как таска завершена в браузере происходит рендеринг. После этого выполняются макро таски — таймауты.
Это очень упрощенный пример, более подробно я бы советовал посмотреть выступление Михаила Башурова


Что выведет этот код?

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

© Habrahabr.ru