December 15, 2024

JavaScript: senior

JavaScript — это основной язык программирования для веб-разработки, позволяющий создавать интерактивные и высокопроизводительные приложения. В этой статье мы рассмотрим ключевые концепции и методы оптимизации, такие как tree shaking, Server-Side Rendering (SSR), виртуальный скроллинг, добавление элементов в DOM, а также управление асинхронными операциями. Эти знания помогут улучшить производительность приложений, оптимизировать работу с API и обеспечить плавную отрисовку контента в Single Page Applications (SPA).

👉🏻Навигация по всем материалам в Telegram

Вопрос 1

Какой из массивов НЕ будет выведен после выполнения этого кода?

Варианты ответа:

  1. ["Мандарин", "Крыжовник", "Агрус"]
  2. ["Мандарин", "Клубника", "Смородина"]
  3. ["Мандарин", "Апельсин", "Слива"]
  4. ["Яблоко", "Апельсин", "Слива"]
  5. Будут выведены все массивы.

Обоснование:

  1. Код создает две переменные fruits и fruits1. Обе изменяются в различных контекстах (глобальном и локальном).
  2. В глобальной области fruits меняется на ["Мандарин", "Апельсин", "Слива"].
  3. Функция test() локально переопределяет обе переменные:
    • const fruits1 становится ["Малина", "Клубника", "Смородина"], но не влияет на глобальный fruits1.
    • let fruits становится ["Груша", "Крыжовник", "Агрус"] и изменяется на ["Мандарин", "Крыжовник", "Агрус"].
  4. После выполнения функции test():
    • console.log(fruits) выведет глобальное значение: ["Мандарин", "Апельсин", "Слива"].
    • console.log(fruits1) выведет глобальное значение: ["Яблоко", "Апельсин", "Слива"].

📌Правильный ответ:
2. ["Мандарин", "Клубника", "Смородина"]
Этот массив существует только в локальном контексте функции test() и нигде не выводится.


Вопрос 2

Перед вами поставили задачу написать веб-приложение «Крестики-нолики». По техническому заданию вам необходимо сохранять состояние игры. Например, вы можете сохранить текущего игрока, расположение крестиков и ноликов на игровом поле и другие данные, чтобы игра могла быть возобновлена после перезагрузки страницы. Какой тип хранилища вы подберете для данной задачи?

Варианты ответа:

  1. Куки (cookies)
  2. LocalStorage
  3. Cache API
  4. Web SQL Database
  5. sessionStorage

Обоснование:

  1. Куки (cookies):
    • Подходят для передачи данных между сервером и клиентом.
    • Однако их использование для сохранения состояния игры неэффективно, так как они имеют ограничение по объему (4 КБ) и требуют отправки данных при каждом HTTP-запросе.
  2. LocalStorage:
    • LocalStorage хранит данные на стороне клиента, доступно для одной и той же страницы, даже после перезагрузки.
    • Объем данных (около 5 МБ) подходит для хранения состояния игры.
    • Поддерживает быстрое сохранение и извлечение данных, что делает его идеальным для задач, подобных этой.
  3. Cache API:
    • Используется для кеширования ресурсов (например, HTML, CSS, JS) и редко применяется для хранения структурированных данных.
    • Не подходит для задачи сохранения текущего состояния игры.
  4. Web SQL Database:
    • Устаревшая технология, уже не поддерживается в большинстве браузеров, таких как Firefox.
  5. sessionStorage:
    • Хранит данные только в течение текущей сессии (до закрытия вкладки браузера).
    • Не подходит для задачи, так как данные должны быть доступны после перезагрузки страницы.

📌Правильный ответ:
2. LocalStorage

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


Вопрос 3

Каким будет результат выполнения следующего выражения в JavaScript?

Варианты ответа:

  1. 50
  2. 1010
  3. 10025
  4. 20
  5. 100

Обоснование:

  1. Операции в JavaScript выполняются слева направо, но порядок вычислений определяется приоритетом операторов.
  2. Умножение (*) имеет более высокий приоритет, чем сложение (+).
    • "5" (строка) будет приведена к числу, поэтому 2 * "5" дает числовое значение 10.
  3. Затем происходит операция сложения:
    • Оператор + используется для строк и чисел. Если одна из операндов — строка, то результатом будет конкатенация строк.
    • В данном случае "10" — строка, а 10 — число. Число будет преобразовано в строку. Итог: "10" + "10" = "1010".

