С момента выхода моей предыдущей статьи о вопросах на собеседованиях Задачи с собеседований (front-end) прошло практически два года.

Но так сложилось, что я снова вышла на тропу войны собеседований, что дало благодатный материал для нового списка вопросов.

Этот виток собеседований ознаменовался большим обилием алгоритмических задач. Сложно сказать тенденция ли это, или это мне так повезло.

Когда вся страна уже вышла на работу, а программисты, как избранные, продолжают работать из дома, еще не поздно найти какую-нибудь вторую работу.


Задача: Написать полифилл для Promise.

Целесообразность такого вопроса вызывает вопросы, даже если нужно поддерживать пресловутый IE 11 (процент использования которого на момент написания статьи колеблется где-то в районе 0.5% — 1.5% по разным статистическим данным), то есть куча готовых полифиллов.

Однако, он, вероятно, призван показать знание того, как в принципе работает Promise, что из себя представляет и как с ним работать. Ну что же, можно и с этой стороны зайти, конечно, но можно было бы и просто спросить «как работать с Promise».

Еще пару раз просто спрашивали теоретически, как бы я реализовала.

Решение
function Promise(fn) {
	this.thenHandlers = [];
	this.catchHandlers = [];
	this.isResolved = false;
	this.isRejected = false;

	setTimeout(() => fn(this.applyResolve.bind(this), this.applyReject.bind(this)));
}

Promise.prototype = {

	applyResolve: function () {
		this.thenHandlers.forEach((handler) => handler());
		this.isResolved = true;
	},

	applyReject: function () {
		this.catchHandlers.forEach((handler) => handler());
		this.isRejected = true;
	},

	then: function (handler) {
		if (this.isResolved) {
			handler();
		} else {
			this.thenHandlers.push(handler);
		}

		return this;
	},

	catch: function (handler) {
		if (this.isRejected) {
			handler();
		} else {
			this.catchHandlers.push(handler);
		}

		return this;
	}

};

const p = new Promise((resolve, reject) => (
	Math.round(Math.random() * 10) % 2 === 0
		? resolve()
		: reject()
));

p
	.then(function () {
		console.log('resolved');
	})
	.catch(function () {
		console.log('rejected');
	});


Задача: Реализовать аналог Promise.all.

Решение
function promiseAll(promises) {
	return new Promise((resolve, reject) => {
		const results = [];
		let resolvedCount = 0;

		promises.forEach((promise, index) => {
			promise
				.then((result) => {
					results[index] = result;

					resolvedCount++;

					if (resolvedCount === promises.length) {
						resolve(results);
					}
				})
				.catch((err) => reject(err));
		});
	});
}

promiseAll([
	new Promise((resolve) => {
		setTimeout(() => resolve('foo'), 5000)
	}),

	new Promise((resolve, reject) => {
		setTimeout(() => resolve('bar'), 1000);
	}),

	new Promise((resolve, reject) => {
		setTimeout(() => {
			Math.round(Math.random() * 10) % 2 === 0
				? resolve('baz')
				: reject(new Error());
		}, 300);
	}),
])
	.then((res) => console.log('RESOLVED: ', res))
	.catch((err) => console.log('REJECTED: ', err));


Задача: Реализовать аналог Function.prototype.bind.

С появлением Rest parameters реализация этой задачи стала чуть проще, чем прежде, когда приходилось делать arguments.slice.

Решение
Function.prototype.bind = function(context, ...argsBind) {
	const fn = this;

	return function (...args) {
		return fn.apply(context, argsBind.concat(args))
	};
};


Вопрос: Что такое делегирование событий? Плюсы/минусы/подводные камни.

Ответ
Делегирование событий — подход при работе с событиями DOM-дерева, при котором обработчики событий добавляются не на каждый конкретный элемент, а только на общий родительский, в то время как необходимость вызова это обработчика для конкретного интересующего нас элемента определяется через ин
ициатора события, узнать который можно из свойства объекта события event.target.
Такой подход возможен благодаря особенностям событийной модели DOM-дерева, а конкретно такой особенности, как всплытие событий.

Плюсы

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

Кроме прочего это в лучшую сторону отразится на потреблении памяти, если таких элементов будет немало.

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

Минусы

Чуть меньшая «прозрачность» и очевидность по сравнению с тем когда обработчик события добавляется непосредственно для нужного элемента.

Потенциально чуть больше нагрузки ложится на CPU, т.к. любое возникновения события внутри родительского элемента запускает обработчик и поиск необходимого элемента для проверки необходимости работы основного обработчика. Так что тут всё индивидуально, в каждом конкретном случае надо смотреть насколько вероятны возникновения событий, которые будут приходиться «мимо» целевых элементов и что нам более важно оптимизировать — память или CPU.

Подводные камни

Любой неосторожный event.stopPropagation может прервать цепочку всплытия события и оно не дойдёт до родительского элемента, на котором установлен обработчик.

