❓👨💻 Вопросы для подготовки к собеседованию по JavaScript. Часть 2
Продолжаем разбирать вопросы для джунов: рассказываем о прототипном наследовании, цикле событий, методах сохранения данных в браузере, конструкторах, генераторах, функциональных выражениях, микро- и макрозадачах.
21. Что такое DOM?
DOM (Document Object Model) – это модель, которая представляет HTML-документ в виде дерева тегов. Каждый HTML-тег в этом дереве является объектом.
Вложенные теги являются дочерними элементами по отношению к своему
родительскому элементу. Текст внутри тега также является объектом. Все эти объекты
доступны для любых манипуляций с помощью JavaScript, а эти манипуляции, в свою очередь, позволяют
динамически управлять содержимым страницы. В приведенном ниже примере мы
выбираем элемент с классом "story"
и сохраняем его в переменной
story.
Затем мы выбираем элементы с id "set-text"
и
"clear-text"
и добавляем им обработчики событий. Когда на элемент с
id "set-text"
нажимают, текстовое содержимое элемента с классом
"story"
меняется на "Ночь 31 декабря началась снегопадом и
метелью."
Когда нажимают на элемент с id "clear-text"
,
текстовое содержимое элемента с классом "story"
очищается:
<!DOCTYPE html> <html> <head> <title>Пример манипуляции DOM</title> </head> <body> <div class="story">Это текст новогодней истории.</div> <button id="set-text">Изменить div-элемент story</button> <button id="clear-text">Очистить div-элемент</button> <script> const story = document.body.querySelector(".story"); const setText = document.body.querySelector("#set-text"); setText.addEventListener("click", () => { story.textContent = "Ночь 31 декабря началась снегопадом и метелью."; }); const clearText = document.body.querySelector("#clear-text"); clearText.addEventListener("click", () => { story.textContent = ""; }); </script> </body> </html>
22. Что такое цикл событий?
Цикл событий в JavaScript – это механизм, который управляет выполнением кода. Он обеспечивает обработку событий и выполнение задач в правильном порядке. Хотя JavaScript работает в однопоточной среде, цикл событий дает возможность обрабатывать асинхронные операции и предотвращает блокировку основного потока выполнения.
Когда асинхронная операция (например, запрос к серверу) завершается, она помещает соответствующее событие в очередь событий. Цикл событий обрабатывает задачи в порядке их поступления. Он берет событие из очереди и передает его для выполнения. Если событие содержит обратный вызов или обработчик – вызывается соответствующая функция для выполнения кода, связанного с этим событием. Цикл событий также обрабатывает задачи, связанные с таймерами и промисами. Благодаря циклу событий, JavaScript быстро реагирует на действия пользователя и эффективно использует ресурсы при работе с асинхронными операциями .
В приведенном ниже примере, несмотря на то, что setTimeout(firstTask, 0)
имеет время ожидания 0
секунд, первая задача firstTask()
все равно не будет выполнена сразу после secondTask()
. Это происходит потому, что JavaScript использует цикл событий для управления выполнением асинхронных операций. Когда мы вызываем setTimeout()
, функция firstTask()
помещается в очередь событий, – цикл событий начинает обрабатывать эту функцию, когда стек вызовов опустеет.
function firstTask() { // Эмуляция долгой операции let i = 1000000000; while (i > 0) { i--; } console.log('Первая задача выполнена'); } function secondTask() { console.log('Вторая задача выполнена'); } setTimeout(firstTask, 0); secondTask();
23. Что такое прототипное наследование?
Прототипное наследование в JavaScript – это механизм, который позволяет одному объекту наследовать свойства и методы другого объекта. Это основной способ наследования в JavaScript.
Каждый объект в
JavaScript имеет внутреннее скрытое свойство prototype
, которое ссылается на другой объект. Этот
другой объект называется прототипом первого объекта. При попытке получить доступ к свойству объекта,
JavaScript сначала проверяет, есть ли это свойство в самом объекте. Если нет,
он ищет его в прототипе объекта. Если и там его нет, то в прототипе прототипа и
так далее. Если свойство или метод отсутствуют в объекте и его прототипе, JavaScript вернет undefined
:
// Прототип TV let TV = { brand: 'Generic', smart: true, resolution: '4k', turnOn() { console.log(`Включаем ${this.brand}`); }, turnOff() { console.log(`Выключаем ${this.brand}`); } }; // Создаем экземпляр Samsung TV let samsungTV = { __proto__: TV, brand: 'Samsung' }; // Создаем экземпляр Sony TV let sonyTV = { __proto__: TV, brand: 'Sony' }; // Используем существующие методы экземпляров samsungTV.turnOn(); // Включаем Samsung sonyTV.turnOff(); // Выключаем Sony // Вызываем существующее свойство console.log(sonyTV.smart); // true // Вызываем несуществующее свойство console.log(samsungTV.size); // undefined
24. Для чего нужен оператор опциональной последовательности?
Оператор
опциональной последовательности ?.
позволяет получить безопасный доступ к
вложенным свойствам объекта – даже в том случае, когда промежуточное свойство
отсутствует. Оператор ?.
прекращает оценку и возвращает undefined
, если часть
после ?.
является либо undefined
, либо null
.
let user = { name: 'Евгений', lastname: 'Онегин', address: { street: 'Невский проспект', city: 'Санкт-Петербург' } }; console.log(user.address?.street); // Невский проспект console.log(user?.lastname); // Oнегин console.log(user?.phone?.number); // undefined - нет ошибки console.log(user.phone.number); // Uncaught TypeError: Cannot read properties of undefined (reading 'number')
25. Что такое теневой DOM?
Shadow DOM – это техника, позволяющая создавать изолированные фрагменты HTML и CSS в специальном сегменте DOM, который находится внутри определенного элемента. Такой подход позволяет исключить влияние инкапсулированных стилей на структуру и внешний вид основной страницы, и обеспечивает большую гибкость и контроль над представлением и поведением элементов.
В приведенном ниже примере инкапсулированные стили пользовательского элемента не влияют на внешний вид остальной части страницы:
<p>Привет, мир!</p> <!-- Пользовательский элемент --> <my-element></my-element> <script> class MyElement extends HTMLElement { constructor() { super(); // Создаем Shadow DOM let shadow = this.attachShadow({mode: 'open'}); // Добавляем в Shadow DOM shadow.innerHTML = ` <style> p { color: #fff; background-color: #000; font-family: Consolas ; font-size: 25px; } </style> <p>Привет, мир!</p> `; } } // Регистрируем пользовательский элемент customElements.define('my-element', MyElement); </script>
26. Что такое рекурсия и как ее можно использовать в JavaScript?
Рекурсия в программировании – это процесс, в котором функция вызывает саму себя. Рекурсия обычно используется для решения задач, которые можно разбить на более простые подзадачи. В JavaScript рекурсию можно использовать, например, для работы с многоуровневыми массивами и обхода деревовидных структур данных.
Так можно преобразовать вложенный массив в одномерный:
const flattenArray = arr => arr.reduce((accumulator, value) => accumulator.concat( Array.isArray(value) ? flattenArray(value) : value ), []); let result = flattenArray([1, [2, [3, [4, 5], [6, 7, 8, [9, 10]]]]]); console.log(result); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
А так можно обойти и визуализировать дерево:
<!DOCTYPE html> <html> <body> <pre id="result"></pre> <script> let tree = { value: '-root', children: [ { value: '|child1', children: [ { value: '-|grandchild1' }, { value: '-|grandchild2' } ] }, { value: '|child2', children: [ { value: '-|grandchild3' }, { value: '-|grandchild4', children: [ { value: '--|great-grandchild1' }, { value: '--|great-grandchild2' } ] } ] } ] }; function traverseTree(node, level = 0) { let indent = ' '.repeat(level * 2); let resultDiv = document.getElementById('result'); resultDiv.innerHTML += `${indent}${node.value}\n`; if (node.children) { node.children.forEach(childNode => traverseTree(childNode, level + 1)); } } traverseTree(tree); </script> </body> </html>
Результат:
27. В чем разница между объявлением функции и функциональным выражением?
Объявление функции и функциональное выражение – это два способа определения функций в JavaScript.
Объявление функции – это традиционный способ определения функции. Функция создается и присваивается переменной, как любое другое значение. При этом объявленные функции доступны во всем коде, даже до того, как программа достигает того участка, где они определены:
console.log(typeof myFunction); // Вывод: function function myFunction() { console.log('Привет, админ!'); }
Функциональное выражение – это альтернативный способ определения функции:
let greet = function() { console.log('Привет, админ!'); } greet(); // Привет, админ!
В отличие от традиционной функции, функциональное выражение нельзя вызывать до определения в коде – это приведет к ошибке . Функциональные выражения могут:
- быть анонимными;
- формировать замыкания;
- передаваться в качестве аргументов другим функциям;
- использоваться как немедленно вызываемые функциональные выражения (IIFE).
28.Что такое функции-конструкторы?
Конструкторы в JavaScript – это специальные функции, используемые для создания объектов. Вот два основных правила при работе с конструкторами:
- Имя конструктора должно начинаться с заглавной буквы.
- Конструктор вызывается при помощи оператора
new
.
Когда мы вызываем
конструктор через new
, происходит следующее:
- Создается новый пустой объект и присваивается в
this
. - Выполняется код внутри конструктора. Обычно он модифицирует объект
this
, добавляя в него свойства. - Значение
this
возвращается из конструктора как результат.
Например:
function User(name) { this.name = name; this.sayHi = function() { alert(`Привет, меня зовут ${this.name}!`); }; } let user = new User("Вася"); user.sayHi(); // Привет, меня зовут Вася!
Здесь User – функция-конструктор. Когда мы
вызываем конструктор через new User("Вася")
,
создается объект user
с
указанным именем и методом sayHi
. Таким образом с помощью конструкторов можно многократного создавать
объекты по одному шаблону.
29. Как получить список ключей и значений объекта?
В JavaScript для получения списка ключей и значений объекта используются методы Object.keys() и Object.values().
Object.keys() возвращает массив со всеми ключами объекта:
const person = { name: "Егор", age: 30, city: "Ростов" }; const keys = Object.keys(person); console.log(keys); // ['name', 'age', 'city']
Object.values() возвращает массив со всеми значениями свойств объекта:
const person = { name: "Егор", age: 30, city: "Ростов" }; const values = Object.values(person); console.log(values); // ['Егор', 30, 'Ростов']
Используя Object.keys() и Object.values(), можно сделать
перебор:
const person = { name: "Егор", age: 30, city: "Ростов" }; Object.keys(person).forEach(key => { console.log(key, person[key]); });
Или вывести ключи и значения в виде объекта:
const person = { name: "Егор", age: 30, city: "Ростов" }; Object.entries(person).forEach(([key, value]) => { console.log(`${key}: ${value}`); });
30. Приведите примеры нововведений, добавленных в JavaScript в версии ES6
В ES6 множество нововведений, вот всего несколько примеров.
Деструктуризация объектов и массивов:
// Деструктуризация объекта const user = { name: "Алиса", age: 20 } const {name, age} = user; // name = "Алиса"; age = 20 // Деструктуризация массива const array = [1, 2, 3]; let [x, y] = array; // x = 1; y = 2
Шаблонные строки:
const name = "Инна"; console.log(`Привет, ${name}!`); // Привет, Инна!
Стрелочные функции:
const sum = (a, b) => a + b; console.log(sum(1, 2)); // 3
Параметры по умолчанию:
function volume(x = 1, y = 2, z = 3) { return x * y * z; } volume(); // 6 volume(2); // 12
Объявление переменных с помощью let и const.
let – аналог var, но с блочной областью видимости:
{ let x = 1; } console.log(x); // Ошибка, x не видна за пределами блока
const – объявление константы (переменной, которую нельзя изменить):
const PI = 3.14; PI = 2; // Ошибка, нельзя изменить значение
Оператор распространения (позволяет распаковывать элементы массива или объекта для аргументов функции или создания новых массивов/объектов):
const obj1 = { a: 1, b: 2 }; const obj2 = { c: 3, d: 4 }; const merged = { ...obj1, ...obj2 }; console.log(merged); // { a: 1, b: 2, c: 3, d: 4 }
31. Как происходит наследование классов в ES6?
Наследование классов в ES6 осуществляется с помощью ключевого слова extends
, которое следует за именем родительского класса. Родительский класс часто называют базовым классом, а класс, который наследует базовый/родительский класс, называется производным или дочерним:
class Parent { constructor(name) { this.name = name; } greeting() { return `Добрый день, ${this.name}!`; } } class Child extends Parent { constructor(name, age) { super(name); // вызывает конструктор родительского класса this.age = age; } greeting() { let greeting = super.greeting(); // вызывает метод родительского класса return `${greeting} Как поживаешь?`; } } let vasya = new Child('Вася', 10); console.log(vasya.greeting()); // Добрый день, Вася! Как поживаешь?
32. Что такое микрозадачи и макрозадачи?
В JavaScript микрозадачи и макрозадачи относятся к типам задач, которые должны выполняться в цикле событий.
Микрозадачи – это задачи, которые должны быть выполнены в текущем цикле событий перед тем, как браузер перерисует страницу. Они обычно добавляются в очередь выполнения с помощью методов, таких как Promise.then(), process.nextTick() (в Node.js) или MutationObserver. Примеры микрозадач – выполнение обработчиков промисов и мутации DOM.
С другой стороны, макрозадачи – это задачи, которые должны быть выполнены после окончания текущего цикла событий и перед тем, как изменения будут отрендерены на экране. Это включает задачи, добавленные в очередь событий с помощью setTimeout, setInterval, requestAnimationFrame, а также обработку входных событий и сетевых запросов. Макрозадачи выполняются после того, как завершается обработка всех микрозадач в текущем цикле событий.
Разница между микрозадачами и макрозадачами определяет порядок выполнения и позволяет управлять приоритетами различных задач в JavaScript. Микрозадачи имеют более высокий приоритет и выполняются до макрозадач, что позволяет быстрее обновлять интерфейс и предотвращает блокировку основного потока выполнения JavaScript. В приведенном ниже примере setTimeout является макрозадачей, а Promise.then() – микрозадачей. Поскольку микрозадачи имеют более высокий приоритет, Promise.then() выполняется перед setTimeout, и поэтому promise появляется в первую очередь:
setTimeout(() => alert("я - макрозадача")); Promise.resolve() .then(() => alert("я - микрозадача")); alert("я - синхронная операция основного кода");
33. Что такое генераторы?
Генераторы в JavaScript представляют собой специальный тип функций, которые генерируют последовательность значений по одному за раз по мере необходимости, и позволяют приостанавливать и возобновлять свое выполнение (в отличие от обычных функций, которые выполняются до завершения). Генераторы хорошо работают с объектами и упрощают создание потоков данных.
Чтобы объявить генератор, используют специальный синтаксис – функцию-генератор. Функция-генератор определяется с помощью символа *
после ключевого слова function:
function* generateNumbers() { yield 1; yield 2; yield 3; yield 4; return 5; } let gen = generateNumbers(); console.log(gen.next()); // { value: 1, done: false } console.log(gen.next()); // { value: 2, done: false } console.log(gen.next()); // { value: 3, done: false } console.log(gen.next()); // { value: 4, done: false } console.log(gen.next()); // { value: 5, done: true }
Генератор возвращает итератор, который можно использовать для контроля над выполнением функции. Основной метод итератора – next(). Когда вызывается next(), выполнение кода продолжается до ближайшего оператора yield. Когда достигнут yield, выполнение функции приостанавливается, и соответствующее значение возвращается во внешний код.
34. Какие существуют методы для сохранения данных в браузере?
Есть 3 основных метода хранения данных в браузере:
- LocalStorage и SessionStorage используются для хранения пар ключ-значение. Данные, сохраненные в них, сохраняются после обновления страницы. При этом только LocalStorage может сохранять данные после перезапуска браузера. Оба хранилища могут использовать только строки в качестве ключей и значений, поэтому объекты необходимо преобразовать с помощью JSON.stringify().
- Cookie – небольшие строки данных, которые хранятся в браузере. Cookie обычно устанавливаются веб-сервером с использованием заголовка Set-Cookie. Браузер затем автоматически добавляет их почти ко всем запросам на тот же домен с использованием заголовка Cookie. Один экземпляр cookie может содержать до 4 кб данных. В зависимости от браузера, допускается более 20 cookie на сайт.
- IndexedDB – встроенная база данных, более мощная, чем localStorage. Это NoSQL-хранилище данных в формате JSON внутри браузера, где доступны несколько типов ключей, а значения могут быть практически любым. IndexedDB поддерживает асинхронный доступ, транзакции для обеспечения согласованности данных и создание индексов для эффективного поиска. Позволяет хранить больше данных, чем localStorage, может быть связана с Service Workers и другими технологиями, которые обеспечивают функционирование PWA в оффлайне.
Вот примеры использования каждого из этих методов хранения данных:
// LocalStorage localStorage.setItem('фрукт', 'апельсин'); console.log(localStorage.getItem('фрукт')); // Выводит: апельсин // SessionStorage sessionStorage.setItem('тема', 'dark'); console.log(sessionStorage.getItem('тема')); // Выводит: dark // Cookie document.cookie = "username=Вася Пупкин; SameSite=None; Secure"; console.log(document.cookie); // Выводит: username=Вася Пупкин // IndexedDB let indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; if (!indexedDB) { console.log("IndexedDB не поддерживается"); } else { let open = indexedDB.open("myDatabase"); open.onupgradeneeded = function() { let db = open.result; db.createObjectStore("notes", {keyPath: "id"}); }; open.onsuccess = function() { console.log("База данных создана"); }; open.onerror = function() { console.log("Ошибка!"); }; }
35. В чем заключается разница между sessionStorage и localStorage?
Сессионное хранилище sessionStorage и локальное хранилище localStorage позволяют сохранять данные в формате ключ-значение в браузере. Оба они используются для хранения данных на стороне клиента, но имеют некоторые отличия:
- Объем хранимых данных – localStorage может хранить до 10 МБ данных, в то время как sessionStorage может хранить только до 5 МБ данных.
- Срок хранения данных – в localStorage данные не удаляются, когда закрывается браузер или вкладка. И напротив, данные в sessionStorage удаляются, когда закрывается вкладка или окно браузера.
- Доступность данных – из localStorage данные доступны в любом окне браузера, в то время как данные из sessionStorage доступны только из того же окна браузера, где они были сохранены.
Пример использования localStorage:
// Установка значения localStorage.setItem('жанр', 'триллер'); // Получение значения let value = localStorage.getItem('жанр'); console.log(value); // Удаление значения localStorage.removeItem('жанр'); // Очистка всего хранилища localStorage.clear();
Пример использования sessionStorage:
// Установка значения sessionStorage.setItem('стиль', 'индастриал'); // Получение значения let value = sessionStorage.getItem('стиль'); console.log(value); // Удаление значения sessionStorage.removeItem('стиль'); // Очистка всего хранилища sessionStorage.clear();
36. Что такое регулярные выражения?
Регулярные выражения (regex) – это паттерны, которые используются для поиска и замены текста в строках. В паттернах используются комбинации специальных символов (шаблоны) и флаги (модификаторы, которые определяют поведение поиска). Регулярные выражения часто используют в комбинации с этими методами:
- test() – проверка на соответствие шаблону
- match() – поиск соответствий
- replace() – замена по шаблону
- search() – поиск индекса
Составлять регулярные выражения можно с помощью конструктора RegExp или литеральной нотации.
Пример определения email-адреса в строке с помощью конструктора RegExp:
const emailRegex = new RegExp('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 'gi'); const email = 'Присылайте резюме на test@example.com, отвечу всем'; console.log(emailRegex.test(email)); // true
Пример извлечения телефонного номера с помощью литеральной нотации:
const phone = 'Теперь у меня новый номер +79876543210, на связи с 09:00 до 21:00'; const phoneRegex = /\+7\d{10}/gi; const found = phone.match(phoneRegex); if (found) { console.log(found[0]); } else { console.log('Совпадения не найдены'); }
37. В чем заключается разница между WeakSet, WeakMap и обычными Set и Map?
WeakSet и WeakMap – это специальные структуры данных в JavaScript, которые отличаются особенностью хранения ссылок на объекты.
В обычных Set и Map хранятся сильные ссылки на объекты. Это значит, что пока существует ссылка на объект в этих структурах, сборщик мусора не удалит этот объект из памяти, даже если больше нигде в коде нет ссылок на него.
И напротив, в WeakSet и WeakMap хранятся слабые ссылки. Это означает, что если объект, на который есть ссылка в этих структурах, больше недоступен в коде (т.е. нигде больше нет сильных ссылок на него), то сборщик мусора может удалить этот объект из памяти, даже если в WeakSet или WeakMap все еще есть ссылка на него. Таким образом, использование слабых ссылок позволяет не держать в памяти ненужные больше объекты и экономить память.
Кроме того, в WeakMap в качестве ключей могут использоваться только объекты, а не примитивные значения. А в WeakSet хранятся только объекты, без ключей.
38. Почему два объекта с одинаковыми полями возвращают false при сравнении?
В JavaScript объекты сравниваются по ссылкам на область памяти, где они хранятся. Два разных объекта, даже если у них одинаковые поля и значения этих полей, располагаются в разных областях памяти. Например, у нас есть:
const example1 = {fruit: 'яблоко'}; const example2 = {fruit: 'яблоко'}; console.log(example1 == example2); // false
Хотя у этих двух объектов одно и то же поле fruit
со значением 'яблоко'
, на самом деле это два абсолютно разных объекта, которые хранятся в разных ячейках памяти. Поэтому если мы сравним их, результат будет false
. Для того чтобы два объекта считались равными, нужно, чтобы это был один и тот же объект, то есть чтобы обе переменные ссылались на одну и ту же область памяти.
39. Как в JavaScript реализованы методы примитивных типов данных?
JavaScript позволяет работать с примитивными типами данных – строками, числами, логическими значениями – как с объектами, поскольку у них тоже есть методы. Например, у строк есть методы toUpperCase() и toLowerCase(), у чисел есть методы toFixed() и toPrecision() и т.д.
Эта возможность реализована благодаря специальным оберточным объектам для каждого примитивного типа данных. Эти объекты называются:
- String – для строк
- Number – для чисел
- Boolean – для логических значений
- Symbol – для символов
Когда мы вызываем метод у примитивного значения, например "test".toUpperCase()
, происходит следующее:
- Создается временный оберточный объект типа
String
со значением"test"
. - У этого объекта вызывается метод
toUpperCase()
. - Результат возвращается обратно в примитивное значение.
- Временный оберточный объект удаляется.
Таким образом реализуется возможность использовать методы у примитивных типов данных. Благодаря этому механизму, примитивы в JavaScript ведут себя как объекты.
40. Как проверить, из какого класса был создан объект?
Для этого в JavaScript используется оператор instanceof. Он позволяет проверить, из какого класса был создан объект, учитывая наследование.
Например, есть базовый класс Animal и классы-наследники Dog и Cat:
class Animal { constructor() {} } class Dog extends Animal { constructor() { super(); } bark() {} } class Cat extends Animal { constructor() { super(); } meow() {} }
Создадим объект класса Dog и проверим с помощью оператора instanceof
, является ли объект экземпляром указанного класса или классов-родителей:
const dog = new Dog(); console.log(dog instanceof Dog); // true console.log(dog instanceof Animal); // true console.log(dog instanceof Cat); // false