Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
460 lines (326 sloc) 25.8 KB
title date description tags layout
Разбор JavaScript квиза с CodeFest
2019-06-29
Разбираем 13 интересных вопросов c javascript квиза
javascript
codefest
layouts/post.njk

Пару месяцев назад прошла самая большая конференция за Уралом - 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, который запустит выполнение внутреннего typeof, который в свою очередь выполнит 1 / 0, значение которого будет равно NaN, т.е. получаем

console.log(typeof typeof NaN);

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

console.log(typeof 'number');

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


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-а, так что дерзайте.

You can’t perform that action at this time.