Пару месяцев назад прошла самая большая конференция за Уралом - CodeFest X. На конференции собрались почти 3к специалистов, сотни докладов и море положительных эмоций. Возможно я когда-нибудь соберусь с силами и напишу самые интересные мысли и идеи, которые излагались спикерами.
Помимо докладов было очень много интерактива, в том числе и опросы (квизы). В одном из них я занял второе место и выиграл умную колонку irbis с Алисой внутри от Plesk. Как оказалось, в этих, на первый взгляд, бессмысленных и запутанных задачках, есть интересная теоретическая составляющая. Сегодня хочу разобрать наиболее понравившиеся примеры.
Что бы было интереснее, вы сначала сможете сами пройти квиз (всего 13 самых интересных вопросов) и проверить, насколько хорошо разбираетесь в понимании, как работает js 👨🎓. А потом почитать, почему это работает именно так. Под каждой задачкой я буду оставлять ссылки на материалы по теме.
После прохождения квиза появятся ссылки на неправильно отвеченные задачи, если такие будут.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Что выведет 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?, в которой хорошо разобраны разные примеры приведения типов.
Что выведется в консоль:
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.
Что выведет 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;
Первое, что приходит в голову, что поднятие для let/const в принципе отсутствует. Но на самом деле hosting для let работает, но по-другому: имя этой переменной «резервируется» для нее с самого начала выполнения интерпретатором блока, но при попытке обратится к переменной мы будем получать ошибку ReferenceError. Даже при наличии внешней переменной с тем же именем!
Подобное поведение называется “временной мёртвой зоной”. Подробнее можно почитать тут: «ES6: Let, Const и «Временная мёртвая зона» (ВМЗ) изнутри».
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
, ornew.target
. Any reference toarguments
,super
,this
, ornew.target
within an ArrowFunction must resolve to a binding in a lexically enclosing environment.
Следовательно, при вызове стрелочной функции this
будет браться из внешней области видимости (в браузере это window) и this.x
будет равно undefined
.
Какой будет результат?
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();
}
Это кстати, единственное задание, которое я решил неверно.
В каком порядке будут выведены результаты?
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 можно представить в виде бесконечного цикла, который ожидает новое событие из очереди событий и запускает его обработчик (когда завершено предыдущее событие, т.е. когда стек вывозов становится пустым):
while(queue.waitForMessage()){
queue.processNextMessage();
}
Цикл отдает приоритет стеку вызовов, и сначала он обрабатывает все, что находит в нет, и, когда там ничего нет, он начинает обрабатывать очередь событий (message queue).
Посмотреть в интерактивном примере как это работает можно посмотреть на демо примере Филиппа Робертса: http://latentflip.com/loupe/ и рекомендую видео от него же: https://www.youtube.com/watch?v=8aGhZQkoFbQ (Единственное, интерактивный пример не работает с промисами, так что имейте это ввиду).
Один цикл обработки событий будет иметь ровно одну задачу, обрабатываемую из очереди макрозадач. К этим задачам относятся например setTimeout-ы. После завершения этой макрозадачи будут обработаны все доступные микрозадачи (к ним как раз относятся промисы), в рамках одного цикла обработки. Пока эти микрозадачи обрабатываются, они могут ставить в очередь еще больше микрозадач, которые будут выполняться одна за другой до тех пор, пока очередь микрозадач не закончится.
Очередь микрозадач имеет больший приоритет, чем очередь макрозадач.
Сразу хорошая аналогия: Вы на горнолыжке стоите 20 минут в очереди на подъемник. Вы - макрозадача, ждете пока остальные макрозадачи выполнятся. Тут подъезжает инструктор, объезжает всю очередь и проходит через специальный проход для персонала. Он - микрозадача. Как только текущая макрозадача загрузится на подъемник, микрозадача втиснется и залезет в следующую кабинку.
Примеры:
setTimeout
, setInterval
, setImmediate
, requestAnimationFrame
, I/O
, рендеринг пользовательского интерфейсаprocess.nextTick
, Promises
, Object.observe
, MutationObserver
Возвращаясь к задаче: сначала выведется А
, далее в очередь событий добавится макрозадача (setTimeout), далее создаётся микрозадача (первая цепочка от промиса), и выведется E
. Далее обрабатывается микрозадача, выводится C
и создается еще одна микрозадача, которая выполняется (выводится D
) и только после этого начинает выполняться макрозадача - выводится B
.
Что выведет 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
.
Чему будет равно proto.prop?
const proto = { prop: 'a' };
const obj = Object.create(proto);
obj.prop = 'b';
Рекомендую почитать материал о прототипах на learn.javascript. Если кратко, то Object.create создает объект, ставя в его прототип объект, переданный первым аргументом.
При чтении свойства из объекта, если его в нём нет, оно ищется в прототипе. Операции присвоения или удаления совершаются всегда над самим объектом, т.е. прототип неявно задействован только для чтения.
В задании при выполнении obj.prop = 'b'
свойство добавляется в объект obj
, не изменяя значения свойства в proto.prop
.
Что выведет 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
имеет смысл только у функций конструкторов.
Что будет выведено console.log-ом?
const bar = typeof class extends Array {};
console.log(bar);
О классах в javascript можно сказать, что это синтаксический сахар над функциями конструкторами (посмотреть, как babel представляет эту запись в ES5 можно тут). Поэтому тут все просто, typeof
от функции конструктора вернет "function"
.
Какой из вариантов использования 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 выше класса ни на что не будет влиять и добавлена просто для запутывания :)
Какой будет результат?
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
.
Если существует элемент с id
my_elem
и у него есть вложенные элементы, то чему будет равно значениеdocument.getElementById('my_elem').lastChild.nextSibling
в таком случае?
На самом деле тут достаточно скинуть ссылку на MDN, где говорится, что nextSibling
возвращает либо следующий элемент, либо null
, если элемент последний. А lastChild
возвращает последний дочерний элемент.
Начав писать эту заметку, я не мог и представить, что на нее уйдет столько сил и времени. Поэтому надеюсь, если вы не синьор-помидор javascript разработчик, то многие темы оказались интересными и стало ясно, в какие области языка стоит погрузиться для более полного понимания. А если все решили правильно, то улыбнулись над гифкой в самом конце :).
Вообще, разобрав темы, которые были подняты в этих вопросах можно без проблем начать проходить кучу технических интервью на знание javascript-а, так что дерзайте.