📌Правильный ответ: 2. 1010


Вопрос 4

Из массива чисел вам нужно сначала отфильтровать четные числа, затем удвоить их и, наконец, вычислить сумму. Какой код это выполняет?

Варианты ответа:

  1. numbers.map(n => n * 2).filter(n => n % 2 === 0).reduce((sum, n) => sum + n)
  2. numbers.reduce((sum, n) => sum + n * 2, 0).filter(n => n % 2 === 0)
  3. numbers.reduce((sum, n) => sum + n).map(n => n * 2).filter(n => n % 2 === 0)
  4. numbers.filter(n => n % 2).map(n => n + 2).reduce((sum, n) => sum + n)
  5. numbers.filter(n => n % 2 === 0).map(n => n * 2).reduce((sum, n) => sum + n, 0)

Обоснование:

  1. Задача разбивается на три этапа:
    • Фильтрация четных чисел: Используем метод .filter(n => n % 2 === 0), который оставляет только числа, делящиеся на 2 без остатка.
    • Удвоение: Используем метод .map(n => n * 2), который умножает каждое число на 2.
    • Суммирование: Используем метод .reduce((sum, n) => sum + n, 0), который суммирует элементы массива, начиная с начального значения 0.
  2. Рассмотрим варианты:
    • Вариант 1: Сначала удваивает числа, а потом фильтрует их на четность. Это неверно, так как фильтрация должна быть первым шагом.
    • Вариант 2: Сначала пытается применить reduce, а затем фильтровать — это неверный порядок действий, так как reduce уже преобразует массив в одно значение.
    • Вариант 3: Сначала суммирует, а затем пытается применить map и filter — это неверно, так как после reduce результат уже не является массивом.
    • Вариант 4: Фильтрует нечетные числа (n % 2), а не четные. Это неправильно, так как требуется отфильтровать четные числа.
    • Вариант 5: Правильный порядок действий: сначала фильтрует четные числа, затем удваивает их и, наконец, суммирует.

📌Правильный ответ:
5. numbers.filter(n => n % 2 === 0).map(n => n * 2).reduce((sum, n) => sum + n, 0)


Вопрос 5

Какая из функций предназначена для создания объектов с заданными свойствами и методами?

Варианты ответа:

  1. Генераторная функция (generator function)
  2. Рекурсивная функция (recursive function)
  3. Функция обратного вызова (callback)
  4. Функция-конструктор (constructor function)
  5. Стрелочная функция (arrow function)

Обоснование:

  1. Генераторная функция (generator function):
    • Предназначена для создания итераторов. Используется для управления последовательностями значений, но не подходит для создания объектов с заданными свойствами и методами.
  2. Рекурсивная функция (recursive function):
    • Самовызванная функция, которая используется для решения задач, требующих многократного вызова себя с новыми параметрами. Она не связана с созданием объектов.
  3. Функция обратного вызова (callback):
    • Используется в качестве параметра для других функций. Обычно применяется для обработки событий или асинхронных операций. Не используется для создания объектов.
  4. Функция-конструктор (constructor function):
    • Специальная функция, используемая для создания объектов. Вызывается с помощью оператора new и позволяет задавать свойства и методы объекта. Это как раз то, что требуется в вопросе.
  5. Стрелочная функция (arrow function):
    • Лаконичный синтаксис для создания функций. Однако стрелочные функции не имеют собственного контекста this, поэтому их нельзя использовать как конструкторы для создания объектов.

📌Правильный ответ:
4. Функция-конструктор (constructor function)
Она предназначена для создания объектов с заданными свойствами и методами.


Вопрос 6

Что будет выведено в консоль при выполнении данного кода?

Варианты ответа:

  1. 0, 1, 2
  2. 1, 2, 3
  3. 2, 3, 4
  4. 1, 1, 1
  5. 0, 0, 0

