Разбор JavaScript квиза с CodeFest

Пару месяцев назад прошла самая большая конференция за Уралом - CodeFest X. На конференции собрались почти 3к специалистов, сотни докладов и море положительных эмоций. Возможно я когда-нибудь соберусь с силами и напишу самые интересные мысли и идеи, которые излагались спикерами.

Помимо докладов было очень много интерактива, в том числе и опросы (квизы). В одном из них я занял второе место и выиграл умную колонку irbis с Алисой внутри от Plesk. Как оказалось, в этих, на первый взгляд, бессмысленных и запутанных задачках, есть интересная теоретическая составляющая. Сегодня хочу разобрать наиболее понравившиеся примеры.

Выигранная колонка Irbis A

Что бы было интереснее, вы сначала сможете сами пройти квиз (всего 13 самых интересных вопросов) и проверить, насколько хорошо разбираетесь в понимании, как работает js 👨‍🎓. А потом почитать, почему это работает именно так. Под каждой задачкой я буду оставлять ссылки на материалы по теме.

Квиз по JavaScript #

Скопировать ссылку на квиз

После прохождения квиза появятся ссылки на неправильно отвеченные задачи, если такие будут.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

Разбор #

1 #

Что выведет console.log?

console.log([] + 1 + 2 + '')

Задача на знание того, чему будет равно [] + ?. При выполнении операции сложения два операнда приводятся к примитивному типу: сначала идет попытка преобразовать значение к примитиву через valueOf (в случае с массивом вернется массив, т.е. не примитивный тип), потом toString (для массивов toString равносилен [].join(), следовательно вернется строка). Т.е. изначальный пример можно переписать как

console.log('' + 1 + 2 + '')

Дальше уже проще, при сложении строки и числа, число преобразовывается к строке:

'' + 1 => '' + '1' => '1'
'1' + 2 => '1' + '2' => '12'
'12' + '' => '12

Получается, что [] + 1 + 2 + '' === '12'. И правильный ответ равен 12.

Рекомендую почитать статью What is {} + {} in JavaScript?, в которой хорошо разобраны разные примеры приведения типов.


2 #

Что выведется в консоль:

const User = () => {
  this.name = "Nina";
};
const user = new User();
console.log(user.name);

Тут все довольно просто, если знать три отличия стрелочных функций от не стрелочных (или одно из них). Первое, это лексическая привязка к значению this (нет собственного this). Второе - нет объекта arguments. И третье - нельзя использовать вместе с директивой new.

Поэтому при попытке вызова new User() мы получим исключение, что User - это не конструктор (TypeError: User is not a constructor). Про стрелочные функции можно почитать на learn.javascript или на jsraccon.


3 #

Что выведет console.log?

console.log(a);
let a = 1;
a += 2;

Это задачка на знание hoisting (поднятие переменных) и temporal dead zone (временной мёртвой зоны). Поднятие переменных применимо как для var (область видимости на уровне функций), так и для let/const (блочная область видимости).

Интерпретатор всегда перемещает («поднимает») объявления функций и переменных в начало области видимости. Следующий фрагмент кода (⚠️ обратите внимание, что я использую var):

function foo() {
    console.log(x);
    var x = 1;
}

На самом деле интерпретируется так:

function foo() {
    var x;
    console.log(x); // undefined
    x = 1;
}

Но если запустить код из примера, то получим ошибку:

console.log(a); // ReferenceError: a is not defined
let a = 1;
a += 2;

What?

Первое, что приходит в голову, что поднятие для let/const в принципе отсутствует. Но на самом деле hosting для let работает, но по-другому: имя этой переменной «резервируется» для нее с самого начала выполнения интерпретатором блока, но при попытке обратится к переменной мы будем получать ошибку ReferenceError. Даже при наличии внешней переменной с тем же именем!

Подобное поведение называется “временной мёртвой зоной”. Подробнее можно почитать тут: «ES6: Let, Const и «Временная мёртвая зона» (ВМЗ) изнутри».


