JavaScript

100 questions

QUESTION: What is the difference between var, let, and const?

In modern JavaScript, there are three key ways to declare variables: var, let, and const. Understanding the differences between them is critical for writing predictable and high-quality code. Historically, variables declared with var appeared first. Their main feature is functional scope, which means the variable is accessible throughout the entire function where it was declared, regardless of code blocks. In addition, var is subject to hoisting and allows re-declaring the same variable within the same scope without throwing errors, which often leads to hard-to-find bugs.

With the advent of the ES6 standard, developers' arsenals were expanded with the let and const keywords, which introduce block scope. Variables declared with let are limited to the current code block enclosed in curly brackets and cannot be re-declared in the same scope, although their value can be changed during program execution. The const keyword works similarly to let by establishing block scope, but it imposes a strict limitation on reassigning the variable. It is important to note that if const is used with objects or arrays, the contents of the structure itself can be mutated by changing properties or elements, but assigning a reference to a new object is no longer possible. In modern development practice, it is recommended to use const by default for all immutable references, and to use let exclusively when the value of the variable is genuinely intended to be overwritten.

QUESTION: What are the data types in JavaScript?

The type system in JavaScript is divided into two main categories: primitive data types and reference types, or objects. Primitives include seven types: string for working with text strings, number for integers and floating-point numbers, boolean for true or false logical values, null to indicate the intentional absence of a value, undefined for unassigned or uninitialized variables, symbol for creating unique identifiers, and bigint for working with arbitrary-precision integers. All primitives are immutable and passed by value. The second category is represented by complex data structures, which include regular objects (object), arrays (array), and functions (function), which are passed by reference and can be modified during program execution.

To determine the data type in code, the special typeof operator is used, which returns a string with the name of the type. However, there are known nuances and historical quirks associated with this operator. For example, checking the type of null using typeof returns the string object, which is a well-known historical bug in the first implementation of JavaScript that cannot be fixed for backward compatibility reasons. Another interesting example is the special numeric value NaN, which stands for Not-a-Number, but actually belongs to the number type and results from incorrect mathematical operations, such as dividing zero by zero. Knowing these features helps avoid common logic errors when developing applications.

QUESTION: What is hoisting?

The hoisting mechanism represents the behavior of the JavaScript interpreter where variable and function declarations are moved to the top of their scope before code execution begins. It is important to understand that lines of code do not physically move in the file, but memory for them is allocated in advance during the compilation stage. At the same time, the method of hoisting depends directly on the keyword used for declaration. Variables created with var are hoisted to the top of the functional scope and automatically initialized with the undefined value, which is why they can be accessed before the declaration line, resulting in an undefined outcome rather than an error.

With the introduction of let and const, this behavior changed to increase code reliability. Declarations via let and const are also hoisted, but they do not receive an initial value and are placed in the so-called temporal dead zone. Attempting to access such a variable before the line of its physical declaration in the code will result in a ReferenceError being thrown. As for functions, declarations of classical functions via function declaration are hoisted completely, including their body, which allows calling a function before it was written in the text of the script. At the same time, it is important to remember that only the declarations themselves are hoisted, not the operations of assigning values to variables.

QUESTION: How does == vs === work?

Comparison operators in JavaScript play a key role in branching and decision-making logic, and the choice between the loose equality operator and the strict equality operator significantly affects the program's execution result. The double equality operator performs a comparison with preliminary type coercion if the operands belong to different categories. This means the interpreter tries to convert both values to a common data type before matching them. For example, the expression '5' == 5 will return the boolean value true because the string value is converted to a numeric one. This behavior often leads to unexpected results and hard-to-detect bugs in the code due to implicit conversions.

In contrast, the triple equality operator performs a strict comparison without any type coercion. If the compared values belong to different data types, the operator immediately returns false, even if their string representations match. For example, the expression '5' === 5 will return false because the left operand is a string and the right one is a number. In the professional developer community, a golden rule has been established: always use strict equality to avoid logic errors and ambiguities. An exception might only be checking for null and undefined simultaneously via comparison with null using the double equality operator, although in modern projects explicit checks or the nullish coalescing operator are more commonly applied.

QUESTION: What are truthy and falsy values?

In JavaScript, all values in the context of logical operations are divided into two categories: truthy, which evaluate to true, and falsy, which evaluate to false. Understanding this division is critical for the correct operation of conditional constructs like the if statement, the ternary operator, or the logical AND and OR operators. There is a strictly limited list of falsy values, which includes the boolean false, the number zero 0, an empty string, the special values null and undefined, and the numeric value NaN. Any other value existing in the language is automatically considered truthy and behaves like true in logical conditions.

Special attention should be paid to complex data types such as arrays and objects. Beginner developers often mistakenly assume that an empty array or an empty object should be equivalent to falsy, but in JavaScript, any arrays and objects, even completely empty ones, are truthy values. This means that the check if ([]) will always execute successfully, which often becomes a source of unexpected code behavior. To explicitly convert any value to a boolean type in code, developers use the Boolean(value) constructor or double negation, which allows precise control over application logic and avoids hidden errors when evaluating conditions.

QUESTION: Как работают логические операторы?

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

Логическое И обозначается как двойной амперсанд и работает по принципу поиска первого ложного значения. Если все операнды истинны, он возвращает самое последнее значение. В программировании это часто используется для условного выполнения кода, например, для проверки существования объекта перед вызовом его метода.
Логическое ИЛИ обозначается как два вертикальных слэша и действует противоположным образом, возвращая первое истинное значение или самое последнее, если все они ложны. Это стандартный способ задания значений по умолчанию в старом коде, когда нужно подставить резервную переменную при отсутствии основных данных.
Оператор нулевого слияния обозначается двумя знаками вопроса и был введен для более точной проверки на наличие данных. В отличие от ИЛИ, которое считает ложными пустые строки или нули, этот оператор проверяет значение строго на null или undefined, что идеально подходит для опциональных параметров конфигурации.
Логическое НЕ инвертирует булевое значение операнда, превращая истину в ложь и наоборот. Двойное отрицание иногда используется для принудительного приведения любого типа данных к булевому значению.
Ленивое вычисление означает, что выполнение цепочки логических операторов останавливается ровно в тот момент, когда результат становится очевидным. Например, первое ложное значение в операторе И сразу прекращает дальнейшую проверку правой части, что экономит ресурсы процессора и защищает от ошибок при обращении к несуществующим свойствам.

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

QUESTION: Как работают логические операторы? ===TRANSLATED_QUESTION=== How do logical operators work?

Logical operators in the JavaScript programming language are fundamental tools for controlling code execution flow, checking conditions, and working with various data types. Understanding their behavior is critically important for writing clean and efficient code in any web applications.

Logical AND is denoted by a double ampersand and works on the principle of finding the first falsy value. If all operands are truthy, it returns the very last value. In programming, this is often used for conditional code execution, such as checking for the existence of an object before calling its method.
Logical OR is denoted by two vertical slashes and acts in the opposite manner, returning the first truthy value or the very last one if all of them are falsy. This is a standard way of setting default values in legacy code when you need to substitute a fallback variable in the absence of primary data.
The nullish coalescing operator is denoted by two question marks and was introduced for more precise data checking. Unlike OR, which considers empty strings or zeros as falsy, this operator checks a value strictly for null or undefined, making it ideal for optional configuration parameters.
Logical NOT inverts the boolean value of an operand, turning true into false and vice versa. Double negation is sometimes used to forcefully coerce any data type into a boolean value.
Short-circuit evaluation means that the execution of a chain of logical operators stops right at the moment when the result becomes obvious. For example, the first falsy value in an AND operator immediately stops further checking of the right-hand side, which saves CPU resources and protects against errors when accessing non-existent properties.

In practice, developers often combine these operators to create elegant checks in real interfaces. One should be cautious with code readability, avoiding overly complex logical chains on a single line so that other programmers can easily maintain the written script.

[ITEM 2] QUESTION: Что такое template literals?

Шаблонные строки или template literals представляют собой мощный синтаксический инструмент в JavaScript, который значительно упрощает работу со строковыми данными по сравнению с традиционными одинарными или двойными кавычками. Этот механизм встроен в современный стандарт языка и активно применяется во фронтенд- и бэкенд-фреймворках.

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

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

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

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

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

На практике применение шаблонных строк делает исходный код лаконичным и менее подверженным синтаксическим ошибкам. Они незаменимы при создании динамических HTML-шаблонов внутри клиентских приложений или при формировании сложных строк логирования для отладки программного обеспечения.

[ITEM 3] QUESTION: Как объявить функцию?

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

Объявление функции представляет собой классический синтаксис, где ключевое слово сопровождается уникальным именем и телом функции, заключенным в фигурные скобки. Такие функции проходят этап всплытия, что позволяет вызывать их в коде до строки физического объявления.
Функциональное выражение предполагает, что анонимная или именная функция присваивается переменной. Это создает значение, с которым можно работать как с обычными данными, но оно не всплывает подобно классическим объявлениям.
Стрелочные функции появились в современном стандарте ECMAScript и отличаются лаконичным синтаксисом. Они очень удобны для использования в качестве коротких колбэков в методах массивов и не создают собственный контекст вызова.
Немедленно вызываемые функциональные выражения создаются и запускаются одновременно в момент интерпретации скрипта. Исторически это применялось для изоляции переменных и создания приватного пространства имен до появления современных модульных систем.

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

[ITEM 4] QUESTION: Как работают стрелочные функции?

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

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

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

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

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

На практике стрелочные функции незаменимы при передаче небольших колбэков в методы вроде map, filter и reduce. Однако их не стоит применять в качестве методов объектов, если требуется динамическая привязка контекста к вызывающему объекту через ключевое слово this.

QUESTION: Что такое замыкание (closure)?

A closure in JavaScript is a fundamental concept that is closely related to how the language handles variable scope. The main essence of a closure is that an inner function always remembers its lexical environment. This means that the variables that were located at the place of its creation remain accessible to it, even after the outer function has completely finished its execution.

In practice, closures are actively used to create private variables that cannot be modified directly from the global scope. This approach significantly increases the security and reliability of the developed code. For example, you can write a counter function that encapsulates internal state and provides special methods to read and modify it, completely hiding the variable itself from external interference.

Another popular use case is function factories. These are functions that generate other functions with pre-configured behavior, such as functions for logging messages with a specific prefix. Closures also form the basis of currying — the process of transforming a function with multiple arguments into a sequence of nested functions that take arguments one by one.

Understanding closures is critical for writing efficient JavaScript code. However, careless use of them can lead to unwanted consequences. In particular, if references to large external data structures are kept longer than necessary, memory leaks can occur.

QUESTION: Как работает this?

The keyword this in JavaScript determines the context in which the current code is executing. Its value is dynamically evaluated at the moment the function itself is called, rather than at the time it is declared. The behavior of this depends on a few clear rules that determine how a function is invoked.

If a function is called as an object method, then this points to that object itself. This allows methods to access the properties of their owner. In a regular function called without any context, this refers to the global window object in a browser or to undefined in strict mode, which is often a source of bugs for novice developers.

Arrow functions behave completely differently. They do not have their own this, and they inherit its value from the outer lexical context at the time of their creation. This makes them ideal for use inside object methods or callbacks where you need to reliably preserve the context of the outer function.

When creating an object using the new operator via a constructor function or class, this inside that function refers to the newly created object. Finally, special methods allow you to control the context manually. By analyzing where a function is called, you can easily determine what exactly this points to in each specific case.

QUESTION: Что такое call, apply, bind?

The call, apply, and bind methods in JavaScript are designed to explicitly control the execution context of a function, that is, to bind a specific this value. These are powerful tools that allow you to reuse code and precisely control the execution environment.

Here are the main ways they are used:

The call method invokes a function immediately, allowing you to pass the context as the first argument, followed by the rest of the function arguments separated by commas, for example, fn.call(obj, a, b).
The apply method works in a similar way, invoking the function immediately, but the arguments are passed as an array, which is convenient when their number is not known in advance or they are already collected into a collection, for example, fn.apply(obj, [a, b]).
The bind method does not invoke the function immediately, but instead returns a new function with a hard-bound context and passed arguments, which can be executed later.

The bind method is especially useful when passing methods as callbacks into asynchronous code or event handlers, where the context is often lost. Another important application of these methods is method borrowing from other objects, such as using array methods to work with array-like objects like arguments or NodeList. Understanding the differences between call, apply, and bind allows you to write more flexible, modular, and predictable code.

QUESTION: Как работать с массивами?

Working with arrays in JavaScript is an everyday task for any developer, as arrays serve as the primary tool for storing ordered collections of data. There are many ways to create arrays and manage their contents depending on the current project tasks.

You can create an array using the square bracket literal [], the new Array() constructor, or the static Array.from() method, which converts iterable objects or array-like objects into a full-fledged array. Various built-in methods are used to modify the structure and contents of arrays.

The following approaches are used to modify elements:

The push and pop methods add and remove elements from the end of the array respectively, changing its length.
The shift and unshift methods perform the same operations on the beginning of the array, although operations at the beginning are slower on large volumes of data because they require shifting all elements.
To get a copy of a portion of an array without mutating it, the slice method is used, while splice allows you to remove, replace, or add elements at an arbitrary position.

To combine multiple arrays, you can use the concat method or the modern spread operator, denoted by three dots, which elegantly unpacks elements. To find elements in an array, the includes method is used (returning a boolean value depending on whether the element exists) and indexOf (returning the index of the first occurrence of the searched value).

QUESTION: Какие методы массивов для итерации?

Array iteration in JavaScript is performed using built-in higher-order methods. They make code more declarative, readable, and compact compared to traditional loops like for, eliminating the need to manually manage indices and counters.

The forEach method simply executes the provided function for each element of the array, without returning any value. This is convenient for side effects, such as logging to the console or sending data to a server. The map method transforms each element of the array according to the provided function and returns a new array of the same length with the modified data.

The filter method selects elements based on a given condition, returning a new array consisting only of those elements for which the callback function returned true. The reduce method reduces an array to a single value by sequentially applying a function to an accumulator and the current element, making it indispensable for calculating sums, grouping data, or building complex structures.

Specialized methods are used to find specific elements. The find method returns the first found element that satisfies the condition, and findIndex returns its index. Finally, the checking methods some and every allow you to evaluate the contents of the array: some checks if at least one element matches the condition, and every checks if all elements satisfy the given condition.

QUESTION: How does reduce work?

The reduce method in JavaScript is a powerful tool for working with arrays, allowing you to transform an entire array into a single resulting value. This can be a number, string, array, or complex object obtained as a result of step-by-step element processing.

The basic syntax of this method looks like arr.reduce((acc, curr) => acc + curr, 0). The first part takes a callback function with the accumulator and current element arguments, and the second argument sets the initial value of the accumulator.

The most crucial aspect of working with reduce is that it is mandatory or strongly recommended to pass the initial value of the accumulator as the second argument. This can be zero for a mathematical sum, an empty string for concatenation, or an empty object for data grouping.