Обоснование:

  1. Функция counter() возвращает замыкание — функцию, которая имеет доступ к переменной count, объявленной внутри counter().
  2. При первом вызове counter() создается новое окружение, в котором count инициализируется значением 0.
  3. Вызов increment() выполняет тело внутренней функции, увеличивая count на 1 (постфиксный инкремент ++count возвращает увеличенное значение) и возвращает результат.
  4. Переменная increment хранит ссылку на эту внутреннюю функцию, поэтому каждый вызов increment() продолжает работать с тем же значением count, увеличивая его.

Разберем вызовы:

  • Первый вызов increment() увеличивает count с 0 до 1 и возвращает 1.
  • Второй вызов increment() увеличивает count с 1 до 2 и возвращает 2.
  • Третий вызов increment() увеличивает count с 2 до 3 и возвращает 3.

📌Правильный ответ: 2. 1, 2, 3


Вопрос 7

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

Варианты ответа:

  1. Начало, Конец, Данные получены: [данные], SetTimeout 1, SetTimeout 2.
  2. Конец, Начало, SetTimeout 1, SetTimeout 2, Данные получены: [данные].
  3. Начало, Конец, SetTimeout 1, Данные получены: [данные], SetTimeout 2.
  4. Начало, Выполнение SetTimeout 1, Выполнение SetTimeout 2, Конец, Данные получены: [данные].
  5. Начало, Конец, SetTimeout 1, SetTimeout 2, Данные получены: [данные].

Обоснование:

  1. Синхронный код выполняется первым:
    • console.log("Начало"); и console.log("Конец"); выполняются сразу, так как это синхронные операции.
  2. Асинхронные операции помещаются в очередь событий (Event Loop):
    • setTimeout с задержкой 0 не выполняется мгновенно, а добавляет колбэк в очередь задач.
    • fetch выполняется в фоне, и его результат обрабатывается в колбэке .then, который также попадает в очередь задач.
  3. Очередность выполнения асинхронного кода:
    • Сначала выполняются все синхронные операции.
    • Затем выполняются задачи из очереди (например, таймеры setTimeout и обработчики fetch).
    • Колбэк setTimeout имеет приоритет над fetch, так как обработка fetch добавляется в очередь позже.

Порядок выполнения:

  1. "Начало" (синхронный код).
  2. "Конец" (синхронный код).
  3. "SetTimeout 1" (асинхронный колбэк таймера).
  4. "SetTimeout 2" (асинхронный колбэк таймера).
  5. "Данные получены: [данные]" (обработка ответа fetch).

📌Правильный ответ: 5. Начало, Конец, SetTimeout 1, SetTimeout 2, Данные получены: [данные].


Вопрос 8

Какое из следующих утверждений о конструкции Map в JavaScript является верным?

Варианты ответа:

  1. Map гарантирует, что элементы будут храниться в порядке добавления.
  2. Ключи в Map могут быть только строками.
  3. Map автоматически сортирует свои элементы по возрастанию ключей.
  4. Метод delete() в Map возвращает удаляемый элемент из коллекции.
  5. Map не позволяет использовать объекты в качестве ключей.

Обоснование:

  1. Map гарантирует, что элементы будут храниться в порядке добавления:
    • Верно. В Map элементы сохраняют порядок, в котором они были добавлены, и их порядок не меняется.
  2. Ключи в Map могут быть только строками:
    • Неверно. В отличие от объектов, в Map ключи могут быть любого типа, включая объекты, функции и числа.
  3. Map автоматически сортирует свои элементы по возрастанию ключей:
    • Неверно. Map не сортирует элементы, порядок сохраняется в соответствии с добавлением.
  4. Метод delete() в Map возвращает удаляемый элемент из коллекции:
    • Неверно. Метод delete() возвращает булевое значение (true или false), указывающее, был ли элемент удален.
  5. Map не позволяет использовать объекты в качестве ключей:
    • Неверно. Map специально разработан для использования объектов (и других типов) в качестве ключей.

📌Правильный ответ: 1. Map гарантирует, что элементы будут храниться в порядке добавления.


Вопрос 9