4 #

let bar = () => this.x;
let foo = bar.bind({ x: 3 });
console.log(foo.call({ x: 5 }));

Буду краток: нельзя "rebind" (применять bind) к стрелочным функциям. Они всегда будут вызываться с контекстом, в котором они были определены. Ссылка на спецификацию: http://www.ecma-international.org/ecma-262/6.0/#sec-arrow-function-definitions-runtime-semantics-evaluation и выжимка от туда:

An ArrowFunction does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment.

Следовательно, при вызове стрелочной функции this будет браться из внешней области видимости (в браузере это window) и this.x будет равно undefined.


5 #

Какой будет результат?

function bar() {
  try {
    throw new Error('Oops...');
  } catch (e) {
    throw e;
    return 1;
  } finally {
    return 2;
  }
  return 3;
}
const foo = bar();

Как работает try-catch я рассказывать не буду, а вот на операторе finally остановлюсь. Если он указан, то код внутри него будет выполнен в любом случае, неважно что указано в try или catch (return, exception, break или continue), но он выполнится непосредственно перед вызовом return/exception/break/continue из try/catch блоков (Хороший пример можно посмотреть на 2ality).

Самое интересное, что если использовать в блоке finally return, то именно это значение и вернется из функции, хотя на первый взгляд может показаться, что throw e внутри catch должно выкинуться наружу. Если бы в finally не было оператора return, то ошибка бы выбросилась наружу, можете попробовать сами, запустив следующий код:

function bar() {
  try {
    throw new Error('Oops...');
  } catch (e) {
    throw e;
    return 1;
  } finally {
    console.log('finally is executed')
  }
  return 3;
}

bar(); // Uncaught Error: Oops...

Возвращаясь к задаче, получаем следующее:

Начинаем выполнять блок try, бросаем ошибку, которая ловится в catch, из catch бросаем ошибку дальше; начинает выполняться finally и возвращает 2 (тем самым «схлопывая» исключение).

Этот способ можно использовать на практике, если нужно освободить ресурсы (в том числе при возникновении ошибки) даже без использования catch (но в данном примере мы никогда не узнаем, произошла ли ошибка или нет, поэтому при использовании такой конструкции нужно хорошенько подумать, точно ли вам это нужно):

try {
  // work with file ...
} finally {
  return closeFile();
}

Это кстати, единственное задание, которое я решил неверно.


6 #

В каком порядке будут выведены результаты?

console.log('A');
setTimeout(() => {
  console.log('B');
}, 0);
Promise.resolve().then(() => {
  console.log('C');
}).then(() => {
  console.log('D');
});
console.log('E');

Эту задачу просто решить, зная, что такое event loop (событийный цикл) и разницу между микрозадачами и макрозадачами.

По этой теме уже есть огромное количество материала, но я все же хочу поделится ссылками и простыми словами попытаться объяснить, почему console.log внутри промиса всегда выполняется быстрее setTimeout-а.

Если очень кратко: порядок выполнения будет такой: обычный код, затем обработка промисов, затем все остальное, например, события/setTimeout и т.д.

Что такое событейный цикл, стек вызовов и очередь событий (или callback queue) вы очень кратко можете почитать на mdn, на javascript.info или на медиуме. А так же крутой доклад «Иван Тулуп: асинхронщина в JS под капотом»: видео + текстовая версия доступны тут: https://habr.com/ru/company/oleg-bunin/blog/417461/

Event loop

Резюмируя статьи, модель event loop можно представить в виде бесконечного цикла, который ожидает новое событие из очереди событий и запускает его обработчик (когда завершено предыдущее событие, т.е. когда стек вывозов становится пустым):

while(queue.waitForMessage()){
  queue.processNextMessage();
}

Цикл отдает приоритет стеку вызовов, и сначала он обрабатывает все, что находит в нет, и, когда там ничего нет, он начинает обрабатывать очередь событий (message queue).