If the initial value is not specified explicitly, the first element of the array is automatically taken as the accumulator. In practice, this often leads to unexpected errors on empty arrays or incorrect results during the first iteration.

The method always returns the same final value, which is accumulated step by step. At each iteration, the callback function returns a new accumulator value, which is passed to the next execution step.

Due to its exceptional flexibility, the method allows not only calculating primitive data types but also forming new complex structures. With its help, you can easily group an array of users by age or count the frequency of word occurrences in text.

Prepare the source data array, for example, a list of numbers to sum up.
Call the reduce method on this array and pass a callback function to it.
Specify the initial value of the accumulator as the second argument of the function.
Return the updated accumulator value from the function to continue iterations.

Despite its versatility, the method is sometimes considered difficult to read for beginner developers. In simple scenarios, it should be replaced with more transparent and understandable methods like map or filter so as not to complicate code maintenance.

QUESTION: How to work with objects?

Working with objects in JavaScript is a fundamental skill for every developer, as objects are used everywhere to store structured data. They allow combining disparate variables and functions into single logical entities for convenient management of application state.

Creating objects is most often done using the object literal syntax with curly braces. This is the modern industry standard, although technically you can also use the universal new Object() constructor for the same purposes.

Two main approaches are available for getting or changing property values. Dot notation is used when the key name is known in advance and is a valid variable identifier without spaces or special characters.

Square brackets are indispensable when working with dynamic keys whose values are evaluated at runtime, or when keys contain spaces, hyphens, and other special characters.

To analyze the contents of an object, the standard library provides convenient static methods. Object.keys returns an array of keys, Object.values returns an array of values, and Object.entries returns an array of key-value pairs, which is critically important when iterating.

Modern syntax makes it easy to merge objects using the spread operator. You can create a new object from a combination of two old ones, where properties will be sequentially overwritten from left to right.

Create an object using curly braces and add the necessary properties to it.
Access the desired property via a dot or square brackets to read the data.
Use the Object.keys method to get a list of all object keys.
Apply the spread operator to create a new object based on an existing one.

Historically, the Object.assign method is also used for copying properties, allowing multiple sources to be combined into a target object. However, it is worth remembering that both of these approaches perform only shallow copying, leaving nested objects as references.

QUESTION: What is destructuring?

Destructuring in JavaScript is a convenient syntactic sugar that allows extracting data from arrays and objects and assigning them to variables in a shorter and more expressive way. This approach significantly reduces the amount of boilerplate code.

When working with arrays, destructuring allows assigning new variables to elements based on their strict order of appearance. For example, a construct like const [a, b] extracts the first two elements of the array into the corresponding constants.

For objects, matching occurs not by order, but by exact key names. A notation like const {name, age} automatically extracts the name and age properties from a given user object regardless of their location in the original structure.

If an existing key name is not suitable for the current context or conflicts with already declared variables, the language allows renaming the variable right during destructuring. A colon is used for this, for example, when extracting the name key into a new variable n.

Additional flexibility is provided by default values, which can be set in case a property is missing. They can be specified by writing an equal sign and the desired value directly inside the destructuring pattern.

Declare a variable using curly or square brackets on the left side of the expression.
Specify the names of the extracted object properties or array element positions.
Assign the target object or array to the right side of the expression for unpacking.
Add a colon and a new variable name to rename the property if necessary.

In addition, deep or nested destructuring is supported. It allows reaching properties of internal objects with a single line of code, extracting a property from a nested object using a combination of a colon and curly braces.

QUESTION: What are spread and rest?

The spread and rest operators in JavaScript are written identically as three dots, but perform opposite tasks depending on the context of their usage. They significantly simplify manipulations with data sets and collections.

The spread operator acts as an unpacker, expanding iterable objects such as arrays or strings into individual elements. With its help, you can easily combine arrays, creating new collections from the elements of old ones.

Also, spread is actively used for cloning and supplementing objects with new keys and values in literal form, replacing outdated data merging methods.

The rest operator, on the other hand, acts as a collector that combines multiple disparate arguments into a single array. This is especially useful when creating functions with a variable number of parameters.

When the exact number of arguments is not known in advance, it is convenient to intercept them via a rest parameter in the function signature to process them as a standard array.

Use three dots before the array name to unpack it using the spread operator.
Apply spread in an object literal to add new properties to existing ones.
Specify three dots before the parameter name in a function to collect arguments via rest.
Combine rest during destructuring to collect remaining elements into a separate array.

The same mechanism of successful data collection is also applied inside destructuring. There, you can take the first element of an array into a separate variable and pack the rest into a new array using the rest syntax.

QUESTION: How to deeply copy an object?

Creating a deep copy of an object in JavaScript is necessary when you need to completely duplicate a complex data structure with all nested objects and arrays. This guarantees that any changes in the copy will not affect the original object.

The most modern and preferred method today is using the built-in structuredClone function. It correctly handles most built-in data types, preserves circular references, and is part of the browser and Node.js standards.

A historically popular approach with serialization via JSON.parse(JSON.stringify(obj)) is still found in other people's code, but has critical limitations. It completely removes function methods, cannot work with special types like Map, Set, Date, and crashes with an error on circular references.

In large projects with an old codebase, proven third-party libraries are often used. For example, the cloneDeep utility method from the popular Lodash ecosystem reliably solves all deep cloning problems for many years.

In extreme cases, when specific business logic or strict control over copied types is required, developers write their own recursive functions for traversing object properties.

Make sure you really need a deep copy, not a shallow reference duplication.
Call the built-in structuredClone function and pass the original object to it as an argument.
Save the result of the function execution into a new variable for further use.
Verify the independence of modified nested fields in the new object relative to the original.

It is important to always remember that the regular spread operator or the Object.assign method are categorically unsuitable for deep copying. They perform exclusively shallow copying and duplicate references to all internal nested objects.

What are Map and Set?

In modern JavaScript, the Map and Set data structures are powerful built-in collections that significantly expand a developer's capabilities compared to traditional objects and arrays. These tools provide high performance and convenience when solving everyday programming tasks.

The Map collection represents a classic key-value pair dictionary. The main advantage of Map is that absolutely any data type can act as a key, including objects, functions, and primitive types, whereas in regular objects, keys can only be strings or symbols.

To create a new collection, the simple constructor syntax `new Map()` is used. After that, standard methods are used to populate and manage data: `set()` to add a pair, `get()` to retrieve a value by key, and the `has()` method, which allows you to quickly check for the existence of a key in the collection.

The Set collection, in turn, is a set of unique values of any type, where each element can occur only once. This makes Set an ideal tool for filtering duplicates from arrays or checking for the presence of elements in large data sets.

The main methods for working with a set include `add()` to add a new element, `has()` to check for its existence, and `delete()` to remove it. To convert a set back into an array, you can use the spread operator or the `Array.from()` method.

An important addition to standard collections is their "weak" counterparts — WeakMap and WeakSet. The main feature of WeakMap and WeakSet is that they use weak references to key-objects or elements, which allows the garbage collector to automatically remove them from memory.

The use of WeakMap and WeakSet prevents memory leaks in complex applications, as elements are automatically deleted as soon as other references to them disappear. This makes them indispensable for data caching and storing private object properties.

What is a Symbol?

The Symbol data type in JavaScript is a unique and immutable primitive data type introduced in the ES6 standard to solve the problem of creating hidden and unique object property identifiers.

The main feature of symbols is that even if two symbols are created with the exact same description or without one altogether, they are still strictly not equal to each other. For example, the call `Symbol('desc')` will always return a unique value, and the condition `Symbol('desc') !== Symbol('desc')` will always evaluate to true.

In practice, symbols are most often used to create unique object property keys that will not conflict with other properties. This is especially useful when developing libraries or adding metadata hidden from standard iteration loops like `for...in`.

For situations where you need to use the same symbol in different parts of an application, there is a global symbol registry. It is accessed through special methods for working with the global namespace:

`Symbol.for(key)` to create or retrieve a symbol by a string key
`Symbol.keyFor(symbol)` to reverse-lookup a string key by a symbol

In addition, JavaScript has a set of so-called well-known symbols, which are built into the language and allow developers to modify the behavior of built-in object mechanisms.

A striking example of such a symbol is `Symbol.iterator`, which defines the default iterator for an object. By implementing this method, a developer gets the opportunity to use their own custom object in a `for...of` loop.

How to work with strings?

Working with strings in JavaScript is one of the most frequent tasks in web application development. To efficiently solve such tasks, the language provides a rich arsenal of built-in methods and properties.

Basic information about a string is managed by the `length` property, which returns its length in characters. To get a specific character by index, the `charAt()` method or standard square bracket index access is used.

When you need to extract a specific text fragment, the `slice()` and `substring()` methods come to the rescue, allowing you to cut out a substring using specified start and end indices. They are convenient for processing user input and formatting output.

To transform text, the `toLowerCase()` and `toUpperCase()` methods are actively used, converting all characters of a string to lower or upper case, respectively. The `trim()` method effectively removes spaces and newline characters from both ends of a text variable.

If the developer faces the task of combining an array of strings into a single text or, conversely, splitting a string into an array by a specific delimiter, the `join()` and `split()` methods are used. This is indispensable when parsing URLs or CSV files.

Substring searching in text is performed using convenient boolean methods `includes()`, `startsWith()`, and `endsWith()`, which check for the presence of text, as well as the beginning and end of the string, respectively. To change the content of strings, the `replace()` method is used to replace the first match found, and `replaceAll()` for global replacement.

In cases where output formatting is required, for example, to add leading zeros to numbers, the `padStart()` and `padEnd()` methods are indispensable. They pad the string to a specified length with specified characters, ensuring neat data alignment.

What are regular expressions?

Regular expressions in JavaScript are a powerful and concise tool for searching, extracting, and manipulating text data based on given patterns. They are indispensable for form validation, log parsing, and processing large volumes of text.

You can create a regular expression in two main ways:

Using a literal by enclosing the pattern in forward slashes, for example `/pattern/flags`
Using the `new RegExp('pattern', 'flags')` class constructor for dynamic patterns

For basic validation of whether a string matches a pattern, the `test()` method is used, which returns a boolean value of `true` or `false` depending on whether matches are found. This method is ideal for quick validation of email addresses or phone numbers.

When you need not just to check for existence, but also to find specific matches, the string methods `match()` and the more advanced `matchAll()` are used, returning an iterator with all match details, including capture groups.

To perform mass text replacements by pattern, the `replace()` method is used, taking a regular expression as its first argument. This allows you to quickly transform date formats or remove unwanted characters from text.

Flags play an important role in working with regular expressions as they modify search behavior. The `g` flag enables global search for all matches, and the `i` flag makes the search case-insensitive.

Additional flags include `m` for multiline mode, `s` for dotall mode matching any character including line breaks, and the `u` flag for full Unicode standard support, which is critical when working with modern alphabets and emojis.

How to work with numbers?

Working with numbers in JavaScript covers many scenarios—from basic arithmetic operations to complex mathematical modeling and processing large volumes of data. The language provides built-in tools for safe conversion and formatting of numbers.

For safe conversion of strings into numeric values, the global functions `parseInt()` for integers and `parseFloat()` for floating-point numbers are used. Checking the validity of numeric data is performed using the `Number.isNaN()` and `Number.isFinite()` methods.

When you need to format a number for display, for example, to limit the number of decimal places in financial calculations, the `toFixed(digits)` method is irreplaceable. It returns a string with a specified number of decimal places, while the `toPrecision()` method controls the overall precision of the number.

Extensive capabilities for mathematical calculations are provided by the built-in `Math` object. It contains methods for rounding (`round()`), rounding down (`floor()`), rounding up (`ceil()`), as well as the `random()` method for generating random numbers.

The `Math.random()` method generates random numbers in the range from zero to one, which is often used in game algorithms, password generators, and automated tests. By combining it with other methods, you can obtain random integers within a specified range.

To work with arbitrary-precision integers that go beyond the standard safe number range (exceeding `Number.MAX_SAFE_INTEGER`), the primitive type `BigInt` was added to modern JavaScript. It is created by appending the `n` suffix to a number or by calling the `BigInt()` function, allowing you to perform precise calculations with huge integers without loss of precision.

What is asynchronous programming in JS?

Asynchronous programming in JavaScript is a fundamental concept that allows executing long-running operations, such as network requests, file reading, or timers, without freezing the entire application. To understand why this is necessary, one must remember that JavaScript is a single-threaded programming language. This means it can execute only one task at a time in its main execution thread. If all operations were executed strictly sequentially and synchronously, any request to a server would freeze the interface, making the page completely unresponsive to user actions.

To avoid this blocking, JavaScript uses an asynchronous mechanism. When code encounters a long operation, it is handed over for execution to an external environment, such as browser Web APIs or the Node.js runtime. Meanwhile, the main thread continues executing the subsequent lines of code. When the background task completes, the result is returned in the form of an event or a callback function.

Over decades of the language's evolution, the approach to writing asynchronous code has undergone significant changes. Historically, the first tools were callbacks—callback functions passed as arguments. Later, they were replaced by Promises, which made the code more readable and helped avoid deep nesting. The pinnacle of convenience became modern async/await syntactic structures, built on top of promises and allowing asynchronous code to be written as if it were synchronous.

Coordinating this entire complex system is a special mechanism called the Event Loop. It constantly monitors the state of the call stack and task queues, distributing functions that are ready for execution. Thanks to this, developers can create fast, responsive web applications capable of efficiently handling multiple background processes simultaneously, providing an excellent user experience without delays or interface freezes.

QUESTION: Что такое Event Loop?

Event Loop, or the event loop, is a fundamental mechanism in JavaScript that ensures the asynchronous behavior of the language despite its single-threaded nature. Without this mechanism, modern web development would be unimaginable, as it coordinates code execution, event handling, network requests, and interface rendering.

At the core of the Event Loop are several key components. The first of these is the Call Stack, where functions of your synchronous code are directly executed. When the interpreter encounters a function, it adds it to the stack, and removes it once execution is complete. If a function calls another one, the new function is placed on top of the first.

The second important element is the task queues. When an asynchronous operation, such as a timer or a server request, finishes, its result is sent to one of the queues. Here, there is a division into the Task Queue, which receives standard callbacks from setTimeout or DOM events, and the Microtask Queue, intended specifically for promises and DOM mutations.

The Event Loop itself is an infinite loop that constantly checks the state of the call stack. If the Call Stack is empty, the loop turns its attention to the queues. A strict priority rule applies here: the Microtask Queue is always processed before the Task Queue. The Event Loop transfers all tasks from the microtask queue to the call stack and executes them until this queue is empty. Only after that is a single task taken from the regular task queue.

This approach guarantees that promise chains and asynchronous operations will be executed as quickly as possible, preserving a predictable code execution order and preventing race conditions in a single-threaded runtime environment.