Вам нужно создать неглубокую копию объекта и массива в JavaScript, при этом важно сохранить методы, определенные в самом объекте (без учета прототипной цепочки). Какой из следующих методов или подходов вы выберете?

Варианты ответа:

  1. const newObj = new Object(obj), newArr = new Array(arr)
  2. const newObj = { ...obj }, newArr = [...arr]
  3. const newObj = obj, newArr = arr
  4. const newObj = Object.assign(obj), newArr = Array.copy(arr)
  5. const newObj = JSON.parse(JSON.stringify(obj)), newArr = JSON.parse(JSON.stringify(arr))

Обоснование:

  1. Вариант 1:
    • new Object(obj) и new Array(arr) создают объект и массив, но не копируют существующий объект или массив. Этот вариант не подходит.
  2. Вариант 2:
    • Использование операторов распространения { ...obj } и [...arr] создает неглубокие копии объекта и массива. При этом сохраняются методы, определенные непосредственно в объекте, что делает этот вариант подходящим.
  3. Вариант 3:
    • Присваивание const newObj = obj и const newArr = arr создает ссылку на оригинальные объекты. Это не копия, а ссылка, поэтому изменения в одном объекте повлияют на другой.
  4. Вариант 4:
    • Object.assign(obj) корректно создает неглубокую копию объекта, но Array.copy(arr) не является существующим методом в JavaScript, что делает вариант неверным.
  5. Вариант 5:
    • Использование JSON.parse(JSON.stringify(obj)) создает глубокую копию объекта и массива, но теряет методы, определенные в объекте. Этот вариант не подходит для сохранения методов.

📌Правильный ответ: 2. const newObj = { ...obj }, newArr = [...arr]


Вопрос 10

У вас есть следующий код. Что будет выведено в консоль при его вызове?

Варианты ответа:

  1. В консоли будет выведено «Rex makes a noise.» и затем «Rex barks.»
  2. В консоли будет выведено «Dog makes a noise.» и затем «Dog barks.»
  3. В консоли будет выведено только «Rex barks.»
  4. Произойдет ошибка из-за вызова super.speak()
  5. В консоли ничего не будет выведено, так как метод speak() переопределен в классе Dog.

Обоснование:

  1. Класс Animal:
    • Определяет конструктор, который принимает имя и сохраняет его в свойстве this.name.
    • Метод speak() выводит сообщение ${this.name} makes a noise.
  2. Класс Dog:
    • Наследуется от Animal с использованием ключевого слова extends.
    • Переопределяет метод speak(), но внутри вызывает родительский метод с помощью super.speak().
    • После вызова super.speak() добавляет свою собственную строку в консоль.
  3. Создание объекта dog:
    • При вызове new Dog("Rex") используется конструктор родительского класса Animal. Свойство name объекта dog становится равным "Rex".
  4. Вызов dog.speak():
    • Сначала выполняется super.speak(), что вызывает метод speak() родительского класса, выводящий Rex makes a noise.
    • Затем выполняется вторая строка метода speak() из класса Dog, которая выводит Rex barks.

📌Правильный ответ: 1. В консоли будет выведено «Rex makes a noise.» и затем «Rex barks.»


Вопрос 11

Вы разрабатываете интерфейс для интернет-магазина, который должен содержать витрину для тысячи товаров. Какой подход стоит использовать, чтобы обеспечить плавное и быстрое отображение этих товаров при прокрутке и избежать задержек и «зависаний» интерфейса?

Варианты ответа:

  1. Применение техники «Debounce» для событий прокрутки.
  2. Реализовать виртуальный скроллинг для отображения товаров в текущем viewport (видимой области) браузера.
  3. Использовать кэширование данных на стороне клиента.
  4. Провести оптимизацию запросов к серверу через WebSockets для непрерывной загрузки данных о товарах.
  5. Применить «ленивую загрузку» для изображений товаров.

