[Перевод] 23 непростых вопроса для JavaScript-собеседования
Хотите подготовиться к собеседованию по JavaScript и ищете вопросы, на которых можно попрактиковаться? Если так — считайте, что ваши поиски окончены. Автор материала, перевод которого мы сегодня публикуем, говорит, что собрал более двух десятков вопросов по JavaScript, предназначенных для тех, кто хочет превратиться из джуниора в сеньора, для тех, кто стремится успешно пройти собеседование в сфере фронтенд-разработки и получить интересное предложение от работодателя.
1. Объясните особенности проверки равенства значений в JavaScript
Сложность: *
В JavaScript есть два оператора для проверки равенства величин. Первый — это так называемый оператор строгого равенства. Второй — оператор нестрогого равенства, при использовании которого может производиться преобразование типов проверяемых величин.
- Оператор строгого равенства (
===
) проверяет значения на равенство, не выполняя при этом преобразования типов. - Оператор нестрогого равенства (
==
) проверяет значения на равенство, выполняя их приведение к общему типу.
var a = "42";
var b = 42;
a == b; // true
a === b; // false
Вот некоторые правила, касающиеся использования различных операторов проверки равенства в JavaScript:
- Если любое из сравниваемых значений может быть значением
true
илиfalse
— постарайтесь избегать оператора==
. Используйте оператор===
. - Используйте оператор
===
в том случае, если работаете со следующими значениями:0
,«»
или[]
(пустой массив). - Во всех остальных случаях можете безопасно использовать оператор
==
. Причём, это не только безопасно, но и способствует упрощению кода и улучшению его читабельности.
→ Источник
2. Приведите примеры приведения к логическому типу значений, не относящихся к этому типу
Сложность: ***
Суть этого вопроса в том, чтобы выяснить, какие значения, в случае преобразования их к логическому типу, превращаются в false
, а какие — в true
.
Вот список значений, которые можно назвать «ложными» (falsy). Они, при преобразовании к логическому типу, превращаются в значение false
:
«»
(пустая строка).0
,-0
,NaN
(не-число).null
,undefined
.
«Ложным» является и логическое значение false
.
Любое значение, которое не входит в этот список, при его преобразовании к логическому типу, превращается в true
(такие значения называют «истинными» — truthy). Например:
«hello»
.42
.[ ]
,[ 1, «2», 3 ]
(массивы).{ }
,{ a: 42 }
(объекты).function foo() { .. }
(функции).
«Истинным» является и логическое значение true
.
→ Источник
3. Что такое IIFE?
Сложность: ***
IIFE (Immediately Invoked Function Expression) — это немедленно вызываемое функциональное выражение. Такое выражение выполняется немедленно после создания.
(function IIFE(){
console.log( "Hello!" );
})();
// "Hello!"
Этот паттерн часто используется для того чтобы не допустить загрязнения глобального пространства имён. Дело в том, что переменные, объявленные в IIFE (как и в любой другой обычной функции), невидимы за пределами этой функции.
→ Источник
4. Когда следует использовать стрелочные функции, которые появились в ES6?
Сложность: ***
Вот простые правила по использованию различных способов объявления функций, которыми я руководствуюсь, разрабатывая код для сред, поддерживающих стандарт ES6 и более новые стандарты:
- Используйте ключевое слово
function
в глобальной области видимости и для свойствObject.prototype
. - Используйте ключевое слово
function
для конструкторов объектов. - В остальных случаях используйте стрелочные функции.
Как видите, стрелочные функции рекомендуется использовать практически везде. У такого положения дел есть несколько причин:
- Удобная работа с контекстом. Стрелочные функции используют значение
this
окружающего контекста, не имея собственногоthis
. Если такие функции применяются последовательно, без использования обычных функций в сложных конструкциях, это обеспечивает безопасную работу с контекстом. - Компактность. Код стрелочных функций легче вводить и легче читать. Возможно, это преимущество стрелочных функций перед обычными покажется вам спорным и зависящим от точки зрения каждого конкретного разработчика.
- Ясность кода. Если практически весь код представлен стрелочными функциями, любая обычная функция выделяется в таком коде тем, что создаёт собственный контекст. Применяя стрелочные функции, программист создаёт более понятный код, в котором легче, чем в коде без стрелочных функций, работать с
this
.
→ Источник
5. В чём разница между ES6-классами и конструкторами функций?
Сложность: ***
Сначала рассмотрим примеры.
Функция-конструктор:
function Person(name) {
this.name = name;
}
ES6-класс:
class Person {
constructor(name) {
this.name = name;
}
}
Если речь идёт о создании простых объектов, то конструкторы и классы, используемые для этой цели, выглядят очень похоже.
Основная разница между конструкторами и классами проявляется при использовании наследования. Если нам нужно создать класс Student
, являющийся подклассом класса Person
, и добавить к этому новому классу поле studentId
, то вот как будет выглядеть код, в котором используются конструкторы, и код, в котором применяются классы.
Функция-конструктор:
function Student(name, studentId) {
// Вызываем конструктор суперкласса для инициализации полей, унаследованных от него.
Person.call(this, name);
// Инициализация собственных полей объекта.
this.studentId = studentId;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
ES6-класс:
class Student extends Person {
constructor(name, studentId) {
super(name);
this.studentId = studentId;
}
}
→ Источник
6. Расскажите о методе Function.prototype.bind ()
Сложность: ***
Процитируем MDN: «Метод bind()
создаёт новую функцию, которая при вызове устанавливает в качестве контекста выполнения this
предоставленное значение. В метод также передаётся набор аргументов, которые будут установлены перед переданными в привязанную функцию аргументами при её вызове».
Полагаю, что метод .bind()
особенно полезен для привязки значения this
в методах классов, которые нужно передавать в другие функции. Этот приём часто используется в React-компонентах.
→ Источник
7. Для чего обычно используются анонимные функции?
Сложность: ***
Анонимные функции используются при создании IIFE — конструкций, переменные, объявленные в которых, не загрязняют глобальную область видимости.
(function() {
// Какой-то код.
})();
Анонимные функции применяют в качестве функций обратного вызова, которые используются лишь в одном месте программы. Код будет выглядеть более самодостаточным и читабельным в том случае, если коллбэк будет объявлен прямо в том месте, где он используется. Это избавляет от необходимости просматривать код в поиске тела функции.
setTimeout(function() {
console.log('Hello world!');
}, 1000);
Анонимные функции удобно использовать в конструкциях, характерных для функционального стиля программирования, или при работе с библиотеками вроде Lodash (этот вариант их использования похож на их применение в качестве коллбэков).
const arr = [1, 2, 3];
const double = arr.map(function(el) {
return el * 2;
});
console.log(double); // [2, 4, 6]
→ Источник
8. В чём разница между методом Object.freeze () и ключевым словом const?
Сложность: ***
Ключевое слово const
и метод Object.freeze()
— это совершенно разные вещи.
Ключевое слово const
применяется к привязкам (к «переменным»). Оно создаёт иммутабельную привязку, то есть — к переменной (константе), объявленной с помощью ключевого слова const
, нельзя привязать что-то новое. Константе нельзя присвоить новое значение.
const person = {
name: "Leonardo"
};
let animal = {
species: "snake"
};
person = animal; // Uncaught TypeError: Assignment to constant variable.
Метод Object.freeze()
работает со значениями. А точнее — с объектными значениями. Он делает объект иммутабельным, что защищает от изменений значения свойств этого объекта.
let person = {
name: "Leonardo"
};
Object.freeze(person);
person.name = "Lima"; // Uncaught TypeError: Cannot assign to read only property 'name' of object
console.log(person);
Обратите внимание на то, что сообщение об ошибке выводится в строгом режиме. В обычном режиме операция изменения свойства «замороженного» объекта просто не срабатывает.
→ Источник
9. Что такое «генератор»?
Сложность: ***
Генераторы — это функции, из которых можно «выходить», и в которые можно «входить» по мере необходимости. Их контекст (привязки переменных) сохраняется между сеансами «входа» в них. Генераторы объявляют с использованием ключевого слова function*
. Такая функция, при её первом вызове, не выполняет код, возвращая особый объект, генератор, который позволяет управлять её выполнением. Для получения очередного значения, выдаваемого генератором, нужно вызвать его метод next()
. Благодаря этому выполняется код функции до тех пор, пока в нём не встретится ключевое слово yield
, возвращающее значение.
Функцию-генератор можно вызывать сколько угодно раз. Каждый раз будет возвращаться новый генератор. Но каждый генератор можно обойти лишь один раз.
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
let iterationCount = 0;
for (let i = start; i < end; i += step) {
iterationCount++;
yield i;
}
return iterationCount;
}
→ Источник
10. Когда стоит использовать генераторы?
Сложность: ***
Если в двух словах описать основные полезные возможности генераторов, то окажется, что они заключаются в следующем:
- Код, в котором используется генератор, сам определяет момент получения следующего значения. Генератор отвечает только за возврат значений, управление им осуществляется извне.
- Существуют асинхронные генераторы. Они позволяют работать с асинхронными потоками данных.
Главное в генераторах — это то, что получить следующее значение, возвращаемое генератором, можно только тогда, когда оно нужно в коде, использующем генератор. Генераторы не возвращают всё за один раз. В некоторых ситуациях эта их особенность может оказаться весьма удобной.
→ Источник
11. Что такое «поднятие переменных»?
Сложность: ****
Сущность концепции «поднятия переменных» заключается в том, что объявления «поднимаются» в верхнюю часть текущей области видимости. В результате переменной можно воспользоваться до её объявления. Поднимаются лишь объявления переменных, но не код их инициализации. Обратите внимание на то, что поведение переменных, объявляемых с использованием ключевого слова var
, отличается от поведения переменных и констант, объявленных с использованием let
и const
.
→ Источник
12. Что выведет следующий код?
Сложность: ****
var output = (function(x) {
delete x;
return x;
})(0);
console.log(output);
Этот код выведет 0
. Оператор delete
используется для удаления свойств объектов. А x
— это не свойство объекта — это локальная переменная. Оператор delete
не воздействует на локальные переменные.
→ Источник
13. Что выведет следующий код?
Сложность: ****
var Employee = {
company: 'xyz'
}
var emp1 = Object.create(Employee);
delete emp1.company
console.log(emp1.company);
Этот код выведет xyz
. Свойство company
является не свойством объекта emp1
, а свойством его прототипа. Оператор delete
не удаляет свойства прототипов объектов. У объекта emp1
нет собственного свойства company
. Проверить это можно так:
console.log(emp1.hasOwnProperty('company')); // false
Если нам всё же необходимо удалить это свойство — сделать это можно, либо напрямую обратившись к объекту Employee
(delete Employee.company
), либо — обратившись к прототипу объекта emp1
, воспользовавшись его свойством __proto__
(delete emp1.__proto__.company
).
→ Источник
14. Расскажите о шаблоне проектирования «Прототип»
Сложность: ****
Прототип (Prototype) — это порождающий шаблон проектирования. Он используется для создания объектов. Объекты, созданные с его помощью, содержат значения, скопированные из их прототипа (из объекта-образца). Этот шаблон ещё называют шаблоном Свойства (Properties).
Пример использования паттерна «прототип» — это инициализация неких объектов стандартными значениями, хранящимися в базе данных. Такие значения, записанные в прототип, копируются в новые объекты без обращения к базе данных.
Надо отметить, что этот паттерн редко используется в классических языках. В JavaScript применяется модель прототипного наследования. Данный паттерн применяется при конструировании новых объектов и их прототипов.
→ Источник
15. Что такое «временная мёртвая зона» в ES6?
Сложность: ****
В ES6 выполняется подъём переменных и констант, объявленных с использованием ключевых слов let
и const
(выполняется и подъём сущностей, объявленных с использованием ключевых слов var
, class
и function
). Однако в коде имеется зона, простирающаяся от входа в область видимости до объявления переменной или константы. При обращении к переменной или константе в этой зоне будет выдана ошибка. Это и есть «временная мёртвая зона» (Temporal Dead Zone, TDZ).
//console.log(aLet) // выбросит ReferenceError
let aLet;
console.log(aLet); // undefined
aLet = 10;
console.log(aLet); // 10
В данном примере TDZ заканчивается после объявления aLet
, но не после присвоения aLet
значения.
→ Источник
16. Можете ли вы описать основное различие методов массивов forEach () и map ()? В каких ситуациях вы предпочли бы один из этих методов другому?
Сложность: ****
Для того чтобы понять разницу между этими методами — поговорим об особенностях работы каждого из них.
Вот как работает .forEach()
:
- Он перебирает элементы массива.
- Он выполняет переданную ему функцию обратного вызова для каждого элемента массива.
- Он ничего не возвращает.
const a = [1, 2, 3];
const doubled = a.forEach((num, index) => {
// Сделать что-то с num и/или с index.
});
// doubled = undefined
Вот краткая характеристика метода .map()
:
- Он перебирает элементы массива.
- Он преобразует каждый элемент исходного массива в элемент нового массива, вызывая переданную ему функцию для каждого элемента исходного массива.
const a = [1, 2, 3];
const doubled = a.map(num => {
return num * 2;
});
// doubled = [2, 4, 6]
В результате оказывается, что основное различие между .forEach()
и .map()
заключается в том, что .map()
возвращает новый массив. Если вам нужно получить результат преобразования элементов исходного массива, не меняя этот массив, тогда стоит выбрать .map()
. Если же нужно просто перебрать элементы массива — тогда можно воспользоваться .forEach()
.
→ Источник
17. Чем отличаются друг от друга необъявленная переменная, переменная, содержащая значение null, и undefined-переменная? Как проверить переменную на предмет того, что она необъявлена, а также на null и undefined?
Сложность: ****
Необъявленная переменная создаётся при назначении значения идентификатору, который не был ранее объявлен с использованием var
, let
или const
. Необъявленные переменные объявляются в глобальной области видимости, за пределами текущей области видимости. В строгом режиме при попытке назначения значения необъявленной переменной будет выброшено исключение ReferenceError
. Использовать необъявленные переменные не рекомендуется — так же, как не рекомендуется использовать глобальные переменные. Их стоит всеми силами избегать. Для того чтобы обезопасить себя от последствий использования необъявленных переменных, воспользуйтесь блоком try/catch
.
function foo() {
x = 1; // Выбрасывает в строгом режиме ReferenceError
}
foo();
console.log(x); // 1
Переменная, содержащая undefined
— это объявленная переменная, которой не назначено некое значение. Значение undefined
образует собственный тип данных. Если функция ничего не возвращает, и при этом результат её вызова записывается в переменную, то в эту переменную попадёт undefined
. Для того чтобы организовать проверку на undefined
, можно воспользоваться оператором строгого равенства (===
) или оператором typeof
, который возвратит строку undefined
. Обратите внимание на то, что при проверке на undefined
не следует пользоваться оператором нестрогого равенства (==
), так как он считает равными значения undefined
и null
.
var foo;
console.log(foo); // undefined
console.log(foo === undefined); // true
console.log(typeof foo === 'undefined'); // true
console.log(foo == null); // true. Не используйте такую конструкцию для проверки на undefined!
function bar() {}
var baz = bar();
console.log(baz); // undefined
Переменная, содержащая значение null
, должна быть явным образом установлена в это значение. Она символизирует отсутствие значения и отличается от undefined
-переменной тем, что значение, находящееся в ней, было ей явным образом назначено. Для того чтобы проверить значение на null
, достаточно воспользоваться оператором строгого равенства. Для проверки на null
, как и в случае с проверкой на undefined
, не следует пользоваться оператором нестрогого равенства, считающим равными значения null
и undefined
.
var foo = null;
console.log(foo === null); // true
console.log(typeof foo === 'object'); // true
console.log(foo == undefined); // true Не используйте такую конструкцию для проверки на null!
Я стараюсь никогда не оставлять переменные в необъявленном состоянии, или в состоянии, когда они объявлены, но им явным образом не назначено никакого значения. Если я не собираюсь записывать в переменную какое-то значение сразу после её объявления, я записываю в неё null
. Если вы пользуетесь линтером, то он обычно сообщает о случаях использования необъявленных переменных.
→ Источник
18. Расскажите о шаблоне проектирования «Открытый модуль»
Сложность: *****
Шаблон «Открытый модуль» (Revealing Module) является разновидностью шаблона «Модуль» (Module). Цель использования этого шаблона заключается в поддержке инкапсуляции и в открытии некоторых свойств и методов, возвращённых в объектном литерале. Вот как будет выглядеть непосредственная реализация этого шаблона:
var Exposer = (function() {
var privateVariable = 10;
var privateMethod = function() {
console.log('Inside a private method!');
privateVariable++;
}
var methodToExpose = function() {
console.log('This is a method I want to expose!');
}
var otherMethodIWantToExpose = function() {
privateMethod();
}
return {
first: methodToExpose,
second: otherMethodIWantToExpose
};
})();
Exposer.first(); // Вывод: This is a method I want to expose!
Exposer.second(); // Вывод: Inside a private method!
Exposer.methodToExpose; // undefined
Очевидный недостаток этого шаблона заключается в том, что при его использовании нельзя обращаться к приватным методам.
→ Источник
19. В чём разница между объектами Map и WeakMap?
Сложность: *****
Эти объекты ведут себя по-разному в том случае, если переменная, содержащая ссылку на объект, являющийся ключом одной из пар ключ/значение, оказывается недоступной. Вот пример:
var map = new Map();
var weakmap = new WeakMap();
(function() {
var a = {
x: 12
};
var b = {
y: 12
};
map.set(a, 1);
weakmap.set(b, 2);
})()
После того, как завершается выполнение IIFE, у нас уже не будет доступа к объектам a
и b
. Поэтому сборщик мусора удаляет ключ b
из weakmap
и очищает память. А вот содержимое map
остаётся при этом неизменным.
В результате оказывается, что объекты WeakMap
позволяют сборщику мусора избавляться от тех своих записей, на ключи которых нет ссылок во внешних переменных. Объекты map
хранят пары ключ/значение вне зависимости от наличия или отсутствия внешних ссылок на ключи. То же самое можно сказать и о реализации структуры данных Map
с использованием обычных массивов. В WeakMap
используются «слабые» ссылки на ключи. Они не препятствуют работе сборщика мусора в том случае, если на объект, используемый в роли ключа, нет других ссылок.
→ Источник
20. Как в JavaScript-функции передаются параметры: по ссылке или по значению?
Сложность: *****
Параметры всегда передаются по значению, но в переменные, представляющие объекты, записаны ссылки на объекты. Поэтому, когда в функцию передают объект и меняют свойство этого объекта, это изменение сохраняется в объекте и при выходе из функции. В результате возникает ощущение того, что параметры в функции передаются по ссылке. Но если изменить значение переменной, представляющей объект, это изменение не повлияет на объекты, находящиеся за пределами функции.
Вот пример:
function changeStuff(a, b, c)
{
a = a * 10;
b.item = "changed";
c = {item: "changed"};
}
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
changeStuff(num, obj1, obj2);
console.log(num);
console.log(obj1.item);
console.log(obj2.item);
Вот что выведет этот код:
10
changed
unchanged
→ Источник
21. Как организовать «глубокую заморозку» объекта?
Сложность: *****
Для того чтобы обеспечить «глубокую заморозку» объекта с использованием Object.freeze()
, нужно создать рекурсивную функцию, которая «замораживает» свойства объекта, которые также являются объектами.
Вот пример обычной «заморозки» объекта:
let person = {
name: "Leonardo",
profession: {
name: "developer"
}
};
Object.freeze(person); // делает объект иммутабельным
person.profession.name = "doctor";
console.log(person); //вывод { name: 'Leonardo', profession: { name: 'doctor' } }
Вот — «глубокая заморозка»:
function deepFreeze(object) {
let propNames = Object.getOwnPropertyNames(object);
for (let name of propNames) {
let value = object[name];
object[name] = value && typeof value === "object" ?
deepFreeze(value) : value;
}
return Object.freeze(object);
}
let person = {
name: "Leonardo",
profession: {
name: "developer"
}
};
deepFreeze(person);
person.profession.name = "doctor"; // TypeError: Cannot assign to read only property 'name' of object
Сообщение об ошибке выводится лишь в строгом режиме. В обычном режиме значение не меняется без вывода сообщений об ошибках.
→ Источник
22. Почему JavaScript-программисты испытывают проблемы при использовании ключевого слова this?
Сложность: *****
Самое важное, что нужно понять о this
, заключается в том, что у функций нет фиксированного значения this
. Это значение зависит от того, как именно вызывается функция. Если мы говорим о том, что функция вызывается с некоторым конкретным значением this
, это значит, что это значение определяется не во время объявления функции, а во время её вызова. Вот некоторые особенности this
:
- Если функция вызывается в обычном виде (то есть, с использованием конструкции вида
someFunc()
), тоthis
будет ссылаться на глобальный объект (в браузере этоwindow
). Если код выполняется в строгом режиме, то вthis
будет записано значениеundefined
. - Если функция вызывается как метод объекта, то ключевое слово
this
будет представлено объектом, которому принадлежит метод. - Если функцию вызывают с использованием call или apply,
this
будет представлено тем, что указано в качестве первого аргументаcall
илиapply
. - Если функция вызывается в виде обработчика события, то в
this
будет целевой элемент события. - Если функцию вызывают в виде конструктора, с использованием ключевого слова
new
, то вthis
будет новый объект, прототип которого установлен в качестве свойстваprototype
функции-конструктора. - Если функция создана с использованием метода bind, то ключевое слово
this
функции будет жёстко привязано к значению, переданномуbind
в качестве первого аргумента. Это — единственное исключение из правила, в соответствии с которым функции не имеют жёстко заданного значенияthis
. Функции, созданные с использованиемbind
, имеют иммутабельное значениеthis
.
→ Источник
23. Сравните использование конструкции async/await и генераторов для реализации одного и того же функционала
Сложность: *****
- При итерировании генератора с использованием метода
.next()
каждый вызов этого метода приводит к возврату одного значения с помощью ключевого словаyield
. При использовании конструкции async/await await-выражения выполняются последовательно. - Конструкция async/await упрощает реализацию определённого сценария использования генераторов.
- Значения, возвращаемые генератором, всегда имеют вид
{value: X, done: Boolean}
, а асинхронные функции возвращают промисы, разрешаемые со значениемX
, либо завершаются с ошибкой. - Асинхронную функцию можно преобразовать в генератор, использующий промисы. Ниже приведён пример такого преобразования.
Вот асинхронная функция:
// Асинхронная функция
async function init() {
const res1 = await doTask1();
console.log(res1);
const res2 = await doTask2(res1);
console.log(res2);
const res3 = await doTask3(res2);
console.log(res3);
return res3;
}
init();
Вот аналогичный генератор.
// Эта функция выполняет генератор
function runner(genFn) {
const itr = genFn();
function run(arg) {
let result = itr.next(arg);
if (result.done) {
return result.value;
} else {
return Promise.resolve(result.value).then(run);
}
}
return run;
}
// Вызывает функцию runner с передачей ей генератора
runner(function* () {
const res1 = await doTask1();
console.log(res1);
const res2 = await doTask2(res1);
console.log(res2);
const res3 = await doTask3(res2);
console.log(res3);
return res3;
});
→ Источник
Уважаемые читатели! Какие вопросы по JavaScript встречались вам на собеседованиях?