QUESTION: Что такое callbacks?

A callback, or callback function, is a traditional approach to organizing asynchronous programming in JavaScript. The core concept is that a regular function is passed as an argument to another function and is called by the latter after a specific operation completes or a given event occurs.

Classic examples of using callbacks include file reading operations, timers, or sending network requests. For example, when requesting data from a server, you pass a function that will only trigger when the response arrives. In standard Node.js practice, it is common to use the error-first callbacks approach. In this case, the first argument passed to the callback function is reserved for an error object. If an error occurred during the operation, it will be passed to this argument. If everything went successfully, the first argument will be null or undefined, and the useful data will be passed in the second and subsequent arguments.

However, using pure callbacks has a significant drawback known as callback hell. When you need to perform multiple asynchronous operations sequentially, one after another, developers are forced to deeply nest functions inside each other. Such code becomes extremely difficult to read, test, and maintain, and it greatly complicates error handling.

It is precisely because of these architectural issues that the community has gradually moved away from the widespread use of callbacks in favor of more advanced constructs, such as Promises and async/await. Despite this, understanding how callback functions work remains critically important, as they are still deeply integrated into many older libraries and fundamental browser API methods, such as addEventListener event listeners.

QUESTION: Что такое Promise?

A Promise in JavaScript is a special object used to represent the successful or failed completion of an asynchronous operation and its resulting value. A promise can be thought of as a guarantee to return some result in the future: you do not receive the data instantly, but you know that it will appear later, or you will receive an error notification.

At any given moment, a promise is in one of three possible states. The initial state is pending, when the asynchronous task is still running and the result is not yet known. If the operation completes successfully, the promise transitions to the fulfilled state, returning the resulting value. If an error occurs, the promise transitions to the rejected state, returning the reason for the failure. It is important to note that once a promise has transitioned to the fulfilled or rejected state, its status can never change again.

To work with promise results, special instance methods are used: then(), catch(), and finally(). The then() method accepts two callback functions: the first runs when the promise is successfully fulfilled, and the second when it is rejected. The catch() method is convenient syntactic sugar for error handling and is called if the promise was rejected. The finally() method runs in any case after the promise finishes its work, regardless of its success or failure, which is useful for cleaning up resources or hiding loading indicators.

The main advantage of promises over old callbacks is their ability to chain. You can call the then() method sequentially, passing the result of one asynchronous operation into the input of the next. This allows you to write clean, flat, and easily readable code, completely avoiding the problem of deep function nesting.

QUESTION: Как создать Promise?

Creating a promise in JavaScript is done using the special new Promise() constructor. This constructor accepts as an argument an executor function, which developers often call the executor. This function runs automatically right after the promise is created and accepts two mandatory parameters that are themselves functions: resolve and reject.

The first function, resolve, is used for the successful completion of the operation. When the asynchronous task inside the executor body successfully completes, you call resolve(value), passing the obtained result to it. This transitions the promise from the pending state to the fulfilled state, making the value available to the then() method. The second function, reject, is called if an error occurred during the task execution or a condition was not met. You call reject(error), passing an error object, which transitions the promise to the rejected state and activates error handlers.

In addition to manually creating promises via the constructor using an executor, JavaScript provides useful static methods for quickly creating ready-made promises. For example, the Promise.resolve(value) method returns a promise that is already in the fulfilled state with the passed value. This is useful when you need to unify the interface of a function that in some cases might return a synchronous value, and in others an asynchronous promise.

The Promise.reject(error) method works similarly, immediately creating and returning a rejected promise with the specified error reason. Using these static methods significantly simplifies writing utility code and allows you to efficiently manage the flow of asynchronous data in modern applications without extra boilerplate.

QUESTION: Как обрабатывать ошибки Promise?

Error handling in promises in JavaScript is a fundamental skill for building stable and reliable web applications. The main tool for this is the .catch(error => {}) method, which catches any failures and rejections arising in previous steps of the asynchronous chain. An alternative option is passing a second argument to the .then(null, onRejected) method, but in practice, the first approach is used much more often due to its clarity and ease of reading code.

The main advantage of the catch method is that catch catches all errors in the chain, regardless of at which specific stage of asynchronous execution the failure occurred. This allows for centralized exception handling rather than writing cumbersome handlers for each individual step. If an unexpected situation arises during code execution, execution is redirected to the nearest catch block.

It is important to remember the situation where a promise is rejected, but no error handling is provided for it. In modern runtimes, this leads to the occurrence of an unhandledrejection event on the global object, which can cause a crash in Node.js or write a warning to the browser console. To avoid such problems, it is always recommended to end promise chains with a call to the catch method.

To run code that must be executed in any case—regardless of whether the promise finished successfully or with an error—the finally() method is used. It accepts a callback function without arguments and is great for cleaning up resources, closing database connections, or hiding a loading indicator on the user interface. A competent combination of all these tools allows you to fully control the lifecycle of asynchronous operations in your projects.

QUESTION: What is Promise.all?

The Promise.all method is a powerful tool for running multiple asynchronous operations in parallel in JavaScript. Syntactically, it accepts an iterable object, most commonly an array containing promises, such as in the construct Promise.all([p1, p2, p3]), and runs them simultaneously rather than sequentially. This dramatically improves application performance when you need to request independent data from multiple sources.

The main feature of this method is that it waits for all promises to settle before continuing execution of the subsequent code. When each of the passed promises resolves successfully, the method returns an array of results, where each element corresponds to the result of the original promise in the same order they were passed into the input array. This is very convenient for destructuring the received data, for example, when simultaneously loading a user profile, their list of orders, and interface settings.

However, this approach has a critical feature that must be considered when designing architecture: it rejects on the first error. If even one of the promises in the array fails (transitions to the rejected state), the entire Promise.all immediately stops waiting for the rest and returns that error. The remaining promises continue to run in the background, but their results will be ignored. Therefore, Promise.all is ideal for tasks where all requests are critically important for the screen to work, and partial success does not make sense.

[ITEM 2] QUESTION: What are Promise.race, allSettled, and any?

In modern JavaScript, alongside the classic Promise.all, there is a whole range of helper methods for working with collections of asynchronous tasks, each solving specific business needs. The race method is designed for situations where you care about the fastest result, regardless of whether it is successful or erroneous. It returns the first settled promise among the passed ones, whether it's a success or an error, which is often used to implement network request timeouts.

The allSettled method works completely differently; it was added for situations when you need to wait for the completion of absolutely all tasks, regardless of their outcome. The allSettled method waits for all promises and returns an array of objects describing the status of each with either the result value or the error reason. The most important property of this method is that allSettled never rejects with a general promise, making it an indispensable tool for analytics or batch data processing where partial failures should not stop the entire process.

Finally, the any method focuses exclusively on finding the first successful result. It returns the very first successful promise, ignoring any preliminary errors. If all passed tasks fail, the any method rejects, returning a special AggregateError object containing all accumulated errors. This behavior makes any an ideal choice for implementing fallback mechanisms or fetching data from multiple mirrors of the same server.

[ITEM 3] QUESTION: How does async/await work?

The async/await construct is convenient syntactic sugar over standard Promises that allows you to write asynchronous code as if it were synchronous, significantly simplifying its reading and maintenance. Any async function always automatically returns a Promise, even if the return operator is not explicitly specified inside it. This guarantees that the calling code can always continue the chain via the then method or use the await keyword.

A key element of this syntax is the await operator, which can only be used inside async functions. When the interpreter reaches a line with await, it pauses the execution of the current function and waits for the specified promise to settle, freeing the thread to execute other tasks in the event loop. As soon as the promise resolves successfully, function execution resumes, and the promise value is assigned to a variable, saving the developer from having to write many nested callback functions.

For example, when developing client applications, you can sequentially send requests to the server, waiting for a response from each of them in a clear linear structure. Essentially, async/await does not introduce fundamentally new capabilities to the JavaScript engine, but it radically changes the approach to writing code, reducing the likelihood of errors due to a natural sequence of steps and simplifying the debugging of complex asynchronous algorithms.

[ITEM 4] QUESTION: How to handle errors in async/await?

Proper error handling in async/await is the key to creating fault-tolerant JavaScript applications. Since traditional .catch methods are not always directly applicable to every line here, the standard approach is to use the classic try/catch construct. You wrap potentially dangerous asynchronous code in a try { await ... } block and, if any issue occurs, catch the error object in the catch (e) { } block, which allows you to flexibly respond to failures, display messages to the user, or log incidents.

An alternative option for targeted handling is to add the standard .catch() method directly to the call of the asynchronous function returning a promise, if you prefer a functional style. To implement mandatory cleanup actions, such as freeing memory or closing connections, the finally block is used, which executes at the very end regardless of whether the try block succeeded or caught an exception. You can successfully combine these approaches to create multi-layered protection for critical sections of your code.

If a developer forgets to wrap an asynchronous function call in a try/catch block or does not handle the promise it returns, unhandled errors occur in the system, leading to the unhandledrejection event. To prevent the application from crashing unexpectedly in a production environment, it is recommended to configure the interception of this event globally at the application level, which will allow centralized collection of failure statistics and graceful termination of critical background processes.

[ITEM 5] QUESTION: How to run async operations in parallel?

To efficiently execute asynchronous operations in parallel in modern JavaScript, the most reliable and elegant solution is to use the built-in construct await Promise.all([fn1(), fn2()]). The main mistake novice developers make is that they sequentially call each asynchronous function via separate await operators, which leads to unnecessary execution thread blocking and an increase in total waiting time. When you pass an array of promises to Promise.all, all asynchronous tasks start simultaneously, and code execution pauses precisely until the longest one completes.

The execution results of all passed functions are returned as a single ordered array, making them easy to access. For convenience, destructuring syntax is often used, for example, const [a, b] = await Promise.all([fetchDataA(), fetchDataB()]), which makes the code clean and readable. However, it is important to remember a key feature: if even one of the promises in the array fails (is rejected), the entire Promise.all will immediately fail, and the rest of the results will be ignored.

If your business logic critically requires obtaining the results of absolutely all requests, regardless of whether they succeeded or failed, instead of Promise.all you should use the Promise.allSettled method. It returns an array of objects describing the status of each promise (fulfilled or rejected) along with its value or error reason. This provides maximum flexibility when handling batch network requests, reading files, or performing other independent asynchronous operations in a web application.

What are setTimeout and setInterval?

The setTimeout and setInterval functions are fundamental tools in JavaScript for managing time and scheduling code execution asynchronously via the event loop mechanism. The setTimeout(fn, ms) method is used to execute a passed function fn once after a specified number of milliseconds ms has elapsed. This is useful for creating delayed actions, such as hiding a popup notification after three seconds or implementing debounce when typing text into a search field.

In turn, the setInterval(fn, ms) method is designed for cyclically repeating a specified function at regular time intervals. It continues to run until execution is forcibly stopped. Both methods return a unique numeric timer identifier (ID) used to cancel them. To cancel a scheduled single call, the clearTimeout(timerId) function is used, and to stop a repeating interval, clearInterval(timerId) is used, which is a mandatory requirement to prevent memory leaks, for example, when unmounting components in frameworks.

It is important to understand how these timers work in the browser. The specified delay in milliseconds is not a guaranteed time for exact execution, but rather represents only the minimum allowable pause. Due to the single-threaded nature of JavaScript and the way the Event Loop works, if the main thread is busy with heavy computations, the timer will wait in line. In addition, in modern browsers, for nested timers or inactive tabs, there is a hardware limitation on the minimum delay, which is usually around 4 milliseconds.

What is requestAnimationFrame?

The requestAnimationFrame method is a specialized API in JavaScript created specifically for smoothly implementing visual animations in a web browser. Its main difference from the classic setInterval is that it is tightly synchronized with the monitor's screen refresh rate, which on most modern displays is 60 frames per second (fps), and on advanced devices can reach 120 Hz and higher.

When you pass a callback function to requestAnimationFrame, the browser guarantees that the animation code will execute right before the next page repaint. This helps avoid unpleasant effects such as lag, stuttering, and frame tearing, which often occur when trying to animate elements at fixed time intervals. The method returns a numeric request identifier that can be passed to the cancelAnimationFrame function if the animation needs to be aborted before its completion, such as when closing a modal window.

Another crucial advantage of requestAnimationFrame is its energy efficiency and care for system performance. If the user switches to another browser tab or minimizes the window, the browser automatically pauses the execution of all animations tied to this method. This allows for a radical reduction in CPU load, battery savings on mobile devices, and prevention of hardware overheating, whereas setInterval would continue to uselessly burden the system in the background.

What is fetch API?

The Fetch API is a modern, powerful, and standard way to perform network HTTP requests in JavaScript, replacing the outdated and bulky XMLHttpRequest. Basic usage comes down to calling the fetch(url, options) function, which receives the target address and an optional settings object, such as the method, headers, and request body. In response, the method returns a Promise that resolves with a Response object as soon as the server sends the headers, without waiting for the entire response body to load.

To extract useful data from the received response, special asynchronous methods of the Response object are provided. Most commonly, response.json() is used for parsing JSON data, but response.text() is also available for working with plain text or HTML, and response.blob() for downloading binary files, images, or audio. These methods also return promises, so the await operator must be applied to them to get the final result.

When working with the Fetch API, developers should keep in mind an important architectural feature: the promise returned by the fetch function is rejected (transitions to the rejected state) only in case of serious network failures, such as a complete lack of internet connection or DNS errors. If the server responds with error codes 404 (Not Found) or 500 (Internal Server Error), the promise is still considered successful. That is why after calling fetch, it is always necessary to check the response.ok flag or manually analyze the response.status property.

How to make a POST request with fetch?

To send data to the server using the modern Fetch API, you need to pass a second argument to the fetch function — a configuration object containing the method, headers, and request body. A standard POST request looks like this: fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }). This approach allows you to pass structured data previously serialized into JSON format using the JSON.stringify method.

It is extremely important to correctly specify the Content-Type: application/json header in the headers object so that the backend server understands the format in which the request body is passed and can parse it correctly. If your goal is to upload files to the server, such as images or documents, the FormData object is used instead of JSON. In this case, a FormData instance with added files is passed in the body, and the browser automatically sets the correct multipart/form-data header along with a boundary marker.

After sending a POST request, you get a promise with a response object from which you can extract data using constructions like await response.json(). However, before doing this, it is strongly recommended to check the success of the operation via the response.ok property or the response.status condition. Remember that fetch does not consider status 400 or 500 to be a promise error, so manual checking of response codes will protect your application from unexpected failures when processing incorrect data on the server.

How to cancel a fetch request?

Modern web applications often face the need to cancel network requests that have become outdated. For example, a user types text into a search bar, and a request is sent to the server for each typed character. Previous requests thus lose relevance, but continue to burden the network and the server. To solve this problem, the built-in AbortController mechanism standard in modern browsers is used.

The first step to implement request cancellation is to create an instance of this class. You declare a constant, for example, const controller = new AbortController(). This object manages the cancellation process and contains a special signal.