Посмотреть в интерактивном примере как это работает можно посмотреть на демо примере Филиппа Робертса: http://latentflip.com/loupe/ и рекомендую видео от него же: https://www.youtube.com/watch?v=8aGhZQkoFbQ (Единственное, интерактивный пример не работает с промисами, так что имейте это ввиду).

Микрозадачи #

Один цикл обработки событий будет иметь ровно одну задачу, обрабатываемую из очереди макрозадач. К этим задачам относятся например setTimeout-ы. После завершения этой макрозадачи будут обработаны все доступные микрозадачи (к ним как раз относятся промисы), в рамках одного цикла обработки. Пока эти микрозадачи обрабатываются, они могут ставить в очередь еще больше микрозадач, которые будут выполняться одна за другой до тех пор, пока очередь микрозадач не закончится.

Очередь микрозадач имеет больший приоритет, чем очередь макрозадач.

Сразу хорошая аналогия: Вы на горнолыжке стоите 20 минут в очереди на подъемник. Вы - макрозадача, ждете пока остальные макрозадачи выполнятся. Тут подъезжает инструктор, объезжает всю очередь и проходит через специальный проход для персонала. Он - микрозадача. Как только текущая макрозадача загрузится на подъемник, микрозадача втиснется и залезет в следующую кабинку.

Микрозадачи vs макрозадачи?

- Сноубордист тут микрозадача
- А лыжник - твой продакшен макрозадача

Примеры:

  • Макрозадачи: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, рендеринг пользовательского интерфейса
  • Микрозадачи: process.nextTick, Promises, Object.observe, MutationObserver

Возвращаясь к задаче: сначала выведется А, далее в очередь событий добавится макрозадача (setTimeout), далее создаётся микрозадача (первая цепочка от промиса), и выведется E. Далее обрабатывается микрозадача, выводится C и создается еще одна микрозадача, которая выполняется (выводится D) и только после этого начинает выполняться макрозадача - выводится B.


7 #

Что выведет console.log?

console.log(typeof typeof (1 / 0));

Маленький пример, но не менее интересный. С учетом приоритетов операторов (табличку можно посмотреть тут), сначала выполнится выражение внутри скобок, далее typeof-ы. Результат выполнения 1 / 0 будет равен Infinity, т.е. получаем

console.log(typeof typeof Infinity);

Infinity принадлежит типу number, следовательно имеем:

console.log(typeof 'number');

Далее думаю вы и сами догадались, что typeof от строки (а "number" это строка) вернет "string".

Если бы скобок не было, то ответ был бы другим. Сначала выполнились typeof-ы (так как у них приоритет выше, чем у оператора деления) и в итоге мы получили выражение "string" / 0, которое после выполнения вернет NaN.



8 #

Чему будет равно proto.prop?

const proto = { prop: 'a' };
const obj = Object.create(proto);
obj.prop = 'b';

Рекомендую почитать материал о прототипах на learn.javascript. Если кратко, то Object.create создает объект, ставя в его прототип объект, переданный первым аргументом.

При чтении свойства из объекта, если его в нём нет, оно ищется в прототипе. Операции присвоения или удаления совершаются всегда над самим объектом, т.е. прототип неявно задействован только для чтения.

В задании при выполнении obj.prop = 'b' свойство добавляется в объект obj, не изменяя значения свойства в proto.prop.


9 #

Что выведет console.log?

const foo = { prop: 1 };
const bar = Object.create(foo);

console.log(
  bar == foo,
  bar.__proto__ == foo,
  Object.getPrototypeOf(bar) === foo.prototype
);

Как и предыдущая задача, она на знание прототипов. foo будет поставлен в прототип объекта bar.

bar.__proto__     <---- foo
foo.__proto__     <---- object
object.__proto__  <---- null

bar.__proto__.__proto__.__proto__ === null