Если реализуете делегирование собственноручно, а не используете готовое решение, то не стоит забывать про тот нюанс, что недостаточно проверить event.target на соответствие требуемому элементу. Необходимо искать по дереву вверх до тех пор пока либо не встретим либо этот элемент, либо родительский, либо дойдём до вершины дерева. Причиной тому служит всё то же всплытие событий — инициатором события может оказаться один из элементов, вложенных в наш, а не он сам.

Задача: Логическое продолжение предыдущего вопроса — реализовать делегирование.

Решение
<div class="wrapper">
	<div class="child"><div><div><div>click me</div></div></div></div>
	<div class="child"><div><div><div>click me</div></div></div></div>
	<div class="child"><div><div><div>click me</div></div></div></div>
	<div class="other"><div><div><div>dont't click me</div></div></div></div>
</div>

const delegate = (eventName, el, selector, handler) => {
	el.addEventListener(eventName, (event) => {
		let node = event.target;
		const items = [].slice.call(el.querySelectorAll(selector));

		if (items.length) {
			while (node !== el && node !== null) {
				const isTarget = items.some(item => node === item);

				if (isTarget) {
					handler(node);
					break;
				} else {
					node = node.parentNode;
				}
			}
		}
	});
};

delegate('click', document.querySelector('.wrapper'), '.child', (el) => el.style.backgroundColor = 'blue');



Задача: Уникализация значений в массиве.

Необходимо написать функцию, принимающую в аргументах массив целых чисел и возвращающую новый массив, состоящий только из уникальных значений первого массива.

Например:

unique([1, 1, 2, 2, 4, 2, 3, 7, 3]); // => [1, 2, 4, 3, 7]

Решение в лоб
function unique(arr) {
	const res = [];

	arr.forEach((item) => {
		if (res.indexOf(item) === -1) {
			res.push(item);
		}
	});

	return res;
}


Ожидания интервьюера
function unique(arr) {
	const res = {};

	arr.forEach((item) => {
		res[item] = '';
	});

	return Object.keys(res).map(item => Number(item));
}


Решение в одну строку
function unique(arr) {
	return arr.filter((item, index, self) => (self.indexOf(item) === index));
}


Задача: «Расплющивание» массива.

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

Например:

flat([1, [2, [3, [4,5]]]]); // => [1, 2, 3, 4, 5]

Решение
function flat(arr) {
	let res = [];
	
	arr.forEach((item) => {
		if (Array.isArray(item)) {
			res = res.concat(flat(item));
		} else {
			res.push(item);
		}
	});

	return res;
}


Ну или...
Есть нативный метод — Array.prototype.flat
Я считаю, что именно с него надо начать ответ на этот вопрос, и только когда (именно когда, а не если) интервьюер скажет, что такое решение ему не подходит и нужно всё сделать руками, приниматься за вышеупомянутое решение через рекурсию.
[1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]].flat(Infinity);


Задача: Написать функцию, принимающую аргументом массив чисел и возвращающую новый массив, состоящий из удвоенных значений первого.

Например:

f([1, 2, null, 7, 8, null, 3]); // => [2, 4, 14, 16, 6]

Решение
function f(arr) {
	return arr
		.filter(item => item !== null)
		.map(item => item * 2);
}


Задача: Обход дерева

Дана структура данных в виде дерева:

const tree = {
	value: 1,
	children: [
		{
			value: 2,
			children: [
				{ value: 4 },
				{ value: 5 },
			]
		},
		{
			value: 3,
			children: [
				{ value: 6 },
				{ value: 7 },
			]
		}
	]
};

Необходимо написать функцию, возвращающую значения всех вершин дерева:

getTreeValues(tree); // => [1, 2, 3, 4, 5, 6, 7]

Решение
Через рекурсию:

function getTreeValues(tree) {
	let values = [ tree.value ];

	if (Array.isArray(tree.children)) {
		tree.children.forEach(item => values = values.concat(getTreeValues(item)));
	}

	return values;
}

Через цикл:

function getTreeValues(tree) {
	const tmpTree = [tree];
	const res = [];
	let current;

	while (tmpTree.length > 0) {
		current = tmpTree.shift();
		res.push(current.value);

		if (current.children) {
			current.children.forEach(item => tmpTree.push(item));
		}
	}

	return res
}


Задача: Сумма вершин дерева

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

Решение
Через рекурсию:

function getTreeSum(node) {
	let sum = node.value;

	if (Array.isArray(node.children)) {
		node.children.forEach(item => sum += getTreeSum(item));
	}

	return sum;
}

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

Задача: Сортировка нечётных.

Необходимо написать функцию, принимающую в аргументах массив и возвращающую новый массив, в котором отсортированы все нечетные числа по возрастанию, в то время как чётные остаются на своих местах.

Например:

oddSort([7, 3, 4, 9, 5, 2, 17]); // => [3, 5, 4, 7, 9, 2, 17]

Решение
function oddSort(arr) {
	arr.forEach((item, index) => {
		if (item % 2 === 1) {
			let sortNumber = item;

			for (let i = 0; i < index; i++) {
				if (arr[i] % 2 === 1) {
					if (arr[i] > sortNumber) {
						const tmp = sortNumber;

						sortNumber = arr[i];
						arr[i] = tmp;
					}
				}
			}
			arr[index] = sortNumber;
		}
	});

	return arr;
}


Задача: Идентичный алфавит

Необходимо написать функцию, принимающую в аргументах две строки и возвращающую true, если эти строки состоят из идентичных букв и false в противном случае.

Например:

isEqualSymbols('кот', 'ток'); // => true
isEqualSymbols('кот', 'тик'); // => false

Я уже писала про эту задачу в предыдущей части, но есть вариант решения получше.

Решение
function isEqualSymbols(str1, str2) {
	if (str1.length !== str2.length) {
		return false;
	}

	if (str1.split('').sort().join('') === str2.split('').sort().join('')) {
		return true;
	}

	return false;
}

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

Если же будет необходимо исходить из того, что символы не уникальны сами по себе и повторные символы должны игнорироваться, то можно предварительно уникализировать символы в обоих строках, воспользовавшись одним из решений из задачи об уникализации значений в массиве.

Задача: Бомба

Надо реализовать «бомбу» (в виде функции-конструктора), которая получает на входе время, через которое взорвется и некоторый «звук взрыва» (строку, которую вернет через заданное время). С фантазией задача.

Решение
function Bomb(message, delay) {
	this.message = message;

	setTimeout(this.blowUp.bind(this), delay * 1000); // взрываем через delay sec
}

Bomb.prototype.blowUp = function () {
	console.log(this.message);
};

new Bomb("Explosion!", .5);


Задача: «Сжатие строк»

Необходимо реализовать функцию, принимающую в аргументах строку, состоящую из букв и вернуть новую строку, в которой повторяющиеся буквы заменены количеством повторений.

Например:

rle('AVVVBBBVVXDHJFFFFDDDDDDHAAAAJJJDDSLSSSDDDD'); // => 'AV3B3V2XDHJF4D6HA4J3D2SLS3D4'

Решение
function rle(str) {
	const result = [str[0]];
	let count = 1;

	for (let i = 1; i < str.length; i++) {
		if (str[i] === str[i - 1]) {
			count++;

			if (i === str.length - 1) {
				result.push(str[i]);
				if (count > 1) {
					result.push(count);
				}
			}
		} else {
			if (i > 1) {
				result.push(str[i - 1]);
			}

			if (i === str.length - 1) {
				result.push(str[i]);
			}

			if (count > 1) {
				result.push(count);
			}

			count = 1;
		}
	}

	return result.join('');
}


Вопрос: Что получится в результате выполнения кода и почему?

var obj = {};

function func(x) {
	x = 1;

	return x;
}

func(obj); // => ?
console.log(obj); // => ?

Ответ
Функция вернёт 1, obj при этом не изменится;
Несмотря на то, что объекты в JavaScript передаются в параметры функций по ссылке, obj не изменится.

Внутри функции создаётся локальная переменная x, в которую изначально попадет ссылка на obj, но позже эта переменная переписывается на числовое значение 1. Т.е. меняется само значение переменной x, но меняется значение, которое находится по ссылке, переданной изначально в функцию.

Вопрос: Как передать изображение размером 10Mb с помощью GET-запроса?

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

P.S.: Вопрос из разряда тех, на которые нет правильного ответа, потому как единственно верным ответом на этот вопрос был бы — не делайте так, не отправляйте файлы методом GET, даже не думайте об этом и всё будет хорошо.

И это не тот случай когда хочется услышать размышления, а дойдёт ли собеседуемый до ожидаемого ответа или нет уже не так важно. Потому как в этом случае можно было бы поставить задачу иначе — указать некоторые дополнительные требования к загрузке файлов: например, чт файлы могут быть неприлично большого размера и необходимо реализовать загрузку с возможностью догрузки, если в каком-то временном промежутке у клиента будут проблемы с сетью. Я имею ввиду, что если хочется услышать про возможность нарезки файлов на клиенте, то можно придумать условия и поадекватнее.

Вопрос: Назовите известные вам HTTP-методы. Что такое CRUD?

Ответ
Про HTTP-методы и CRUD:
GET — read — используется только для получения данных.
POST — create — создание новых сущностей.
PUT/PATCH — update — обновление данных.
DELETE — delete — удаление.

Вообще HTTP-методов сильно больше, помимо выше перечисленных есть OPTIONS, HEAD, TRACE и др.

Есть ещё пул вопросов, связанных с фреймворками и смежными с frontend'ом темами, возможно оформлю их в отдельную статью.