The second step is to pass this signal into the parameters of the fetch function. You call fetch(url, { signal: controller.signal }), thereby linking the network request with the created controller. Now the browser knows that this request is under the control of this controller.

When the need arises to cancel an operation, such as when destroying a React component or launching a new search query, you call the controller.abort() method. This action instantly interrupts the execution of the network request at the browser level.

An important aspect is handling this event in code. When cancelled, fetch throws a DOMException of type AbortError. To prevent the application from crashing with an unhandled error, the code should be wrapped in a try/catch block or use the .catch() promise chain, where you must check the error type. If the error is an instance of DOMException and its name is AbortError, you can simply ignore it, since the cancellation was planned. This approach allows you to significantly save user traffic and improve overall interface responsiveness.

QUESTION: Что такое Web Workers?

JavaScript является однопоточным языком программирования, что означает выполнение всего кода в одном главном потоке. Если выполнить сложную математическую вычсления или обработку больших объемов данных, интерфейс приложения зависнет и перестанет реагировать на действия пользователя. Для решения этой проблемы были созданы Web Workers, которые позволяют выполнять JavaScript-код в фоновом отдельном потоке.

Главная особенность технологии заключается в изоляции. Потоки воркеров выполняются параллельно с основным потоком пользовательского интерфейса, но они не имеют абсолютно никакого доступа к DOM-дереву, объектам window или document. Попытка обратиться к элементам страницы из воркера приведет к ошибке.

Создание фонового скрипта происходит очень просто. Вы инстанцируете объект с помощью конструкции new Worker('worker.js'), где передаете путь к файлу со скриптом, который будет выполняться в фоновом режиме. После этого между главным потоком и воркером настраивается асинхронный обмен данными.

Для отправки сообщений из главного потока в воркер используется метод postMessage. Внутри самого файла worker.js данные принимаются через глобальное событие onmessage. Обратная связь работает точно так же: воркер может отправить результат своей работы в основной поток с помощью своего собственного postMessage, а главный поток перехватит его через обработчик onmessage у объекта worker.

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

[ITEM 2] QUESTION: What are Web Workers?

JavaScript is a single-threaded programming language, which means all code executes in a single main thread. If you perform complex mathematical calculations or process large amounts of data, the application interface will freeze and stop responding to user actions. To solve this problem, Web Workers were created, allowing JavaScript code to run in a separate background thread.

The main feature of this technology is isolation. Worker threads execute in parallel with the main user interface thread, but they have absolutely no access to the DOM tree, the window object, or the document object. Attempting to access page elements from a worker will result in an error.

Creating a background script is very simple. You instantiate an object using the new Worker('worker.js') construct, where you pass the path to the script file that will run in the background. After that, asynchronous data exchange is established between the main thread and the worker.

The postMessage method is used to send messages from the main thread to the worker. Inside the worker.js file itself, data is received via the global onmessage event. Feedback works in the exact same way: the worker can send the result of its work back to the main thread using its own postMessage, and the main thread will intercept it via the onmessage handler on the worker object.

Web Workers are ideal for resource-intensive tasks such as filtering large arrays of data, image processing, encryption, or file compression. Offloading heavy computations to background threads guarantees that the user interface remains smooth and responsive under any loads.

[ITEM 3] QUESTION: What are Service Workers?

Service Workers are specialized scripts that the browser runs in the background, separately from the web page. They act as a programmable network proxy, intercepting and handling all network requests sent by your web application.

One of the main advantages of this technology is the ability to provide full offline functionality. Thanks to integration with the Cache API, a Service Worker can save HTML pages, styles, scripts, and images locally on the user's device. When the user loses their internet connection, the application still continues to load and work using resources served from the cache.

In addition to caching, Service Workers support background synchronization and Push notifications. Even if the tab with your site is completely closed, the browser can receive a push message from the server, process it using the worker, and show a system notification to the user, which significantly increases audience engagement.

It is important to note the strict security requirements imposed on this technology. Due to their powerful capabilities for intercepting traffic and modifying network responses, Service Workers run exclusively on secure HTTPS protocols (the only exception being local development environments like localhost).

The process of working with them involves registering the script in the main application code, followed by installation and activation stages. During installation, pre-caching of key resources typically occurs, and afterwards the worker intercepts the fetch event, checking for data in the cache before sending a request to the real network.

[ITEM 4] QUESTION: What is WebSocket?

WebSocket technology is radically different from the classic request-response model of the HTTP protocol. Instead of constantly opening a new connection for each action, WebSocket establishes a persistent, full-duplex connection between the client and the server over a single TCP connection.

To create such a connection in JavaScript, a built-in class is used. You simply initialize it by calling const socket = new WebSocket('ws://example.com/socket'), after which the browser initiates the handshake procedure to switch to the WebSocket protocol.

Managing the connection lifecycle is built on events. The WebSocket object has several key handlers. The onopen event fires immediately after the connection is successfully established. The onmessage event triggers every time new data arrives from the server. The onerror event records connection errors, and onclose reports that the channel has been closed.

To send text or binary data to the server, the send() method is used, which is called directly on the socket object. The server can send a message to the client at any time without a preliminary request from the client's side.

The main advantage of WebSockets is instant data transfer speed and minimal network overhead, since message headers are significantly smaller than HTTP headers. This technology is indispensable when developing interactive real-time applications such as chats, online games, financial market charts, collaborative document editing systems, and sports broadcasts.

[ITEM 5] QUESTION: What is queueMicrotask?

The asynchronous model of JavaScript relies on the concept of the event loop and task queues. The queueMicrotask function is a platform-built mechanism that allows you to explicitly add a new microtask to the JavaScript engine's special microtask queue.

When you call queueMicrotask(() => {}), the passed anonymous function does not execute immediately. It enters the microtask queue and is guaranteed to execute right after the execution of the current synchronous code block completes, but before control is handed over to other types of tasks or UI rendering.

The main feature of microtasks is their highest priority within the event loop. For example, macrotask queues, such as setTimeout or setInterval, have significantly lower priority. The engine will always empty the entire microtask queue to the end first, and only then proceed to execute the next macrotask.

The queueMicrotask function conceptually works in the exact same way as creating and resolving a promise via a construct like Promise.resolve().then(() => {}). However, using queueMicrotask is a cleaner, more native, and readable way to schedule microtasks when you do not need to create the promise object itself just to execute deferred code.

This tool is often used by libraries and frameworks for batch processing of state updates, deferred resource cleanup, or guaranteeing that certain code will execute asynchronously, but as early as possible, without the delays characteristic of timers.

QUESTION: Как работают классы ES6?

Classes in ES6 and higher standards are a powerful tool for code structuring, but under the hood they are implemented very simply. Their main essence is that class syntax is convenient syntactic sugar over familiar JavaScript prototypical inheritance, freeing developers from the need to manually write constructor functions and configure prototype chains. A basic class definition looks like this: we use the class keyword followed by a name, and inside the body we describe a special constructor() {} method, which is automatically called when creating a new instance via the new operator and serves to initialize the initial state of the object.

To implement inheritance and expand the capabilities of existing entities, an inheritance mechanism is used with the extends keyword, which allows the child class to adopt the functionality of the parent. In this case, the child class constructor must call the super() function, which passes control to the parent constructor and initializes the this context. In practice, this looks like this: an Animal class is created with basic properties like a name, and then a Dog extends Animal class is created, which adds dog-specific methods.

An important feature of class implementation in JavaScript is that all methods declared inside the class body are automatically written to the constructor function's prototype, i.e., prototype. This provides significant RAM savings, since instances do not duplicate methods on themselves, but share them through a common prototype chain. Each created object stores only its own unique properties, and borrows methods from the common parent template.

Thus, classes combine a strict and clear syntax familiar to programmers from other languages while retaining all the flexibility and lightweight nature of JavaScript's prototypal model. Using classes makes the codebase more readable and maintainable, especially in large team projects where component writing standardization plays a key role.

QUESTION: Что такое static в классах?

The static keyword in JavaScript classes opens up possibilities for creating static properties and methods that belong not to a specific object instance, but to the class as a whole. Declaring a static method looks like static method() {}, and a static field is defined via a construction like static property = value. The main difference between static members and regular ones lies in the way they are called: they are accessed exclusively through the class name, for example ClassName.method(), rather than through a created object instance.

Moreover, trying to call a static method or read a static property via a variable storing a class instance will result in an error, since these entities simply do not fall into the object's prototype chain. Memory for static members is allocated once when the class is loaded into memory, and they exist regardless of whether at least one application or instance of this class has been created.

In practice, static methods and properties are most often used to implement various utility functions, auxiliary algorithms, factory methods for creating objects, as well as to store constants or general state that should be uniform for all future instances of a given class. For example, a Database class can contain a static Database.connect() method that returns a single database connection following the Singleton pattern.

Using static allows you to logically isolate logic that does not depend on the data of a specific object, making the code cleaner, more structured, and professional. Developers can easily group auxiliary tools right inside business classes without creating separate files with scattered functions for this purpose.

QUESTION: Что такое getter и setter?

Getters and setters in JavaScript are special object and class methods that allow you to control access to properties, masking function calls as regular value assignment or reading. A getter is declared using the get keyword before the property name, for example get propertyName() { return this._value; }, and a setter is defined via the set keyword, for example set propertyName(val) { this._value = val; }. The key feature of these constructs is that they are called like regular properties via dot notation, without using parentheses, which makes the interface for working with the object as transparent and natural for the developer as possible.

In practice, getters and setters are indispensable when you need to implement complex validation of incoming data before writing it to a hidden field. For example, an age setter can check if the passed number is positive and throw an error otherwise, protecting the object from an invalid state. Getters, in turn, are great for automatically recalculating computed properties that depend on other parameters on the fly, eliminating the need to manually synchronize data.

In addition to validation and calculations, this mechanism is actively used to log any attempts to read or modify the internal state of an object, which is extremely useful when debugging complex systems. You can easily track at what point in time and what value was assigned to a certain property by adding corresponding console outputs to the setter.

Furthermore, this mechanism is applicable not only in classes, but also in regular object literals, which makes it easy to introduce encapsulation and reactive behavior into a wide variety of data structures in your application without writing redundant boilerplate. This makes the code more reliable, flexible, and resistant to unexpected changes from other program modules.

QUESTION: Что такое приватные поля классов?

Private class fields, introduced in the ES2022+ standard, radically changed the approach to data encapsulation in JavaScript, allowing developers to create properties and methods that are truly hidden from the outside world. To make a field or method private, it is enough to add a hash symbol before its name, which looks like #privateField for properties and #privateMethod() {} for methods. The main security guarantee of such members is that they are accessible exclusively inside the body of the class itself where they were declared, and are completely isolated from the outside world.

If you try to access a private field or call a private method from outside the class via an instance or from descendant functions, the JavaScript interpreter will immediately throw a syntax or runtime error, preventing unauthorized access to internal logic. This innovation solved a long-standing problem in JavaScript, where developers for a long time had to use various naming conventions, like adding an underscore before the property name, or complex closures to hide data.

In practice, private fields are ideal for storing internal authorization tokens, request caches, auxiliary counters, or intermediate calculation results that should not concern the user of this class. For example, in a User class, you can create a #passwordHash private field that will be populated upon registration and used only inside the verification method, remaining inaccessible for reading from the global scope.

Now, native support for private fields at the language level allows you to write reliable, secure code protected from accidental modifications, clearly separating the public interface of the class from its internal implementation. This elevates the architecture of client and server applications to a completely new level of reliability and security.

QUESTION: How does class inheritance work?

Class inheritance in modern JavaScript is built on the basis of syntactic sugar over prototypal inheritance and is implemented using the extends keyword. For a child class to successfully inherit from a parent class, a construct of the form class Child extends Parent {} is used. At the same time, creating an instance of a child class requires strict compliance with the rules for working with the constructor. If a descendant class defines its own constructor, calling super() in it is mandatory and must be on the very first line before accessing the this keyword. This is necessary to correctly initialize the context of the parent class. If you omit the super() call, the interpreter will throw an error.

In addition to initialization, the super operator is actively used to call parent methods from the child class. For example, if in the child class you want to extend the functionality of a method that already exists in the parent, you can write super.method() inside your overridden method to first execute the parent's logic and then add your own. Method overriding allows you to flexibly customize object behavior for specific task requirements.

To check whether an object belongs to a specific class or constructor function, the instanceof operator is used. It allows you to safely determine whether an object was created based on a specific class, which is especially useful when working with class hierarchies and polymorphism.

QUESTION: What is new.target?

The special property new.target, introduced in the ES6 standard, is a powerful tool for metaprogramming in JavaScript. The main task of this property is to directly point to the constructor that was called using the new keyword. If a function or class was called normally, without the new operator, the value of new.target will be undefined. This feature makes it easy to determine whether a constructor was called correctly, and if necessary, force it to be called via new or throw an error.

One of the classic practical applications of new.target is implementing abstract classes, which should not have instances directly. You can write a check inside the base class constructor: if new.target equals the abstract class itself, you should initiate an error prohibiting direct object creation, allowing instances to be created only through child subclasses.

It is important to consider the behavior of new.target inside various types of functions. In regular functions and class constructors, it refers to the invoking constructor. However, arrow functions do not have their own new.target property. Inside an arrow function, it is inherited from the outer context, just like the this keyword, which makes the behavior predictable when using nested functions.

QUESTION: What is instanceof?

The instanceof operator in JavaScript is used to check whether a specific object is part of the prototype chain of a certain class or constructor function. The usage syntax is extremely simple and looks like obj instanceof Class. If, during the search through the prototype chain of the obj object, a prototype matching the prototype property of the checked Class is found, the operator returns true; otherwise, it returns false.

This mechanism works equally well with both modern classes and traditional constructor functions. However, it should be remembered that instanceof is not intended for checking primitive data types such as strings, numbers, or boolean values. For primitives, the typeof operator is traditionally used.

The JavaScript language also provides the ability to customize the behavior of the instanceof operator using the special built-in Symbol.hasInstance symbol. By defining this static method inside your class, you can completely override the check logic, making it custom. This allows developers to create flexible libraries and frameworks where checking whether objects belong to data types can be managed programmatically.

QUESTION: What are mixins?

Mixins in JavaScript represent a design pattern used as an effective alternative to multiple inheritance, which is not directly supported by the language. The essence of a mixin is encapsulating a specific set of methods and properties into a separate object or function, which can then be added to the prototype of the target class.

The simplest way to implement a mixin is using the Object.assign(Class.prototype, mixin) method. This approach allows you to quickly copy all methods from the mixin object directly into your class's prototype, making them available to all future instances. For more complex scenarios, mixin functions are used, which take a base class as an argument and return a new extended class using the extends syntax with dynamic return values.