Обоснование:

  1. Применение техники «Debounce» для событий прокрутки:
    • Техника «Debounce» помогает оптимизировать частоту вызовов функции при частом срабатывании событий (например, при прокрутке).
    • Это полезно, но само по себе не решает проблему плавного отображения большого количества данных.
  2. Реализовать виртуальный скроллинг:
    • Виртуальный скроллинг отображает только те элементы, которые находятся в текущей видимой области экрана (viewport).
    • Остальные элементы не рендерятся до тех пор, пока не станут видимыми.
    • Это эффективный способ обработки тысяч элементов без задержек в интерфейсе.
  3. Использовать кэширование данных на стороне клиента:
    • Кэширование ускоряет доступ к ранее загруженным данным, но не решает проблемы обработки большого количества элементов на экране.
  4. Оптимизация запросов через WebSockets:
    • WebSockets обеспечивает непрерывный обмен данными между клиентом и сервером.
    • Это полезно для обновлений данных, но не связано с рендерингом большого количества элементов.
  5. Применение «ленивой загрузки» для изображений:
    • «Ленивая загрузка» улучшает производительность, загружая изображения только тогда, когда они появляются в viewport.
    • Однако она решает только часть проблемы, связанную с загрузкой ресурсов, а не с рендерингом интерфейса.

📌Правильный ответ: 2. Реализовать виртуальный скроллинг для отображения товаров в текущем viewport (видимой области) браузера.


Вопрос 12

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

Варианты ответа:

  1. 1, 4, 6, 2, 3, 7, 5
  2. 1, 4, 6, 7, 2, 5, 3
  3. 1, 6, 4, 7, 2, 3, 5
  4. 1, 6, 4, 2, 7, 5, 3
  5. 1, 2, 4, 6, 5, 7, 3

Обоснование:
Чтобы определить порядок выполнения, нужно учитывать последовательность обработки синхронного кода, микрозадач (Promise), макрозадач (setTimeout), и других асинхронных механизмов (например, requestAnimationFrame).

  1. Синхронный код:
    • console.log('1') выполняется первым.
    • console.log('6') выполняется после всех остальных синхронных операций.
  2. Микрозадачи (Promise):
    • Promise.resolve().then() добавляет задачи в очередь микрозадач.
    • Микрозадачи выполняются после завершения текущего синхронного кода, перед макрозадачами.
    • Следовательно, console.log('4') будет выполнен до вызовов setTimeout или requestAnimationFrame.
  3. Макрозадачи:
    • setTimeout добавляет колбэки в очередь макрозадач.
    • requestAnimationFrame выполняется после завершения синхронного кода, но раньше, чем макрозадачи.
  4. Обработка колбэков setTimeout и вложенного Promise:
    • console.log('2') выполняется первым среди задач setTimeout.
    • Затем Promise.resolve().then() внутри этого таймера добавляет console.log('3') в очередь микрозадач, которая выполняется сразу после текущей макрозадачи.
    • Аналогично, console.log('5') из второго setTimeout выполнится на следующей итерации очереди макрозадач.

Порядок выполнения:

  1. 1 (синхронный код).
  2. 6 (синхронный код).
  3. 4 (микрозадача).
  4. 7 (requestAnimationFrame).
  5. 2 (первая макрозадача).
  6. 3 (микрозадача из первого setTimeout).
  7. 5 (вторая макрозадача).

📌Правильный ответ: 3. 1, 6, 4, 7, 2, 3, 5


Вопрос 13

Для чего применяется tree shaking в системах сборки JavaScript на прикладном уровне?

Варианты ответа:

  1. Для асинхронного выполнения функций с использованием деревьев вызовов (call trees).
  2. Для автоматического определения и удаления неиспользуемого кода во время сборки.
  3. Для шифрования и обфускации JavaScript-кода с целью защиты интеллектуальной собственности.
  4. Для создания иерархии древовидной структуры данных в JavaScript-приложениях.
  5. Для отображения и визуализации иерархической структуры DOM-элементов веб-страницы.

Обоснование:

  1. Tree shaking — это техника оптимизации, используемая в системах сборки (например, Webpack, Rollup) для устранения мертвого кода (dead code elimination). Она основывается на анализе импортов/экспорта модулей и удаляет код, который не используется в итоговом бандле.
  2. Термин "tree shaking" происходит от метафоры: представьте дерево кода, с которого "стряхиваются" ненужные ветви. Это позволяет уменьшить размер бандла и повысить производительность приложения.