Сразу становится понятно, что первое выражение равно false, а второе true. Что касается последнего, то foo.prototype будет равно undefined. Свойство prototype указывает не на прототип объекта, а на объект, который будет выставлен в качестве прототипа объекта, созданного с помощью new + функции конструктора. Т.е. свойство prototype имеет смысл только у функций конструкторов.


10 #

Что будет выведено console.log-ом?

const bar = typeof class extends Array {};

console.log(bar);

О классах в javascript можно сказать, что это синтаксический сахар над функциями конструкторами (посмотреть, как babel представляет эту запись в ES5 можно тут). Поэтому тут все просто, typeof от функции конструктора вернет "function".


11 #

Какой из вариантов использования static некорректен в ES2018?

const defaultProps = {
  display: false,
};
class Foo {
  // 1
  static defaultProps;
  // 2
  static render() { }
  // 3
  static get classes() {}
}

В ES2015 (ES6) в javascript-е появились классы. Они быстро набрали популярность, хоть и были синтаксическим сахаром над функциями конструкторами. В них были и конструкторы, и геттеры с сеттерами и статические методы (статические геттеры тоже были). По мне, одним из недостатков классов было отсутствие возможности задания полей классов - их можно было задавать только через конструктор.

В 2017 вышел прополз class fields, который описывает работу полей класса. Сейчас эта фича доступна в Chrome v72, Firefox и Nodejs 12, но до сих пор не вошла в стандарт, находясь на stage 3 (Все фичи, которые еще не вошли в стандарт, можно неформально относить к ESNext).

Ответом будет первый вариант, он пока не стандартизирован. Кстати переменная defaultProps выше класса ни на что не будет влиять и добавлена просто для запутывания :)


12 #

Какой будет результат?

const bar = {
  prop: 1,
};

class Foo {
  constructor(prop) {
    this.bar = bar;
    bar.prop = prop;
    return bar;
  }
  print() {
    console.log(this.bar.prop);
  }
}
const foo = new Foo(2);
foo.print();

Опять классы, опять возвращаемся к функциям конструкторам. Если из конструктора вернуть примитивный объект, то интерпретатор проигнорирует его и вернет this (ссылку на созданный инстанс класса). Но если вернуть ссылочный тип, то вернется именно он.

В примере выше все вызовы new Foo() будут возвращать ссылку на переменную bar:

const foo = new Foo(2); // foo === bar

У bar нет метода print, и при попытке его вызова получим ошибку foo.print is not a function.


13 #

Если существует элемент с id my_elem и у него есть вложенные элементы, то чему будет равно значение document.getElementById('my_elem').lastChild.nextSibling в таком случае?

На самом деле тут достаточно скинуть ссылку на MDN, где говорится, что nextSibling возвращает либо следующий элемент, либо null, если элемент последний. А lastChild возвращает последний дочерний элемент.


Итак #

Начав писать эту заметку, я не мог и представить, что на нее уйдет столько сил и времени. Поэтому надеюсь, если вы не синьор-помидор javascript разработчик, то многие темы оказались интересными и стало ясно, в какие области языка стоит погрузиться для более полного понимания. А если все решили правильно, то улыбнулись над гифкой в самом конце :).

Вообще, разобрав темы, которые были подняты в этих вопросах можно без проблем начать проходить кучу технических интервью на знание javascript-а, так что дерзайте.

Оставайтесь на связи
Чтобы не пропустить новые посты или анонсы проектов, которые я делаю, вы можете присоединиться к телеграм каналу. А так же оставаться на связи, задавать вопросы или просто вместе обсуждать различные инженерные темы.
Присоединиться

Вам может быть интересно

Что нового в es2019: Array.prototype.flat, Object.fromEntries

08/02/2019
Недавно стандарт ES2019 был окончательно утвержден, включая несколько новых фич. Все фичи уже реализованы в Chrome 73
2

Обзор митапа JS Party в новосибирске

01/03/2019
2

История одной оптимизации React приложения

29/03/2020
Рассмотрим как оптимизировать React приложения на реальном примере
16