Using mixins allows you to build application architecture based on behavior composition rather than a rigid inheritance hierarchy. You can create small, independent modules of functionality—such as logging, event management, or validation—and attach them only to the classes that actually need them, avoiding code duplication and the creation of cumbersome inheritance trees.

QUESTION: What is Object.create()?

The Object.create() method is a fundamental tool in a JavaScript developer's arsenal for working with prototypal inheritance. It allows you to create an entirely new object while explicitly specifying a prototype object for it. The basic call syntax looks like Object.create(proto, descriptors), where the first argument is the prototype of the object being created, and the second is optional property descriptors.

One of the most well-known and useful variations of this method is creating an object without a prototype by calling Object.create(null). Such an object does not inherit standard methods from Object.prototype, including toString, valueOf, and others. This makes the created object a perfectly clean dictionary or hash map, eliminating any naming conflicts and security issues when dynamically adding keys.

The second argument of the method, descriptors, allows you to finely tune the properties of the created object using descriptors, setting their enumerability, writability, and readability parameters. This gives full control over the data structure and allows for encapsulation at the object property level.

QUESTION: What is Object.defineProperty?

The Object.defineProperty method in JavaScript is a powerful tool for low-level property management of objects, allowing you to precisely configure their behavior. It defines a new property directly on an object or modifies an existing one, returning the same object. The main feature of the method lies in the ability to use descriptors—special configuration objects that define the hidden characteristics of a property.

There are two types of descriptors: data descriptors and accessor descriptors. A data descriptor includes parameters such as value, which defines the property's value, and writable, which controls whether this value can be modified. An accessor descriptor uses getter and set accessor functions instead of value to intercept reading and writing of the property, which is convenient for "on-the-fly" computations or validation.

An important aspect of the method's operation is that by default, all control flags (writable, enumerable, configurable) for new properties created via Object.defineProperty are set to false. This means the property will not be overwritable by default, will not participate in loops like for...in, and cannot be deleted or have its descriptors changed later. To check the current settings of a specific property, you can use the paired method Object.getOwnPropertyDescriptor.

Practical applications of Object.defineProperty include creating immutable constants within objects, hiding utility fields from iteration, or creating dynamic properties computed on every access. For example, you can define a fullName property that automatically compiles from firstName and lastName upon reading, and splits the string back upon writing.

Что такое Proxy?

Объект Proxy в современном JavaScript представляет собой специальную обёртку вокруг другого объекта, которая позволяет перехватывать и переопределять базовые операции с ним, такие как чтение свойств, запись, вызов функций и многое другое. Синтаксис создания прокси выглядит как new Proxy(target, handler), где target — это оригинальный объект, а handler — объект, содержащий функции-перехватчики, которые называют ловушками или traps.

Среди наиболее популярных ловушек можно выделить get для перехвата чтения свойств, set для перехвата записи, apply для перехвата вызова функций и construct для перехвата создания экземпляров через оператор new. Благодаря этому Proxy открывает огромные возможности для метапрограммирования, создания кастомного поведения и решения архитектурных задач, которые раньше были невозможны или требовали сложных костылей.

На практике Proxy активно используется для реализации реактивности в современных фреймворках, когда изменение состояния объекта автоматически запускает перерисовку пользовательского интерфейса. Также прокси незаменим для валидации данных «на лету»: например, можно создать ловушку set, которая будет проверять, чтобы присваиваемое свойство age всегда было положительным числом, и выбрасывать ошибку в противном случае. Другие сценарии включают логирование доступа к свойствам, виртуализацию больших массивов или создание объектов с защитой от доступа к несуществующим полям.

При написании ловушек часто возникает необходимость выполнить стандартное действие оригинального объекта. Для этого используется встроенный объект Reflect, методы которого полностью дублируют ловушки Proxy. Использование Reflect внутри handler гарантирует корректный контекст выполнения this и правильную обработку внутренних механизмов JavaScript.

Что такое Reflect?

Встроенный объект Reflect в JavaScript представляет собой глобальный объект, который предоставляет набор статических методов для выполнения перехватываемых операций над объектами. Концептуально методы Reflect зеркально повторяют ловушки объекта Proxy и служат для того, чтобы упростить выполнение стандартных операций вроде чтения свойств, их записи, проверки наличия или удаления.

Среди ключевых методов Reflect можно выделить Reflect.get, Reflect.set, Reflect.has и Reflect.deleteProperty. Главное отличие методов Reflect от старых операторов или методов Object заключается в типе возвращаемого значения. Например, оператор delete или метод Object.defineProperty при неудачной операции могут выбросить исключение или вернуть неинтуитивный результат, в то время как методы Reflect всегда возвращают логическое значение boolean, указывающее на успешность или неуспех операции.

Reflect тесно связан с объектом Proxy и создавался во многом для совместного использования с ним. Когда вы перехватываете операцию в Proxy-обработчике, стандартной практикой является делегирование этой операции оригинальному объекту с помощью соответствующего метода Reflect. Это обеспечивает предсказуемое поведение кода, правильную передачу контекста и совместимость с внутренними слотами объектов.

Помимо работы с Proxy, Reflect предлагает более явный, функциональный и унифицированный API для повседневной работы с метаданными объектов. Например, проверка наличия свойства через Reflect.has(target, propertyKey) выглядит чище и современнее, чем использование оператора 'in' в некоторых сложных паттернах проектирования, делая код более читаемым и легко поддерживаемым в рамках больших командных проектов.

Как сделать объект неизменяемым?

Обеспечение неизменяемости данных — частая задача в разработке на JavaScript, особенно при работе с парадигмами функционального программирования или управлением состоянием. В языке предусмотрено несколько уровней защиты объектов от изменений, каждый из которых решает свои задачи с разной степенью строгитрости.

Самым радикальным методом является Object.freeze(). Этот метод осуществляет полную заморозку объекта: в него нельзя добавлять новые свойства, удалять существующие, а также изменять их значения или дескрипторы (writable и configurable устанавливаются в false). Однако важно помнить, что заморозка является поверхностной, то есть если свойство объекта само является вложенным объектом, то внутренний объект останется изменяемым, если не применить к нему заморозку рекурсивно.

Промежуточный уровень защиты предоставляет метод Object.seal(). Он делает объект запечатанным: запрещает добавление новых свойств и удаление существующих, но оставляет возможность изменять значения уже имеющихся свойств, если их флаг writable равен true. Еще более мягкий вариант — Object.preventExtensions(), который запрещает только добавление новых свойств, оставляя всё остальное без изменений.

Для проверки текущего состояния защищенности объектов в JavaScript предусмотрены специальные булевы методы: Object.isFrozen(), Object.isSealed() и Object.isExtensible(). Понимание разницы между этими методами позволяет разработчикам выбирать оптимальный уровень защиты данных в зависимости от архитектурных требований приложения, предотвращая случайные мутации состояния в сложных логических цепочках.

Что такое Object.entries/keys/values?

Методы Object.keys(), Object.values() и Object.entries() представляют собой стандартный и удобный набор инструментов для трансформации объектов в массивы и дальнейшей их итерации. Каждый из этих статических методов решает свою специфическую задачу по извлечению данных из структуры ключ-значение.

Метод Object.keys() принимает объект в качестве аргумента и возвращает массив строк, содержащий все имена собственных ключей этого объекта. Метод Object.values(), напротив, возвращает массив значений, соответствующих этим ключам. Метод Object.entries() объединяет оба подхода, возвращая двумерный массив пар, где каждый элемент представляет собой массив вида [ключ, значение], что идеально подходит для циклов вроде for...of или передачи в методы массивов.

Важной особенностью всех трех методов является то, что они работают исключительно с собственными перечисляемыми свойствами объекта (enumerable own properties). Это означает, что свойства, унаследованные по цепочке прототипов, а также свойства с флагом enumerable, установленным в false, будут автоматически проигнорированы и не попадут в результирующие массивы.

Для обратной трансформации в JavaScript предусмотрен метод Object.fromEntries(). Он выполняет противоположную операцию, принимая массив пар [ключ, значение] (или любой другой итерируемый объект с таким же форматом) и собирая из них полноценный объект. Это трио методов в сочетании с методами массивов вроде map, filter и reduce образует мощный пайплайн для декларативной обработки и трансформации данных любой сложности.

Как выбрать элементы DOM?

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

Самым быстрым способом найти уникальный элемент на странице является метод getElementById. Он принимает в качестве аргумента строку с уникальным идентификатором элемента (атрибутом id) и возвращает объект этого элемента или null, если ничего не найдено. Поскольку ID должен быть уникальным в пределах документа, этот метод работает очень эффективно.

Для более сложных и универсальных задач используются методы querySelector и querySelectorAll. Первый из них, querySelector, принимает любой валидный CSS-селектор и возвращает первый попавшийся элемент, который удовлетворяет этому условию. Например, можно искать по классу, тегу, атрибуту или их комбинации. Если вам нужно получить все элементы, соответствующие условию, используется querySelectorAll. Этот метод возвращает статичный список элементов в формате NodeList, по которому можно итеративно пройтись, например, с помощью цикла forEach.

Исторические методы поиска, такие как getElementsByClassName и getElementsByTagName, возвращают живую коллекцию HTMLCollection. Они ищут элементы по конкретному классу или тегу соответственно. Особенность "живых" коллекций заключается в том, что они автоматически обновляются в реальном времени при изменении структуры DOM, что бывает удобно в определенных сценариях, но требует осторожности при манипуляциях с элементами внутри циклов.

На практике выбор метода зависит от вашей задачи. Для точечного поиска уникальных элементов лучше всего подходят getElementById или querySelector. Когда требуется стилизация или пакетная обработка группы элементов по сложным правилам верстки, незаменимыми окажутся querySelectorAll и селекторы CSS.

[ITEM 1] QUESTION: How to change the content of an element?

Changing the content of elements in the DOM is one of the most frequent tasks when developing interactive web applications. JavaScript provides several different properties for this purpose, each solving specific problems and having its own security and performance nuances.

The safest and most recommended way to manage text content is the textContent property. It sets or returns the text content of a node and all its descendants. When using textContent, all passed text is treated strictly as a string, not as HTML code. This means that if a user tries to inject a malicious script through an input field, the browser will simply display it as text rather than execute it. This approach completely protects the application from XSS (Cross-Site Scripting) attacks, so this property should always be preferred when working with plain text.

On the other hand, the innerHTML property allows not only changing text but also injecting full HTML markup. With its help, you can dynamically create complex structures inside an element by adding new tags, attributes, and nested blocks. However, the main problem with innerHTML is its vulnerability to XSS attacks. If you inject data obtained directly from users into innerHTML without prior strict sanitization, an attacker can execute arbitrary JavaScript code in the browser of other users. Therefore, innerHTML should only be used with fully trusted static or pre-sanitized content.

There is also the innerText property, which is largely similar to textContent, but takes into account CSS styles and the visibility of elements on the page. For example, innerText will not return the text of elements hidden using display: none and takes formatting into account. However, its use is more expensive for the browser in terms of performance, as it requires recalculating the page layout.

In real-world development, the choice boils down to a simple rule: use textContent for outputting plain text to ensure maximum security and speed, and use innerHTML for inserting pre-made markup after ensuring the data source is secure.

[ITEM 2] QUESTION: How to work with attributes?

Working with HTML element attributes in JavaScript allows you to dynamically manage the behavior, state, and appearance of page components. There is a standard set of methods and properties for this that provide complete control over any standard and custom attributes.

Basic manipulations are performed using the getAttribute() and setAttribute() methods. The getAttribute(name) method takes the attribute name as a string and returns its current value. If the element does not have such an attribute, the method will return null. The setAttribute(name, value) method allows you to set a new value for the specified attribute or create it if it did not exist yet. To remove unnecessary attributes, the removeAttribute(name) method is used, which completely erases the specified attribute from the element's markup. You can quickly check for the presence of an attribute using the hasAttribute(name) method, which returns a boolean value of true or false.

Of particular interest are custom attributes, which in modern HTML are conventionally named using the data- prefix (for example, data-user-id, data-role). For convenient work with them, JavaScript provides a special dataset property. It automatically converts all data- attributes of an element into an object with camelCase notation. For example, if you have a data-user-id attribute, you can get or change its value directly through the el.dataset.userId property. This eliminates the need to constantly call getAttribute and setAttribute methods, making the code cleaner and more readable.

In practice, these tools are used everywhere: from passing database record IDs to click handlers to storing interface state directly in the markup. Using dataset is the modern standard for custom data, while the classic getAttribute and setAttribute methods remain indispensable when working with standard attributes like href, src, disabled, or aria labels.

[ITEM 3] QUESTION: How to work with element classes?

Managing CSS classes in JavaScript is a key tool for creating dynamic user interfaces, animations, and switching color themes. The most modern, convenient, and efficient way to work with classes is the classList property, which is a special DOMTokenList object.

The classList object provides a rich set of methods for targeted modification of an element's classes. The classList.add('className') method adds one or more new classes to an element if they are not already there. The classList.remove('className') method removes the specified class, making it easy to hide elements or reset states. The universal classList.toggle('className') method works like a switch: if the element has the class, it is removed; if not, it is added. This is extremely convenient for implementing drop-down menus, modal windows, or theme switching.

To check for the presence of a specific class, the classList.contains('className') method is used, returning a boolean true or false, which is indispensable in conditional statements. Meanwhile, the classList.replace(oldClass, newClass) method allows replacing an existing class with a new one in a single statement, eliminating the need to write combinations of remove and add.

The historical approach involved using the className property, which returns or sets the entire string of element classes at once. Working with className is less convenient because when changing a single class, you have to manually parse and concatenate the entire string to avoid overwriting the remaining styles. Unlike it, classList eliminates these problems, allowing you to manage each class isolated and safely.

For example, to implement an interactive "like" button, it is enough to attach an event handler to it that will call el.classList.toggle('is-active'). This makes the code minimalist, readable, and easily maintainable in any project.

[ITEM 4] QUESTION: How to change element styles?

Changing element styles using JavaScript allows you to create dynamic and responsive interfaces that react to user actions in real time. There are several approaches to managing the appearance of elements, each suitable for a specific range of tasks.

The most direct way to change an individual style property is to access the style property of the target element, for example: el.style.color = 'red'. It is important to remember that CSS property names consisting of multiple words are written in JavaScript in camelCase format instead of kebab-case. Thus, the CSS property background-color turns into backgroundColor in JavaScript. This approach is ideal for targeted changes to one or two parameters when the logic is encapsulated directly in the script.

If you need to apply multiple styles simultaneously, it is more convenient to use the cssText property. It allows you to write an entire string of CSS rules directly into an element, for example: el.style.cssText = 'color: red; background-color: blue; font-size: 16px;'. However, it is worth considering that assigning a value via cssText completely overwrites all inline styles that were previously set via the style property.

Sometimes there is a need to find out not what is written in the element's inline styles, but its actual styles computed by the browser, taking into account all stylesheets and inheritance rules. For this, the global getComputedStyle(el) function is used. It returns an object with all computed styles of the element, the values of which can be read, but cannot be directly modified through this object.