Анализ вариантов:

  1. Асинхронное выполнение функций с использованием деревьев вызовов:
    • Неверно. Tree shaking не связан с выполнением кода, он работает только на этапе сборки.
  2. Автоматическое определение и удаление неиспользуемого кода во время сборки:
    • Верно. Это и есть основная цель tree shaking.
  3. Шифрование и обфускация JavaScript-кода:
    • Неверно. Tree shaking не занимается защитой кода, а фокусируется на его оптимизации.
  4. Создание иерархии древовидной структуры данных:
    • Неверно. Tree shaking не связан с построением структур данных.
  5. Отображение и визуализация иерархической структуры DOM-элементов:
    • Неверно. Tree shaking не имеет отношения к DOM или визуализации.

📌Правильный ответ: 2. Для автоматического определения и удаления неиспользуемого кода во время сборки.


Вопрос 14

Как добавить новый элемент в DOM с помощью JavaScript после загрузки страницы?

Варианты ответа:

  1. Используя комбинацию методов document.createElm() и document.insert().
  2. Используя метод document.write().
  3. Используя метод document.createAndInsertElement().
  4. Используя комбинацию методов document.create() и document.append().
  5. Используя комбинацию методов document.createElement() и document.appendChild().

Обоснование:

  1. document.createElm() и document.insert():
    • Неверно. Эти методы не существуют в JavaScript.
  2. document.write():
    • Используется для записи содержимого в документ, но только в момент загрузки страницы. После загрузки страницы использование этого метода перезапишет существующий документ.
  3. document.createAndInsertElement():
    • Неверно. Такого метода нет в JavaScript.
  4. document.create() и document.append():
    • Неверно. document.create() не существует, но document.createElement() — правильный метод для создания элемента.
  5. document.createElement() и document.appendChild():

Верно.

    • document.createElement() создаёт новый элемент DOM.
    • document.appendChild() добавляет созданный элемент в дерево DOM как дочерний узел указанного родительского элемента.

📌Правильный ответ: 5. Используя комбинацию методов document.createElement() и document.appendChild().


Вопрос 15

Вы работаете над SPA (Single Page Application), которое активно обращается к API для обработки действий пользователя и вывода изменяющегося контента. Ваша задача — увеличить производительность приложения и сократить время полной первой отрисовки страницы. У вас в наличии мощный сервер, вы не ограничены пропускной способностью сетевого оборудования. Какой метод вы используете для достижения этой цели?

Варианты ответа:

  1. Web Workers
  2. Кэширование ответов от API в localStorage
  3. Техника Lazy Loading
  4. Техники Throttling и Debouncing
  5. Server-Side Rendering (SSR)

Обоснование:

  1. Web Workers:
    • Используются для выполнения фоновых операций без блокировки основного потока.
    • Это полезно для вычислительных задач, но не влияет на первую отрисовку страницы.
  2. Кэширование ответов от API в localStorage:
    • Ускоряет повторные запросы, но не сокращает время полной первой отрисовки страницы, так как данные сначала должны быть загружены.
  3. Техника Lazy Loading:
    • Уменьшает нагрузку за счёт отложенной загрузки ресурсов (например, изображений), но не оптимизирует время полной первой отрисовки.
  4. Техники Throttling и Debouncing:
    • Ограничивают частоту выполнения функций (например, обработчиков событий), но не решают задачу, связанную с полной первой отрисовкой.
  5. Server-Side Rendering (SSR):
    • Позволяет рендерить HTML на сервере перед его отправкой клиенту, что значительно сокращает время первой отрисовки страницы.
    • Подходит для мощных серверов и решает задачу повышения производительности SPA.

📌Правильный ответ: 5. Server-Side Rendering (SSR)

Заключение

Эффективное использование JavaScript требует понимания как базовых механизмов, так и современных практик оптимизации. Методы, такие как SSR для быстрой первой отрисовки, виртуальный скроллинг для работы с большими данными, tree shaking для удаления мертвого кода, а также грамотная работа с DOM, позволяют разработчикам создавать производительные и удобные веб-приложения. Знание этих техник поможет вам решать реальные задачи и достигать лучших результатов при разработке веб-интерфейсов.