Working with CSS custom properties (variables) deserves special attention. You can dynamically change the values of CSS variables directly from JavaScript by calling the setProperty method on the element's style object. For example, el.style.setProperty('--main-color', '#ff0000'). This is an incredibly powerful approach for implementing themes, where all appearance logic is moved to CSS, and the script merely switches variable values.

[ITEM 5] QUESTION: How to create and add an element?

Creating and adding new elements to the document is one of the most common tasks when developing web applications using JavaScript. This process consists of two main stages: direct creation of the node in the browser's memory and its subsequent integration into the existing DOM tree. To create a new node, the document.createElement method is used, which accepts a string with the tag name, for example, document.createElement('div'). Once the element is created, you can assign classes, attributes, and text content to it, after which it must be inserted into the page.

To add the created element, there are several methods depending on where exactly it needs to be placed. The classic and most versatile method is parent.appendChild(el), which adds the node to the very end of the specified parent's child list. If you need to place an element before a specific existing child element, the parent.insertBefore(el, ref) method is used, where the first argument is the new element, and the second is the reference element before which the insertion will occur.

The modern JavaScript standard offers more flexible and convenient methods for working with nodes. The el.append() and prepend() methods allow adding elements inside the parent element at the very end or at the very beginning, respectively. In addition, append and prepend accept not only nodes but also plain text strings, and allow adding multiple elements in a single call.

For even more precise positioning, the insertAdjacentHTML and insertAdjacentElement methods are used. They allow inserting markup or elements relative to the chosen element at four points: before the element itself, immediately after its opening tag, before the closing tag, or after the element itself. The correct choice of method depends on your application architecture and performance requirements.

[ITEM 1] QUESTION: Как удалить элемент? ===ANSWERS=== QUESTION: How to remove an element?

Managing the lifecycle of elements on a web page involves not only creating them, but also correctly removing them when they are no longer needed by the user. In modern JavaScript, there are several effective ways to remove elements from the DOM tree, each with its own application features. The simplest, most concise, and modern way is the el.remove() method, which is called directly on the element being removed. This approach is supported by all modern browsers and does not require accessing the parent container, making the code cleaner and more readable.

Historically, another approach exists that is often found in older codebases: the parent.removeChild(el) method. To use it, you must first find the parent element of the node to be removed, and then pass the node itself as an argument. Although this method requires more lines of code compared to remove(), it can be useful in specific optimization scenarios or when strict hierarchy control is required.

Sometimes, instead of completely removing an element, you need to replace it with another one. The el.replaceWith(newEl) method is great for this purpose, automatically replacing the current element with a new node or a set of nodes passed in the arguments. If the task is to quickly clear a large container of all child elements, developers often use the trick of resetting the parent element's innerHTML property to an empty string, for example, innerHTML = ''. This method works very quickly, although it has nuances related to memory leaks in some browsers due to event listeners not being properly removed.

It is important to note that removing an element from the DOM using the listed methods does not completely destroy its object in RAM if variables in the JavaScript code continue to reference it. Due to this, removed elements can be inserted back into the document in a different place using standard insertion methods, while preserving their state, attached data, and nested structure.

[ITEM 2] QUESTION: Что такое event bubbling и capturing?

Understanding event propagation mechanisms in the browser environment is critically important for working effectively with user input in JavaScript. When an event, such as a mouse click, occurs on an element, the browser initiates a process where the event travels through the entire DOM tree, consisting of two consecutive phases: capturing and bubbling.

The first phase is called capturing. At this moment, the event begins its journey from the root window object down through all intermediate parent elements straight to the target element where the action occurred. By default, event listeners do not fire during this phase; however, a developer can explicitly instruct the browser to intercept events during the capturing phase. To do this, when registering a handler via the addEventListener method, the boolean value true or an object with the option capture: true is passed as the third argument.

The second and main phase is called bubbling. It begins immediately after the event reaches the target element. At this stage, the event starts moving in the opposite direction—from the target element up to its parents, rising all the way to the document and window objects. By default, all standard events in JavaScript operate precisely in the bubbling mode, which allows handling actions at higher levels of the hierarchy.

Special tools are provided to manage this process. For example, the event.stopPropagation() method, called inside an event handler, completely stops the further propagation of the event up or down the DOM tree, preventing similar handlers on parent elements from firing. This is often necessary in complex interfaces when a click on a child button should not trigger a click on the surrounding card.

[ITEM 3] QUESTION: Что такое event delegation?

Event delegation is one of the most powerful and elegant interface design patterns in JavaScript. The essence of this approach is that instead of attaching a separate event handler to each child element, we set just one common handler on their shared parent. This method relies on the event bubbling mechanism, thanks to which a click or any other action on a child node always reaches the parent container.

When such a common handler triggers, two key properties are always available in the event object: event.target and event.currentTarget. The event.target property points to the specific element where the action directly occurred, for example, a specific button inside a list. At the same time, event.currentTarget references the element to which the handler itself is attached, i.e., the parent container. The difference between these properties lies at the core of the pattern's operation.

Event delegation becomes an indispensable tool when working with dynamic content. If a list of elements is constantly updated with new items from a database or a user adds them interactively, the standard approach would require re-attaching handlers to each new element. With delegation, new elements start working automatically because their events bubble up to the old parent.

Since the event bubbles from the target element, the handler on the parent often needs to figure out which exact element was clicked. To filter and check elements, standard DOM API methods such as matches() and closest() are used. The matches('selector') method checks whether an element matches a given CSS selector, and closest('selector') travels up from the target element in search of the nearest ancestor that satisfies the condition, which is especially convenient if the click happened on an icon inside a button.

[ITEM 4] QUESTION: Как предотвратить действие по умолчанию?

Many web page elements have built-in standard behavior set by the browser by default. Examples of such behavior include submitting a form when clicking a submit button, following a link when clicking an a tag, opening a context menu on a right-click, or scrolling the page when pressing arrow keys. However, in modern web development, there is often a need to intercept these actions and process them using scripts, for example, sending form data via AJAX without reloading the page.

To cancel the browser's standard behavior, the event.preventDefault() method is used, which is called on the event object inside the handler function. As soon as this method is triggered, the browser stops executing the default action associated with the current event. It is important to note that this method cancels precisely the standard behavior, but does not stop the propagation of the event through the DOM tree, so bubbling or capturing will continue unless stopped separately.

In old code and legacy JavaScript standards, returning the boolean value false at the end of the handler function, for example via return false, was often used for the same purposes. This approach simultaneously canceled the default action and stopped event bubbling; however, in modern code, it is recommended to abandon it in favor of explicitly calling event.preventDefault() to improve code readability and predictability.

To check whether the standard behavior has been canceled for a specific event, developers can use the special event.defaultPrevented property. It returns the boolean value true if the preventDefault() method has already been called by any of the previous handlers. This is useful in large, complex applications where several independent modules may listen to the same event and make decisions based on the current state of the interface.

[ITEM 5] QUESTION: Что такое DOMContentLoaded и load?

The DOMContentLoaded and load events play a key role in the web page loading lifecycle, and understanding the difference between them is critically important for a frontend developer. The DOMContentLoaded event fires on the document object when HTML is fully loaded and parsed, and the DOM tree is completely built. At the same time, the browser does not wait for stylesheets, images, and subframes to load. This makes DOMContentLoaded the ideal moment to initialize interactive interface elements, attach event handlers, and execute basic script logic that does not need image dimensions.

The load event, in turn, fires on the window object slightly later, when not only the HTML and DOM tree are fully loaded, but also all associated resources: stylesheets, scripts, images, and third-party frames. Since loading heavy graphics can take a noticeable amount of time, load occurs significantly later than DOMContentLoaded.

In practice, DOMContentLoaded fires much faster, which helps improve the user-perceived loading speed of the site. To ensure scripts execute on time and do not block page rendering, the defer attribute is often used for the script tag, which instructs the browser to load the script asynchronously in the background and execute it immediately after DOMContentLoaded completes.

To check the current document loading state at any given time, you can use the document.readyState property. It takes three possible values: loading, when the document is still loading; interactive, when the document has been fully read and parsed (which corresponds to the moment before the DOMContentLoaded event); and complete, when all resources are fully loaded (which corresponds to the moment before the load event). This property is convenient to use in universal functions that need to execute regardless of whether the document managed to load before the script was connected.

Как работать с формами в JS?

Работа с формами в JavaScript является одной из наиболее частых задач при создании интерактивных веб-приложений, от простых контактных форм до сложных многошаговых опросников. Для эффективного взаимодействия с элементами формы используется свойство form.elements, которое представляет собой живую коллекцию всех полей ввода, кнопок и селектов, содержащихся внутри формы. Обратиться к конкретному полю можно как по его индексу в коллекции, так и по значению атрибута name, что делает код более читаемым и надежным при изменении верстки.

Получить введенные пользователем данные или изменить их можно с помощью свойства input.value для текстовых полей, чекбоксов и радиокнопок. Для переключателей и чекбоксов также активно используется свойство checked. Если вам нужно программно инициировать отправку формы без участия пользователя, например, после валидации данных или по таймеру, применяется метод form.submit(). Стоит учитывать, что программный вызов submit() не вызывает событие submit самой формы, поэтому всю предварительную логику валидации нужно выполнять вручную.

Для возврата формы в исходное состояние используется метод form.reset(), который очищает все поля ввода и возвращает их к значениям по умолчанию, прописанным в HTML. Это удобно использовать после успешной отправки данных через AJAX, чтобы пользователь мог ввести новую информацию.

Для удобного сбора и отправки данных формы на сервер без перезагрузки страницы используется объект FormData. Он автоматически собирает все значения из элементов формы, у которых задан атрибут name. Пример типичного сценария работы выглядит так: пользователь заполняет поля, по клику на кнопку отправки вы перехватываете событие submit, отменяете стандартное поведение браузера через preventDefault(), создаете экземпляр FormData на основе элемента формы, а затем отправляете эти данные на сервер с помощью асинхронного запроса fetch. Такой подход обеспечивает современный и плавный пользовательский опыт без дерганья страницы.

Что такое FormData?

Объект FormData в JavaScript представляет собой мощный и удобный инструмент для создания и манипуляций с наборами пар ключ-значение, которые затем могут быть легко отправлены на сервер с использованием асинхронных запросов, таких как fetch или XMLHttpRequest. Основной способ создания объекта FormData — передача в конструктор существующей HTML-формы: const formData = new FormData(formElement). При этом объект автоматически считывает все поля ввода, имеющие атрибут name, включая текстовые поля, выпадающие списки и даже файлы, выбранные пользователем через input типа file.

Помимо автоматического сбора данных из существующей формы, вы можете работать с FormData программно, используя встроенные методы. Метод append(name, value) позволяет добавлять новые данные или дополнительные значения к существующим ключам. Если нужно получить конкретное значение по ключу, используется метод get(name), а для удаления данных служит метод delete(name). Также объект поддерживает метод set(name, value), который перезаписывает все существующие значения с указанным именем на новое.

Для обхода всех данных, содержащихся в объекте FormData, предусмотрены специальные методы итерации, такие как entries(). С его помощью можно легко пройтись по всем парам ключ-значение с помощью цикла for...of, что особенно полезно для отладки или предварительной валидации данных перед отправкой.

Главным преимуществом FormData является встроенная поддержка мультимедийных данных. Вы можете добавлять файлы напрямую из input.files, и браузер автоматически сформирует запрос в формате multipart/form-data, который корректно обрабатывается бэкендом на любых языках программирования. Отправка таких данных с помощью fetch выглядит крайне лаконично: достаточно передать объект FormData в качестве значения свойства body в конфигурационном объекте запроса, не заботясь о ручной установке заголовков Content-Type, так как браузер установит их самостоятельно вместе с необходимым уникальным разделителем.

Как работать с localStorage?

Объект localStorage в JavaScript является частью веб-хранилища браузера и позволяет сохранять пары ключ-значение непосредственно в браузере пользователя для персистентного хранения данных между сессиями. Главная особенность localStorage заключается в том, что сохраненные данные остаются доступны даже после закрытия вкладки, перезапуска браузера или выключения компьютера, пока пользователь или скрипт явно их не удалят.

Для записи данных в хранилище используется метод localStorage.setItem(key, value), где оба аргумента обязательно должны быть строками. Чтобы прочитать сохраненные данные, применяется метод localStorage.getItem(key, value), который возвращает строку со значением или null, если указанный ключ отсутствует в базе. Удаление конкретного элемента осуществляется с помощью метода localStorage.removeItem(key), а если требуется полностью очистить всё хранилище для данного домена, используется метод localStorage.clear(), который удаляет абсолютно все сохраненные пары ключ-значение.

Важнейшей технической особенностью localStorage является то, что он работает исключительно со строками. Если вы попытаетесь записать в него объект, массив или булево значение, JavaScript автоматически преобразует его в строку "[object Object]", потеряв все исходные данные. Чтобы успешно сохранять сложные структуры данных, такие как массивы объектов или пользовательские настройки, перед записью необходимо сериализовать их в формат JSON с помощью метода JSON.stringify(data), а при чтении — обратно десериализовать с помощью JSON.parse(savedData).

Объем памяти localStorage обычно составляет около 5–10 мегабайт на один домен, чего с избытком хватает для текстовых настроек, токенов авторизации и черновиков постов. Все операции с хранилищем происходят синхронно в главном потоке выполнения, поэтому не стоит злоупотреблять сохранением слишком больших объемов данных, чтобы избежать возможных задержек в отрисовке интерфейса.

Чем отличаются localStorage и sessionStorage?

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

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

Оба хранилища имеют схожие ограничения по объему памяти, который обычно составляет от 5 до 10 мегабайт в зависимости от конкретного браузера. И первое, и второе хранилище поддерживают одинаковый набор методов: setItem, getItem, removeItem, clear, а также работают исключительно со строками, требуя применения JSON.stringify и JSON.parse для сохранения объектов и массивов.

Синхронность операций характерна для обоих типов хранилищ: все запросы на чтение и запись выполняются в основном потоке JavaScript блокирующим образом. Выбор между ними зависит от бизнес-логики: localStorage идеально подходит для сохранения пользовательских настроек темы, токенов авторизации или корзины интернет-магазина, в то время как sessionStorage незаменим для временных данных, таких как состояние многошаговой формы, данные фильтров текущей сессии или защита от повторной отправки запросов, которые не должны переноситься между разными вкладками одного сайта.

Что такое cookie в JavaScript?

Понятие cookie в языке программирования JavaScript представляет собой механизм, позволяющий веб-приложениям сохранять небольшие объемы текстовых данных прямо в браузере пользователя. Основным инструментом для работы с ними выступает специальное свойство document.cookie. Важно понимать, что документ document.cookie возвращает не массив или объект, а единую строку, содержащую все доступные куки текущего домена, разделенные точкой с запятой и пробелом, например, имя равно значению. Такой формат требует от разработчика самостоятельного парсинга строки при чтении конкретных параметров.

Для установки или изменения нового значения используется простая операция присваивания, где вы задаете строку формата document.cookie = 'name=value'. Однако на практике этого редко бывает достаточно, поэтому применяются дополнительные параметры атрибутов, которые задаются через точку с запятой. Атрибут expires задает конкретную дату удаления куки в формате UTC, а max-age определяет время жизни куки в секундах с момента создания, что часто бывает удобнее. Параметры path и domain определяют область видимости куки, ограничивая ее конкретным путем на сайте или определенным поддоменом.

Существуют и важные ограничения безопасности. Так, флаг HttpOnly делает куки недоступными из JavaScript, защищая их от кражи через XSS-атаки, поскольку такие куки могут передаваться только сервером в заголовках HTTP. Кроме того, накладывается жесткое техническое ограничение: размер одной куки вместе с именем и всеми атрибутами не может превышать 4 килобайта, а общее количество кук для одного домена также ограничено браузерами. Примером практического использования может служить сохранение выбранной пользователем темной темы оформления сайта, которая считывается при повторном визите, обеспечивая персонализированный и комфортный пользовательский опыт.

QUESTION: Как работать с history API?

Working with the History API in JavaScript allows web developers to programmatically manage the browser's session history, which is the contents of the history stack of the current tab. This is critically important for creating modern single-page applications (SPAs), where transitions between sections occur without a full page reload, while the user still expects familiar interface behavior with working browser forward and back buttons.

The primary method for changing the history without a reload is history.pushState(state, title, url). This method accepts three arguments: the state object, which contains data bound to that state; the page title, which is ignored by most modern browsers; and the new URL, which is displayed in the browser's address bar. If you need to modify the current entry rather than add a new one, the history.replaceState() method is used, which overwrites the current state and address.

For navigating through history in code, the API provides history.back(), which takes the user back one page, history.forward(), which moves forward, and the universal history.go(delta) method, where the numeric argument specifies how many steps forward or backward to shift in the stack. When the user clicks the browser's navigation buttons or when the go, back, or forward methods are called, the popstate event fires on the window object. In the handler for this event, the developer can retrieve the current state via event.state and redraw the application interface to match the loaded section.

QUESTION: Что такое Intersection Observer?

The Intersection Observer API in JavaScript is a powerful and efficient tool designed for asynchronously observing the intersection of a target element with its parent container or with the browser's own viewport. The main advantage of this approach is that the code runs not on every mouse movement or scroll pixel, but only at moments when the visibility state changes, which dramatically reduces CPU load and improves web page performance.

To create an observer, the new IntersectionObserver(callback, options) constructor is used, where the callback function fires upon each intersection, and the options object allows you to configure the root element, margins, and thresholds. After creating the observer, you need to start tracking a specific DOM node using the observer.observe(element) method, passing the desired element to it.

Classic examples of the practical application of Intersection Observer are two common tasks: lazy loading of images and infinite scrolling. In the first case, an image starts loading from the server only when the user approaches it during scrolling. In the second case, a special marker element at the very bottom of the page triggers the loading of the next portion of content right at the moment it enters the user's field of view, creating a smooth and seamless interaction experience.

QUESTION: Что такое Mutation Observer?

The Mutation Observer API in JavaScript is a built-in mechanism that allows you to asynchronously track any changes occurring in the Document Object Model, i.e., the DOM structure. This tool replaced obsolete mutation events and works much more efficiently because it batches all changes and invokes the callback function once per rendering cycle.

Creating an observer is done using the new MutationObserver(callback) constructor, which receives a function that accepts an array of mutation records and the observer instance itself. To start tracking a specific node, the observer.observe(target, config) method is used, where target is the target DOM element and config is a configuration object that defines the types of changes to monitor.

In the configuration object, you can specify the childList parameter to track the addition or removal of child elements, attributes to monitor attribute changes, and characterData to track changes to the text content of nodes. In addition, if you need to monitor not only direct descendants but the entire nested tree structure of elements inside the target node, the subtree boolean flag is used with a value of true. This is indispensable when creating complex user interfaces, automatic text translation plugins, or testing systems.

QUESTION: Что такое Resize Observer?

The Resize Observer API in JavaScript is a modern standard for tracking changes to the sizes of elements on a web page. Before this API appeared, developers had to attach heavy and inefficient handlers to the window resize event, even though the size of absolutely any block could change independently of the browser window, for example, when opening drop-down menus or dynamically loading content.

Creating a size observer is implemented via the new ResizeObserver(callback) constructor. Inside the callback function, the developer receives an array of change objects. Each such object contains the entry.contentRect property, which stores up-to-date numeric data on the width, height, and coordinates of the element, and these measurements take internal padding into account.

To start the observation process, simply call the observer.observe(element, options) method and pass the target DOM node to it. This approach works exceptionally smoothly and efficiently because size changes are processed asynchronously in the browser's special rendering loop. This is an ideal solution for creating responsive interface components, adaptive canvas charts, or complex tables that need to instantly rebuild their internal layout when the dimensions of the parent container change.

QUESTION: Что такое ES модули?

The modern JavaScript ecosystem is based on modularity standards, among which ES modules hold a special place. This is the official standard for organizing and reusing code in JavaScript, supported by both modern browsers and the Node.js environment. The main feature of this approach is that each file is treated as a separate, isolated module with its own scope. To share functionality with other parts of the application, developers use various types of exports. For example, named export allows exporting multiple variables or functions from a single file using a construct like export const x = 1, while default export uses the export default fn syntax, which is typically used for the main component or function of a module.

In turn, to include created modules in other parts of the program, import mechanisms are used. To get specific named items, destructuring is used at the time of import, for example, import {x} from './module'. If there is a need to import all of a module's functionality at once as a single object, star import is used: import * as M from './module', after which all exported entities become available as properties of the M object. A crucial characteristic of ES modules is static imports. This means that the dependency structure between files is determined and analyzed by the compiler even before the code itself executes, at the parsing stage. This approach allows build tools and browsers to understand in advance which files will be needed for the application to work.

For the browser to correctly process such modules when loading an HTML page directly, you must explicitly specify the script type in the tag. The type='module' attribute is used for this in HTML, for example, in the form of the script type='module' src='main.js' /script construct. Thanks to this, the browser enables strict module mode, automatically processes them as asynchronous scripts, and allows using all the advantages of modern import and export syntax directly on the client side of the web application without prior bundling.

QUESTION: Что такое динамический импорт?

Динамический импорт в JavaScript представляет собой мощный механизм загрузки модулей в тот момент, когда это действительно необходимо, непосредственно во время выполнения программы. В отличие от статического импорта, который фиксируется на этапе компиляции, динамический импорт реализуется в виде функции, принимающей путь к модулю в качестве аргумента. Существует два основных синтаксических подхода для работы с ним. Первый вариант предполагает использование классических промисов: import('./module').then(module => { логика работы с модулем }). Второй, более современный и удобный способ, основан на синтаксисе async/await, когда код выглядит следующим образом: const module = await import('./module'). Такой подход делает асинхронный код линейным и более читаемым, избавляя разработчика от необходимости выстраивать цепочки из методов then.

Главная практическая польза динамического импорта заключается в реализации разделения кода на части, известного как code splitting. Когда веб-приложение разрастается до больших размеров, отправка всего исходного кода пользователю одним огромным файлом приводит к серьезным задержкам при первой загрузке страницы. Динамический импорт позволяет разделить приложение на логические блоки и загружать их только тогда, когда пользователь переходит к соответствующему разделу или выполняет определенное действие.

С этим напрямую связана концепция ленивой загрузки, или lazy loading. Например, тяжелые библиотеки для работы с графикой, сложные модальные окна или редкие разделы админ-панели не нужно загружать при старте. Их можно подгрузить «на лету» в момент клика пользователя. Поскольку функция динамического импорта всегда возвращает Promise, приложение может легко обрабатывать состояние загрузки, показывать пользователю анимацию ожидания или спиннер, а в случае сетевой ошибки — перехватывать исключение через блок try/catch или метод catch. Это делает современные веб-приложения гораздо более отзывчивыми и производительными на любых устройствах, включая мобильные телефоны с медленным интернетом.

What is dynamic import?

Dynamic import in JavaScript is a powerful mechanism for loading modules right when they are actually needed, directly at runtime. Unlike static import, which is fixed at compile time, dynamic import is implemented as a function that takes the path to the module as an argument. There are two main syntactic approaches for working with it. The first option involves using classic promises: import('./module').then(module => { module logic }). The second, more modern and convenient way is based on the async/await syntax, where the code looks like this: const module = await import('./module'). This approach makes asynchronous code linear and more readable, saving the developer from having to build chains of then methods.

The main practical benefit of dynamic import lies in implementing code splitting. When a web application grows to a large scale, sending the entire source code to the user as one huge file leads to serious delays during the initial page load. Dynamic import allows you to split the application into logical blocks and load them only when the user navigates to the corresponding section or performs a specific action.

Directly related to this is the concept of lazy loading. For example, heavy graphics libraries, complex modal windows, or rare admin panel sections do not need to be loaded at startup. They can be loaded "on the fly" at the moment of a user click. Since the dynamic import function always returns a Promise, the application can easily handle the loading state, show a waiting animation or spinner to the user, and catch exceptions via a try/catch block or the catch method in case of a network error. This makes modern web applications much more responsive and performant on any device, including mobile phones with slow internet connections.

[ITEM 2] QUESTION: Чем отличаются CommonJS и ES модули?

В мире JavaScript исторически сложилось сосуществование двух основных систем модулей: CommonJS и ES модулей. Понимание их различий критически важно для разработчиков, так как они имеют принципиально разную архитектуру и область применения. Система CommonJS была создана на заре развития Node.js для серверного JavaScript. В ней для экспорта данных используется объект module.exports, а для подключения зависимостей применяется функция require(). Типичный пример такого кода выглядит как const module = require('./module'). Главная технологическая особенность CommonJS заключается в том, что она является синхронной. Это означает, что при вызове функции require файл полностью считывается и выполняется синхронно в момент вызова, что отлично работало на сервере, где все файлы хранятся на локальном жестком диске.

С другой стороны, ES модули представляют собой современный официальный стандарт языка JavaScript, который использует ключевые слова import и export. В отличие от предшественника, ES модули являются асинхронными по своей природе. Стандарт разработан с учетом особенностей работы в браузере, где файлы модулей могут загружаться по сети с разной задержкой. Поэтому перед выполнением кода браузер или среда выполнения сначала анализируют граф зависимостей, загружают все необходимые модули и только затем запускают их выполнение.

Долгое время CommonJS был стандартом исключительно для бэкенда на Node.js, в то время как ES модули внедрялись в браузеры и инструменты сборки. Однако современная экосистема шагнула вперед: сегодня Node.js успешно поддерживает оба формата. Разработчики могут использовать ES модули даже на сервере, указав в файле package.json поле "type": "module", либо используя расширение файла .mjs для файлов с новым синтаксисом и .cjs для традиционного CommonJS. Тем не менее, миграция индустрии в сторону ES модулей продолжается, так как они предоставляют лучшие возможности для статического анализа кода и оптимизации.

What is the difference between CommonJS and ES modules?

In the world of JavaScript, two main module systems have historically coexisted: CommonJS and ES modules. Understanding their differences is critically important for developers, as they have fundamentally different architectures and use cases. The CommonJS system was created at the dawn of Node.js for server-side JavaScript. It uses the module.exports object to export data and the require() function to import dependencies. A typical example of such code looks like const module = require('./module'). The main technological feature of CommonJS is that it is synchronous. This means that when the require function is called, the file is fully read and executed synchronously at the moment of the call, which worked great on the server where all files are stored on a local hard drive.

On the other hand, ES modules represent the modern official JavaScript language standard, which uses the import and export keywords. Unlike its predecessor, ES modules are asynchronous by nature. The standard was designed taking into account the specifics of working in the browser, where module files can be loaded over the network with varying latency. Therefore, before executing the code, the browser or runtime environment first analyzes the dependency graph, loads all necessary modules, and only then starts their execution.

For a long time, CommonJS was the standard exclusively for Node.js backends, while ES modules were being adopted in browsers and build tools. However, the modern ecosystem has moved forward: today Node.js successfully supports both formats. Developers can use ES modules even on the server by specifying the "type": "module" field in the package.json file, or by using the .mjs file extension for files with the new syntax and .cjs for traditional CommonJS. Nevertheless, the industry's migration toward ES modules continues, as they provide better opportunities for static code analysis and optimization.

[ITEM 3] QUESTION: Что такое bundler?

Современные веб-приложения состоят из сотен, а иногда и тысяч отдельных модулей, файлов стилей, изображений и сторонних библиотек. Если передавать все эти файлы в исходном виде в браузер пользователя, это приведет к сотням HTTP-запросов и критическому падению производительности. Для решения этой проблемы используется специальный инструмент — сборщик, или bundler. Его главная задача заключается в том, чтобы объединять разрозненные модули в один или несколько оптимизированных файлов, готовых к продуктивной работе в браузере. Популярными примерами таких инструментов на сегодняшний день являются Webpack, Vite, Rollup и современный сверхбыстрый сборщик esbuild.

Процесс работы сборщика не ограничивается простым склеиванием файлов в один большой документ. В современных инструментах заложены мощные механизмы оптимизации. Один из них — это tree shaking, процесс интеллектуального удаления неиспользуемого кода, который позволяет исключить из финальной сборки те функции и переменные, которые были импортированы из библиотек, но фактически не вызываются в проекте. Другим важным инструментом является code splitting, или разделение кода. Сборщик автоматически или по указанию разработчика разбивает монолитный бандл на несколько чанков, которые могут загружаться параллельно или лениво по мере необходимости.

Кроме того, bundler выполняет минификацию кода. Этот процесс включает в себя удаление всех комментариев, лишних пробелов, сокращение длинных имен переменных и функций до однобуквенных аналогов без изменения логики работы приложения. В результате размер итогового бандла уменьшается в разы, что напрямую влияет на скорость скачивания ресурсов браузером и общую производительность веб-приложения. Сборщики также часто интегрируются с транспайлерами вроде Babel, превращая современный код JavaScript в версии, понятные для старых браузеров, и обрабатывают предварительные процессоры стилей, такие как Sass или Less.

QUESTION: What is transpilation?

The JavaScript world is evolving at an incredible pace, and language standards are updated almost annually, adding new syntactic constructs, methods, and features. However, users often run outdated browsers that do not understand modern syntax, leading to critical errors on websites. Transpilation is the process of converting new JS into old JS, ensuring full compatibility and stable application performance on any device. Transpilers analyze modern code (such as ES6+ syntax with arrow functions, classes, and destructuring) and rewrite it into standard ES5, which is supported even by the most archaic browsers.

The most famous and popular transpilation standard in the web development world is Babel. It allows developers to use the most advanced language features today without waiting for all users to update their browsers. In addition, TypeScript also transpiles code: this popular strictly typed language is not executed directly by the browser; its compiler (tsc) first converts types and modern syntax into plain, clean JavaScript. It is important to understand that transpilation modifies the code syntax itself, but it does not add missing global objects or methods—such as Promise or array methods—to older browsers. To solve this problem, transpilation often goes hand in hand with using polyfills for new APIs, creating a reliable foundation for cross-browser development.

QUESTION: What is a polyfill?

Even after successful code transpilation, older browsers may face the issue of lacking support for certain global objects, methods, or functions introduced in newer language specifications. This is where a polyfill comes to the rescue—it is a special piece of code that adds missing functionality to an environment that does not natively support it. If an older browser does not know what promises or the includes array method are, the polyfill checks for the existence of this function and, if it is missing, implements it in a standard way. Thus, your code continues to work equally well in both the most modern mobile browser and an outdated desktop system.

Historically, developers used various libraries to include missing features, but today the industry standard is the core-js library, which contains a comprehensive set of polyfills for virtually any modern API. Clear examples of the need for such solutions are Promise objects for working with asynchronous code and the fetch function for making network requests in legacy environments. Previously, the @babel/polyfill package was actively used to automate this process, but today it is officially deprecated and no longer recommended for use. Instead, the modern ecosystem offers flexible configuration of core-js combined with the Babel compiler, allowing you to automatically include only those polyfills that are truly necessary for your project's target browsers, significantly saving on total download traffic.

QUESTION: What is npm and package.json?

Any modern JavaScript project consists not only of your own code, but also of numerous third-party libraries, frameworks, and utility tools. To manage this complex ecosystem, npm is used—the official Node.js package manager, which allows you to download, update, and remove necessary dependencies with a single command. The center of every project is the package.json file, which is a project configuration in JSON format. It contains all metadata about the application, including its name, version, author, as well as lists of required libraries for the application to run and for development.

Dependencies in this file are strictly divided into categories. The dependencies field lists libraries that are vital for the application to run in production (such as React or date-handling libraries), while devDependencies specifies tools needed exclusively at the development and build stages (such as Webpack, ESLint, or TypeScript). Additionally, the scripts section plays an important role in package.json, containing custom commands for running the project, building, testing, or linting code, allowing you to execute complex command chains with a simple shortcut like npm run build. To ensure stability and reproducibility of the build on any computer or server, a package-lock.json file is created, which locks the exact versions of all installed packages and their sub-dependencies, guaranteeing that you and your coworker will run an absolutely identical set of code on your devices.

QUESTION: What is yarn and pnpm?

In the software development world, the constant search for faster and more efficient tools has led to the creation of alternatives to the standard package manager. Yarn is a popular alternative to npm from Facebook, originally created to solve issues with installation speed and dependency security by offering parallel downloading and strict version determinism through its own yarn.lock file. In turn, pnpm represents an even more innovative approach to dependency management, using hard links and symbolic links from a global storage at the operating system level, ensuring unprecedentedly efficient file storage and saving gigabytes of free disk space.

Both of these tools have earned the trust of developers because they work significantly faster and more reliably than traditional npm, especially in large monorepos or projects with hundreds of dependencies. To lock versions, they use their own lock files: yarn.lock for Yarn and pnpm-lock.yaml for pnpm, which perform the same function as package-lock.json, guaranteeing an identical environment for all team members. At the same time, all these alternative managers are fully compatible with the npm registry, meaning you can download, use, and publish any packages from the official public JavaScript repository without any issues, simply by switching to using the new command for installing dependencies in your project.

QUESTION: Что такое memory leaks?

Memory leaks in JavaScript represent a situation where an application continues to hold objects in RAM that are no longer needed for its operation. Over time, this leads to serious performance degradation, interface freezes, and even complete browser tab crashes.

The main reason lies in unreleased memory, when the built-in garbage collector physically cannot delete an object because active references to it still exist. A frequent cause of such problems is accidental or intentional global variables that are declared without the var, let, or const keywords, or are bound to the window object in non-strict mode. Because of this, such data lives throughout the entire lifecycle of the page.

Another common source of leaks is forgotten timers and event listeners. If you used functions like setInterval or added addEventListener to some DOM element, and then removed the element itself from the page, but forgot to call clearInterval or removeEventListener, then the callback function and the closed-over variables associated with it will remain in memory forever.

Developers should pay special attention to closures that capture DOM elements. If an inner function references a UI element, that element will not be removed from memory even after it disappears from the document tree, because the execution context continues to hold a reference to it.

For effective detection and elimination of such problems, professional developers use specialized built-in tools. A great example is the Chrome DevTools Memory panel, where you can take heap snapshots, compare their state before and after certain user actions, and find detached DOM nodes or uncleared data structures remaining in memory.

QUESTION: What is garbage collection?

Garbage collection in JavaScript is an automated memory management mechanism. It completely relieves the programmer from the need to manually allocate and free bytes in RAM, as is done in low-level languages like C or C++.

The main algorithm used by modern engines for these purposes is called Mark-and-sweep. This process runs cyclically in the background.

The garbage collector starts traversing the object graph from root elements, such as the global scope object window or global, and marks all available and reachable objects as active and needed. All those entities that cannot be reached via a chain of references from the roots are considered obsolete and are removed from memory.

For fine-tuning memory management and preventing leaks in complex applications, developers use special data structures such as WeakMap and WeakSet. They store weak references to objects, which allows for flexible management of data lifecycles.

If an object in a WeakMap no longer has other strong references in the code, it is automatically removed by the garbage collector, even though it is present in that map. It is important to understand that this process is fully automated, and the programmer cannot force it to run manually from the script code, although some execution environments do provide hidden flags for debugging.

QUESTION: What are debounce and throttle?

The debounce and throttle techniques are fundamental performance optimization methods in JavaScript. They allow you to effectively control the frequency of function calls when frequent events occur, such as page scrolling, window resizing, or user text input.

The debounce method works on the principle of delayed execution. It guarantees that the function will be called only after the stream of events stops for a certain pause specified by the developer.

If a new event occurs during this pause, the timer resets from the beginning. This is ideal for autocomplete search bars or handling the completion of browser window resizing, when there is no point in performing heavy computations on every single pixel of change.

In turn, the throttle method solves the opposite problem. It limits the execution of a function so that it is called no more than once in a given time interval, for example, exactly once every one hundred milliseconds.

This is indispensable for events such as page scrolling scroll or mouse movement mousemove, when smooth interface response is needed, but a continuous stream of calls would clog the main processor thread. Implementing these patterns from scratch every time is not required, as professional developers usually use ready-made, proven solutions from popular libraries such as lodash.debounce and lodash.throttle.

QUESTION: What is currying?

Currying in JavaScript is the process of transforming a function with multiple arguments, taking the form f(a, b, c), into a chain of functions, each of which takes exactly one argument. The result is a construction of the form f(a)(b)(c).

This powerful approach is actively used in functional programming to create more flexible and reusable code. In addition, it is indispensable for implementing partial application of arguments, when some parameters are fixed in advance and the remaining ones are passed later.

A classic example in modern syntax is the arrow function const add = a => b => a + b. In this case, calling add(

will return a new function that waits for the next argument b, and calling add(5)(
will return the final result of the addition.

This approach allows creating specialized functions based on universal templates. For example, a logging function can be curried so that it first accepts the message severity level and then the text itself, which simplifies passing ready-made loggers to various application modules.

Furthermore, currying opens up wide possibilities for function composition, where the output data of one operation is smoothly passed to the input of another. This makes the code declarative, clean, easy to test, and readable within large enterprise projects.

QUESTION: What is immutability?

Immutability in JavaScript is a programming concept according to which a created object, array, or primitive cannot be modified after its creation. If a developer needs to update some data in such an object, they do not mutate the initial state directly.

Instead, an absolutely new object is created, into which the old properties are copied and only those fields that require changes are overwritten. In practice, this is implemented using the spread operator.

For this, a simple syntax like const newObj = { ...obj, key: newVal } is used, which allows assembling a new object in a fraction of a second. At the same time, the old object is preserved unchanged for state history or debugging.

Manually creating deep copies of complex data structures can be tedious and error-prone, so developers often use specialized libraries for convenience. A great example is the Immer library, which allows writing code as if you are modifying data directly, but creates immutable copies under the hood.

Adhering to the principle of immutability is critically important for modern state management libraries and frameworks, such as React and Redux. It is data immutability that allows components to instantly and without unnecessary overhead determine the fact of a state change by comparing object references, which directly affects rendering speed and user interface stability.

QUESTION: Что такое Web Components?

The Web Components technology is a powerful set of standardized web platform APIs that allow you to create new, custom, encapsulated, and reusable tags for use in web applications and on web pages without the need to connect third-party heavyweight libraries or frameworks like React, Vue, or Angular. This technology radically changes the approach to user interface development, making components truly independent and portable between projects.

The foundational basis of Web Components is built on several key specifications. The first of these is Custom Elements, which give developers the ability to define their own tags and fully control their lifecycle, including creation, addition to the DOM, removal, and attribute updates. The second most important component is Shadow DOM, which provides style and markup isolation. Thanks to it, the internal styles of a component will never leak outside and ruin the global design of the site, and external styles will not be able to accidentally break the layout inside the element. The third element is HTML Templates, represented by the template and slot tags, which allow defining immutable markup templates that are cloned as needed.

In practice, creating such an element looks like this: the developer creates a class extending HTMLElement, in the constructor method of which they configure the Shadow DOM and add markup to it. Then the customElements.define() method is used, linking the name of the new tag, for example my-element, with the created class. After this, the given tag can be freely used in a regular HTML document just like standard div or span elements, which ensures maximum flexibility and versatility of modern web development.

QUESTION: What is the Canvas API?

The Canvas API is one of the most powerful tools in modern web development, built into HTML5, which gives developers the ability to programmatically draw 2D graphics directly in the browser using JavaScript. This programming interface opens up enormous opportunities for creating interactive graphic editors, charts, data visualizations, engaging browser games, and complex dynamic interfaces without using external graphic editors or plugins.

To start working with graphics on a page, an HTML canvas element is created, after which a special rendering context is obtained in the script by calling the canvas.getContext('2d') method. This very context object becomes the main tool for all further manipulations. The developer gets at their disposal a rich set of methods to create visual images. For example, the fillRect and strokeRect functions allow you to instantly draw filled or outlined rectangles with specified coordinates and dimensions, while the arc method is used to create circles, arcs, and complex rounded UI elements.

In addition to geometric primitives, the Canvas API handles raster images exceptionally well. The drawImage method makes it possible to load pictures, photos, and sprites onto the canvas, scale them, and cut out individual fragments for frame-by-frame animation. To create smooth, continuous animations with a high frame rate, the requestAnimationFrame function is used, which synchronizes the rendering process with the refresh rate of the monitor or mobile device screen. This guarantees maximum performance, no lag, and smoothness of moving objects, making the interface responsive and professional.

QUESTION: What is WebGL?

WebGL technology is a low-level programming interface for JavaScript that allows for high-performance interactive 3D and 2D graphics rendering directly within the window of any modern web browser. The main advantage of WebGL is its ability to leverage hardware acceleration from the graphics processor of a computer or mobile device, which provides colossal performance and allows for processing complex polygonal meshes, millions of vertices, and heavy visual effects in real time.

To start working with this technology, a developer creates a standard canvas element on the page and then requests a graphics context from it by calling the canvas.getContext('webgl') method or its more modern version, webgl2. Unlike the familiar 2D context, WebGL requires a fundamentally different approach to programming. The basis of its operation is shaders — special mini-programs that are compiled directly on the graphics card and written in a specialized programming language called GLSL. Vertex shaders are responsible for transforming the coordinates of three-dimensional points into 2D screen projections, while pixel or fragment shaders determine the exact color of each individual pixel taking into account lighting, shadows, and textures.

Since writing pure WebGL code requires deep knowledge of mathematics, linear algebra, and memory buffer management, developers often use popular wrapper libraries like Three.js in practice. This library significantly simplifies the work: it takes care of the routine of creating scenes, cameras, light sources, and loading 3D models, turning a complex low-level process into concise and understandable JavaScript code. Due to this, WebGL is actively used to create interactive 3D tours, product presentations, virtual museums, and cutting-edge web games.

QUESTION: What is the Web Audio API?

The Web Audio API is a powerful, flexible, and high-performance system for complex audio processing, synthesis, and playback directly in the web browser using JavaScript. This interface was created to go beyond the standard HTML audio tag, giving developers full control over audio signals, the ability to create spatial sound effects, dynamic mixing, and real-time music generation on the fly.

At the core of the Web Audio API architecture is the concept of an audio graph consisting of interconnected nodes. All sound manipulations occur inside a special AudioContext object, which manages the state and timing of all audio operations. Sound sources, such as audio files, a microphone, or built-in waveform generators (Oscillator), are connected to a chain of processing nodes. For example, the Gain node is responsible for adjusting the volume and signal decay, while various Filter nodes allow you to cut out specific frequency ranges, creating muffled sound or echo effects.

One of the most important features of the API is the availability of tools for deep real-time audio stream analysis, such as AnalyserNode. With its help, you can read frequency characteristics and sound amplitude, making it possible to create beautiful music visualizers that react to bass and rhythm. In addition, the Web Audio API supports spatial sound (PannerNode), which simulates a three-dimensional acoustic environment. By changing the coordinates of the listener and the sound source, you can achieve a realistic effect of sound moving around the user, which is critical for modern browser 3D games and VR applications.

QUESTION: What are the new features in ES2023+?

The ECMAScript standard from 2023 onwards has brought many long-awaited and useful features to JavaScript, significantly improving the daily work of developers, increasing code cleanliness and application performance. These innovations affected working with arrays, asynchronous operations, security, and syntax, making the language an even more expressive and powerful tool for creating large-scale software products.

A significant step forward was the improvement of working with arrays, in particular the addition of immutable methods Array.prototype.toSorted, toReversed, and the with() method. Previously, the standard sort() and reverse() methods mutated the original array, which often led to hard-to-find bugs in applications with architectures like Redux. The new methods return a new array with the changes already applied, leaving the original intact. The with(index, value) method allows you to safely replace an element at a specific index while also creating a copy of the array. Another wonderful addition is the Array.fromAsync() method, which significantly simplifies the creation of arrays from asynchronous iterators or promises.

In addition to arrays, the language has been enriched with convenient syntactic features. Support for Hashbang grammar at the beginning of script files has appeared, allowing JS files to be run directly in the terminal as executable scripts in Unix-like systems. The possibilities for using Symbol objects as keys in WeakMap collections have also expanded, opening up new patterns for creating private properties and efficient memory management without the risk of leaks. All these changes demonstrate the language creators' desire to make JavaScript as convenient, secure, and ready for solving the most complex tasks of modern development as possible.