Mowgle88 / coreJS-interview-QA

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JavaScript Core

JavaScript

Parsing (Парсинг), Reflow (Пересчёт макета), и Repaint (Перерисовка)

Parsing (Парсинг), Reflow (Пересчёт макета), и Repaint (Перерисовка) являются ключевыми этапами рендеринга веб-страниц, которые браузер выполняет для отображения контента на экране. Давайте рассмотрим каждый из этих этапов подробнее:

Parsing (Парсинг)

Парсинг — это процесс анализа и преобразования входного кода (HTML, CSS, JavaScript) в удобные для работы структуры данных. В контексте веб-браузеров это обычно означает преобразование HTML в DOM (Document Object Model), CSS в CSSOM (CSS Object Model) и выполнение JavaScript-кода.

  • HTML-парсинг приводит к построению DOM-дерева. Браузер читает HTML-документ сверху вниз и создаёт дерево, где каждый тег становится узлом.
  • CSS-парсинг создаёт CSSOM на основе стилей, применяемых к странице. CSSOM используется вместе с DOM для создания render tree (дерева рендеринга), которое содержит только те узлы, которые должны быть отображены, и их стили.

Reflow (Пересчёт макета)

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

Repaint (Перерисовка)

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

Как минимизировать влияние на производительность

  • Избегайте частых изменений стилей, которые вызывают reflow и repaint. Особенно избегайте их в циклах.
  • Используйте классы для изменения стилей вместо прямого изменения стилей через JavaScript.
  • Применяйте техники, такие как виртуализация списка, для уменьшения количества элементов в DOM.
  • Используйте CSS-свойства, которые не вызывают reflow, для анимаций (например, transform и opacity).

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

Циклы while и for

Цикл while используется для выполнения блока кода, пока условие оценивается как true. Синтаксис выглядит следующим образом:

while (условие) {
  // блок кода, который будет выполнен
}

Пример:

let i = 0;
while (i < 5) {
  console.log(i);
  i++;
}

Этот цикл будет выводить числа от 0 до 4 в консоль.

Цикл for чаще используется, когда известно количество итераций. Синтаксис:

for (начало; условие; шаг) {
  // блок кода, который будет выполнен
}

Пример:

for (let i = 0; i < 5; i++) {
  console.log(i);
}

Аналогично предыдущему примеру, этот цикл выводит числа от 0 до 4.

Условие if

Оператор if используется для выполнения блока кода, если заданное условие истинно.

if (условие) {
  // блок кода, который будет выполнен, если условие истинно
}

Пример:

let num = 10;
if (num > 5) {
  console.log("Число больше 5.");
}

Конструкция switch

Оператор switch позволяет выполнять разные блоки кода в зависимости от условия.

switch (выражение) {
  case x:
    // блок кода
    break;
  case y:
    // блок кода
    break;
  default:
    // блок кода
}

Пример:

let fruit = "apple";
switch (fruit) {
  case "banana":
    console.log("Hello, banana!");
    break;
  case "apple":
    console.log("Hello, apple!");
    break;
  default:
    console.log("Unknown fruit.");
}

Логические операторы

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

  • && (И) - возвращает true, если оба операнда истинны.
  • || (ИЛИ) - возвращает true, если хотя бы один из операндов истинен.
  • ! (НЕ) - инвертирует значение операнда.

Примеры:

if (true && false) {
    console.log("Этот код не будет выполнен.");
}

if (true || false) {
    console.log("Этот код будет выполнен.");
}

if (!false) {
    console.log("Также этот код будет выполнен.");
}

Условные операторы

Условные операторы (? :) позволяют сделать условное присваивание значений.

const age = 19;
const status = (age >= 18) ? 'взрослый' : 'несовершеннолетний';
console.log(status); // Выведет "взрослый"

Alert/prompt/confirm

  • alert выводит сообщение и ждет, пока пользователь нажмет "ОК".
  • prompt выводит диалоговое окно с запросом на ввод текста.
  • confirm выводит диалоговое окно с запросом на подтверждение (ОК или Отмена).
alert("Привет, мир!"); // Показывает сообщение
const age = prompt("Сколько вам лет?", 18); // Запрашивает и возвращает введенное значение
const isAdult = confirm("Вам есть 18?"); // Возвращает true, если нажата ОК, иначе false

Различия между let, const и var в JavaScript

Область видимости (Scope) Одно из ключевых отличий между var, let и const заключается в их области видимости.

  • var объявляет переменную с функциональной областью видимости (function scope) или глобальной областью видимости, если объявлена вне функции.
  • let и const объявляют переменные с блочной областью видимости (block scope), что означает, что они доступны только в рамках блока, в котором были объявлены (например, внутри цикла или условного блока).

Пример с var:

if (true) {
  var a = 1;
}
console.log(a); // Выводит 1, потому что переменная a объявлена с помощью var и имеет функциональную область видимости.

Пример с let:

if (true) {
  let b = 2;
}
console.log(b); // Выдаст ошибку ReferenceError: b is not defined, потому что b имеет блочную область видимости.

Всплытие (Hoisting)

  • Переменные, объявленные с помощью var, "всплывают" в начало функции или глобального контекста, но инициализируются там, где были объявлены. Это значит, что их можно использовать до объявления в коде, но они будут undefined до момента инициализации.
  • let и const также всплывают, но остаются во временной мертвой зоне (TDZ, Temporal Dead Zone) до их объявления, что делает их недоступными до момента объявления в коде.

Пример всплытия с var:

console.log(x); // undefined
var x = 5;

Пример всплытия с let:

console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;

Изменяемость (Mutability)

  • let позволяет изменять значение переменной после её объявления.
  • const требует инициализации переменной при объявлении и не позволяет изменять её значение позже. Это делает const идеальным выбором для объявления констант.

Пример с let:

let c = 3;
c = 4; // Допустимо

Пример с const:

const d = 4;
d = 5; // TypeError: Assignment to constant variable.

Однако стоит отметить, что если const используется для объявления объекта или массива, то сам объект или массив может быть изменён (например, добавление или удаление элементов массива), но не сама переменная, указывающая на объект или массив.

const e = [];
e.push(1); // Допустимо
e = [1]; // TypeError: Assignment to constant variable.

Заключение

  • Используйте let для переменных, значение которых будет изменяться.
  • Используйте const для переменных, значение которых не предполагается изменять, а также для объявления объектов и массивов, которые не будут переназначены.
  • Избегайте var в современном JavaScript, поскольку let и const предоставляют более строгую и понятную область видимости.

Виды функций и их отличия, hoisting

В JavaScript функции могут быть созданы разными способами, каждый из которых имеет свои особенности, в том числе по поводу области видимости, возможности использования до объявления (из-за механизма всплытия, или hoisting), а также this. Давайте рассмотрим основные типы функций в JavaScript и их отличия, а также поговорим о всплытии для функциональных объявлений.

1. Функциональные объявления (Function Declarations)

Функциональное объявление, или объявление функции, определяет функцию с указанным именем.

function sum(a, b) {
  return a + b;
}

Особенности:

  • Всплытие (Hoisting): Функциональные объявления полностью всплывают, что позволяет вызывать функцию до её объявления в коде.
  • Область видимости: Такая функция доступна во всей области видимости, в которой была объявлена.

2. Функциональные выражения (Function Expressions)

Функциональное выражение создаёт функцию и присваивает её переменной. Функция может быть именованной или анонимной.

const sum = function(a, b) {
  return a + b;
};

Особенности:

  • Всплытие: Переменная, которой присвоено функциональное выражение, всплывает как обычная переменная. Это означает, что до выполнения строки с присваиванием функции переменная существует, но имеет значение undefined.
  • Область видимости: Зависит от используемого ключевого слова (var, let, const).

3. Стрелочные функции (Arrow Functions)

Стрелочные функции представляют собой компактный способ создания функций. Они не имеют собственного контекста this, arguments, super или new.target.

const sum = (a, b) => a + b;

Особенности:

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

Всплытие (Hoisting)

Функциональные объявления всплывают полностью. Это означает, что функцию можно вызвать до её объявления в коде:

console.log(sum(5, 3)); // Выведет 8

function sum(a, b) {
  return a + b;
}

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

console.log(sum(5, 3)); // TypeError: sum is not a function

const sum = function(a, b) {
  return a + b;
};

В случае использования var переменная будет всплывать и иметь значение undefined до выполнения присваивания:

console.log(sum); // undefined
console.log(sum(5, 3)); // TypeError: sum is not a function

var sum = function(a, b) {
  return a + b

;
};

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

Деструктуризация и оператор распространения

Деструктуризация (Destructuring Assignments)

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

Деструктуризация объектов:

const person = {
  name: 'Алексей',
  age: 30,
  position: 'Web Developer'
};

const { name, age } = person;
console.log(name); // Алексей
console.log(age); // 30

Деструктуризация массивов:

const numbers = [1, 2, 3, 4, 5];
const [first, second] = numbers;

console.log(first); // 1
console.log(second); // 2

Оператор распространения (Spread Operator)

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

Использование в массивах:

const first = [1, 2, 3];
const second = [4, 5, 6];

const combined = [...first, ...second];
console.log(combined); // [1, 2, 3, 4, 5, 6]

Использование в объектах:

const person = { name: 'Алексей', age: 30 };
const updatedPerson = { ...person, position: 'Web Developer' };

console.log(updatedPerson);
// { name: 'Алексей', age: 30, position: 'Web Developer' }

Опциональная цепочка (Optional Chaining) и Опциональный оператор (Optional Mark)

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

const user = {
  profile: {
    name: 'Алексей',
    details: {
      age: 30
    }
  }
};

console.log(user.profile?.name); // 'Алексей'
console.log(user.profile?.address?.city); // undefined без выброса ошибки

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

Опциональный вызов функции:

Если не уверены, существует ли функция, можно использовать ?.() для вызова.

const user = {
  greet() {
    console.log('Привет!');
  }
};

user.greet?.(); // Привет!
user.sayGoodbye?.(); // Ничего не произойдет, функция не вызовется и не выдаст ошибку

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

Работа с массивами

Map (Карта)

Метод map создаёт новый массив, результатом выполнения функции на каждом элементе исходного массива.

const numbers = [1, 2, 3, 4];
const squared = numbers.map(n => n * n);

console.log(squared); // [1, 4, 9, 16]

Filter (Фильтр)

Метод filter создаёт новый массив со всеми элементами, прошедшими проверку, задаваемую в передаваемой функции.

const numbers = [1, 2, 3, 4];
const evens = numbers.filter(n => n % 2 === 0);

console.log(evens); // [2, 4]

Reduce (Сокращение)

Метод reduce применяет функцию к аккумулятору и каждому элементу массива (слева-направо), сводя его к одному значению.

const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((accumulator, current) => accumulator + current, 0);

console.log(sum); // 10

Find (Найти)

Метод find возвращает первый элемент в массиве, который соответствует предоставленному тестовому условию. Если ничего не найдено, возвращает undefined.

const numbers = [1, 2, 3, 4];
const firstEven = numbers.find(n => n % 2 === 0);

console.log(firstEven); // 2

Includes (Включает)

Метод includes проверяет, содержит ли массив определённый элемент, и возвращает true или false соответственно.

const numbers = [1, 2, 3, 4];
const includesTwo = numbers.includes(2);

console.log(includesTwo); // true

Some (Некоторые)

Метод some проверяет, удовлетворяет ли хотя бы один элемент в массиве условию, заданному в передаваемой функции.

const numbers = [1, 2, 3, 4];
const hasOdd = numbers.some(n => n % 2 !== 0);

console.log(hasOdd); // true, потому что есть нечетные числа

Эти методы являются мощными инструментами для работы с массивами. Они делают код более чистым и понятным, избавляя от необходимости написания избыточных циклов for или while для обработки данных.

Строгий vs Нестрогий Сравнения

В JavaScript существует два типа сравнений: строгие (===, !==) и нестрогие (==, !=). Основное отличие между ними заключается в том, как они обрабатывают сравнение разных типов данных.

Нестрогие Сравнения (==, !=)

Нестрогие сравнения (==, !=) автоматически приводят типы перед сравнением, что может привести к неочевидным результатам.

console.log(1 == "1"); // true, число приводится к строке
console.log(0 == false); // true, 0 и false оба приводятся к false
console.log(null == undefined); // true, null и undefined считаются равными

Эти преобразования делают нестрогие сравнения менее предсказуемыми и потенциально более затруднительными в отладке.

Строгие Сравнения (===, !==)

Строгие сравнения (===, !==) проверяют равенство без приведения типа, т.е. значения считаются равными только если они одного типа и значения.

console.log(1 === "1"); // false, разные типы
console.log(0 === false); // false, разные типы
console.log(null === undefined); // false, разные типы

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

Всплытие (Hoisting)

Всплытие (hoisting) в JavaScript описывает поведение, при котором объявления переменных и функций перемещаются вверх их области видимости перед выполнением кода. Это важно понимать, поскольку влияет на доступность переменных и функций.

Всплытие переменных

Переменные, объявленные с помощью var, подвергаются всплытию, но их инициализация не всплывает.

console.log(x); // undefined, а не ReferenceError
var x = 5;

Переменные, объявленные через let и const, также всплывают, но они находятся во временной мертвой зоне (Temporal Dead Zone, TDZ) до тех пор, пока не достигнут своего объявления, что делает их недоступными до объявления.

console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;

Всплытие функций

Функциональные объявления (function declarations) полностью всплывают, что позволяет вызывать функцию до её объявления в коде.

console.log(sum(5, 3)); // 8
function sum(a, b) {
  return a + b;
}

Функциональные выражения (function expressions), назначенные переменным через var, let, или const, ведут себя как обычные переменные в отношении всплытия.

console.log(sum); // undefined при var, ReferenceError при let или const
var sum = function(a, b) {
  return a + b;
};

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

Шаблонные Строки (Template Strings)

Шаблонные строки в JavaScript позволяют вставлять выражения в строковые литералы, обеспечивая более удобное форматирование и создание строк. Они заключаются в обратные кавычки (` `) вместо обычных одинарных или двойных кавычек и могут содержать местозаполнители, обозначаемые знаком доллара и фигурными скобками (${expression}).

Пример:

const name = "Алексей";
const greeting = `Привет, ${name}!`;

console.log(greeting); // Выводит "Привет, Алексей!"

Шаблонные строки также поддерживают многострочный ввод, что делает их идеальными для создания больших блоков текста без необходимости использования символа переноса строки \n.

const multiLineText = `Это строка,
которая переносится
на несколько строк`;
console.log(multiLineText);

Стрелочные функции vs Обычные функции

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

Синтаксис

Стрелочная функция:

const sum = (a, b) => a + b;

Обычная функция:

function sum(a, b) {
  return a + b;
}

this в контексте функций

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

Возврат значения

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

Конструктор

Стрелочные функции не могут быть использованы как конструкторы, и попытка сделать это приведёт к ошибке. Обычные функции могут быть использованы в качестве конструкторов.

Аргументы

Стрелочные функции не имеют собственного объекта arguments, в отличие от обычных функций. Однако можно достичь того же эффекта с помощью оператора распространения (...args).

const func = (...args) => {
  console.log(args);
};
func(1, 2, 3); // Выведет [1, 2, 3]

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

IIFE

Немедленно вызываемое функциональное выражение (Immediately Invoked Function Expression, IIFE) — это шаблон в JavaScript, который позволяет определить анонимную функцию и немедленно вызвать её. Это полезно для создания новой области видимости, позволяя скрыть переменные и функции от глобальной области видимости и избежать конфликтов имен.

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

Синтаксис

(function() {
  // Код здесь изолирован от глобальной области видимости
})();

Или с параметрами:

(function(a, b) {
  console.log(a + b); // Выводит сумму a и b
})(10, 5);

Примеры

Базовый пример IIFE:

(function() {
  var localVar = "Я локальная переменная";
  console.log(localVar); // Выведет "Я локальная переменная"
})();

// Попытка доступа к localVar снаружи IIFE приведет к ошибке:
console.log(localVar); // Uncaught ReferenceError: localVar is not defined

IIFE с параметрами:

(function(greeting, name) {
  console.log(greeting + ", " + name + "!");
})("Привет", "Вася"); // Выведет "Привет, Вася!"

Использование IIFE для создания приватных переменных:

var myModule = (function() {
  var privateVar = "Я приватная переменная";
  
  function privateMethod() {
    console.log("Я приватный метод");
  }
  
  return {
    publicMethod: function() {
      console.log("Доступ к приватной переменной: " + privateVar);
      privateMethod();
    }
  };
})();

myModule.publicMethod(); // Доступ к приватной переменной: Я приватная переменная
                         // Я приватный метод
// Попытка напрямую обратиться к privateVar или privateMethod приведет к ошибке.

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

Параметры по умолчанию в функциях

В современном JavaScript (ES6 и выше) для функций доступна возможность задавать параметры по умолчанию. Это значит, что если при вызове функции аргумент не был предоставлен, будет использовано значение по умолчанию.

Синтаксис:

function имяФункции(параметр1 = значение1, параметр2 = значение2) {
  // тело функции
}

Пример:

function greet(name = "Гость") {
  console.log("Привет, " + name + "!");
}

greet("Анна"); // Выведет: Привет, Анна!
greet();       // Выведет: Привет, Гость!

Здесь, если greet вызвана без аргументов, name будет иметь значение "Гость".

Оператор расширения для аргументов функции (...)

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

Использование в функциях:

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

Пример:

function sum(x, y, z) {
  return x + y + z;
}

const numbers = [1, 2, 3];

console.log(sum(...numbers)); // Выведет: 6

В этом примере, ...numbers "расширяет" массив numbers, так что его элементы передаются в функцию sum как отдельные аргументы.

Использование для сбора всех аргументов:

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

function logAllArguments(...args) {
  console.log(args); // args будет массивом всех переданных аргументов
}

logAllArguments("Привет", "мир", "!");
// Выведет: ['Привет', 'мир', '!']

Здесь ...args создает массив args из всех аргументов, переданных функции logAllArguments.

Temporary Dead Zone

Temporary Dead Zone (TDZ) — это термин, используемый для описания области кода, где переменные, объявленные с помощью let или const, не могут быть использованы до того момента, как они объявлены. Это явление возникает из-за блочной области видимости (block scope) этих переменных. TDZ начинается от начала блока, в котором объявлена переменная, и продолжается до её объявления.

Важно понимать, что TDZ относится к переменным, объявленным через let и const, и не применим к переменным, объявленным через var, так как последние подвергаются всплытию и имеют функциональную область видимости или область видимости всего скрипта, если они объявлены вне функций.

Пример с let:

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 3;

В этом примере переменная a находится в TDZ от начала блока до момента её объявления через let. Попытка обратиться к переменной перед её объявлением приводит к ReferenceError.

Пример с const:

console.log(b); // ReferenceError: Cannot access 'b' before initialization
const b = 5;

Аналогично, переменная b, объявленная через const, находится в TDZ и не доступна до её объявления.

Значение TDZ

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

Использование "var" и поведение переменных без "var"

Использование "var"

В JavaScript var используется для объявления переменных с функциональной областью видимости (function scope) или глобальной областью видимости, если объявление произошло вне функции. Основные особенности переменных, объявленных через var:

  • Всплытие (Hoisting): Объявления переменных "всплывают" в начало функции или глобального контекста, но инициализация значения остается на месте объявления.
  • Переопределение: Переменные, объявленные через var, могут быть переопределены в пределах одной и той же области видимости.
  • Функциональная или глобальная область видимости: В отличие от let и const, которые имеют блочную область видимости, переменные var доступны во всей функции или глобально.

Поведение переменных без "var" (или "let", "const")

Когда переменная объявляется без использования var, let или const, она автоматически становится глобальной (если только не находится в строгом режиме "use strict", где такое действие приведет к ошибке). Это может привести к нежелательным последствиям, таким как загрязнение глобальной области видимости и конфликты имен переменных.

function foo() {
  x = 10; // без 'var', 'let', или 'const'
}
foo();
console.log(x); // 10 - переменная стала глобальной

Механизм обхода лексического окружения

Механизм обхода лексического окружения в JavaScript — это процесс, посредством которого движок JavaScript определяет, где и как искать переменные и функции в вашем коде. Лексическое окружение — это спецификация, которая описывает, как хранятся переменные и как они связаны с областями видимости во время выполнения кода.

Лексическое окружение

Каждая функция и блок кода (в режиме ES6 let и const в блоках {}) создает новое лексическое окружение. Лексическое окружение состоит из двух частей:

  1. Запись окружения (Environment Record): Где хранится информация о переменных и функциях, объявленных в области видимости.
  2. Ссылка на внешнее окружение (Outer Lexical Environment Reference): Которая указывает на лексическое окружение родительской области видимости. Это позволяет обеспечить доступ к переменным и функциям из внешнего окружения.

Механизм обхода

Когда в JavaScript необходимо получить доступ к переменной или функции, движок сначала ищет в текущем лексическом окружении. Если он не находит переменную в текущем окружении, он следует по ссылке на внешнее окружение и ищет там. Этот процесс продолжается до тех пор, пока не будет найдено соответствующее объявление или не будет достигнут самый верхний уровень (глобальное лексическое окружение). Если после достижения глобального окружения переменная так и не будет найдена, будет выброшена ошибка о том, что переменная не определена.

Пример

Рассмотрим следующий код для демонстрации механизма обхода лексического окружения:

var x = 10;

function findName() {
    console.log(x);
    var y = 20;
}

function findX() {
    var x = 30;
    findName();
}

findX(); // Выведет: 10

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

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

Что такое сборщик мусора? Зачем он нужен?

Сборщик мусора (Garbage Collector, GC) — это механизм управления памятью, который автоматически освобождает память, занятую объектами, до которых невозможно достучаться или которые больше не используются в программе. GC важен для следующих причин:

  • Автоматизация управления памятью: Программисту не нужно вручную управлять выделением и освобождением памяти, что снижает вероятность ошибок, таких как утечки памяти и двойное освобождение памяти.
  • Предотвращение утечек памяти: Несмотря на то что сборщик мусора не может предотвратить все типы утечек памяти, он значительно снижает риск их возникновения, автоматически освобождая память от объектов, которые больше не доступны.
  • Упрощение разработки: Управление памятью может быть сложным и подверженным ошибкам аспектом программирования. GC позволяет разработчикам сосредоточиться на логике приложения, не беспокоясь о деталях управления памятью.

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

Методы работы с объектами: Keys, Values, Entries, FromEntries

Object.keys(obj)

Object.keys(obj) возвращает массив из ключей собственных перечисляемых свойств переданного объекта.

const person = { name: 'Алексей', age: 30 };
console.log(Object.keys(person)); // ["name", "age"]

Object.values(obj)

Object.values(obj) возвращает массив из значений собственных перечисляемых свойств объекта.

console.log(Object.values(person)); // ["Алексей", 30]

Object.entries(obj)

Object.entries(obj) возвращает массив пар [ключ, значение] собственных перечисляемых свойств объекта.

console.log(Object.entries(person)); // [["name", "Алексей"], ["age", 30]]

Object.fromEntries(entries)

Object.fromEntries(entries) преобразует список пар [ключ, значение] в объект. Это обратная операция к Object.entries().

const entries = [["name", "Алексей"], ["age", 30]];
console.log(Object.fromEntries(entries)); // { name: "Алексей", age: 30 }

Методы работы с массивами: Flat, FlatMap, Includes, Array.from()

Array.prototype.flat([depth])

flat() создает новый массив, в котором все вложенные массивы рекурсивно "выпрямлены" до указанной глубины.

const arr = [1, [2, [3, [4]]]];
console.log(arr.flat()); // [1, 2, [3, [4]]]
console.log(arr.flat(2)); // [1, 2, 3, [4]]

Array.prototype.flatMap(callback)

flatMap() сначала применяет функцию к каждому элементу массива, а затем выпрямляет результат до глубины 1. Это комбинация map() и flat().

const arr = [1, 2, 3];
console.log(arr.flatMap(x => [x, x * 2])); // [1, 2, 2, 4, 3, 6]

Array.prototype.includes(valueToFind[, fromIndex])

includes() определяет, содержит ли массив определенный элемент, и возвращает true или false соответственно.

console.log(arr.includes(2)); // true
console.log(arr.includes(5)); // false

Array.from(arrayLike[, mapFn[, thisArg]])

Array.from() создает новый экземпляр массива из массивоподобного или итерируемого объекта. Может использоваться для преобразования строк, NodeList и других типов в массивы.

const str = '123';
console.log(Array.from(str, num => Number(num))); // [1, 2, 3]

Эти методы являются частью современного стандарта ECMAScript и предоставляют разработчикам более эффективные и удобные инструменты для работы с данными в JavaScript.

Типы данных. Примитивные типы и ссылочные (объектные) типы

В JavaScript существует две категории типов данных: примитивные типы и ссылочные типы (или типы-объекты).

Примитивные типы данных

Примитивные типы — это основные типы данных, которые представляют собой простые значения. К ним относятся:

  1. Number: для числовых значений (например, 123 или 3.14).
  2. String: для текстовых данных (например, "Привет, мир!").
  3. Boolean: для логических значений true и false.
  4. Undefined: тип для переменных, которым не было присвоено значение (например, объявленная переменная без значения).
  5. Null: представляет собой "ничто" или "пустое значение".
  6. Symbol (введен в ECMAScript 2015): уникальный и неизменяемый тип данных, используемый как ключ объекта.
  7. BigInt (введен в ECMAScript 2020): для представления целых чисел произвольной точности, которые превышают предел Number.

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

Ссылочные типы (Типы-объекты)

Ссылочные типы или объекты представляют собой более сложные структуры данных. К ним относятся:

  1. Object: базовый объект, от которого наследуются все остальные объекты.
  2. Array: для представления упорядоченных коллекций данных.
  3. Function: функции также являются объектами в JavaScript.
  4. Специализированные объекты, такие как Date, RegExp, и различные встроенные объекты-обертки для примитивных типов, как String, Number, и Boolean.

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

Основные различия между примитивными и ссылочными типами:

  • Хранение в памяти: Примитивы хранятся в стеке, что обеспечивает быстрый доступ к их значениям, в то время как ссылочные типы хранятся в куче, что может потребовать больше времени для доступа из-за необходимости разыменования ссылки.
  • Иммутабельность: Примитивные значения неизменяемы (например, когда вы изменяете строку, создается новая строка, а старая остается без изменений), тогда как объекты могут быть изменены после создания.
  • Сравнение: Примитивные значения сравниваются по их значению, тогда как ссылочные типы сравниваются по их ссылкам (адресу в памяти).
  • Копирование значений: Копирование примитивного значения в другую переменную создает копию этого значения, в то время как копирование ссылочного типа в другую переменную создает вторую ссылку на тот же объект.

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

Set, Map, WeakSet, и WeakMap в JavaScript

В JavaScript, Set, Map, WeakSet, и WeakMap — это специализированные коллекции данных, введенные в стандарте ES6. Каждая из этих структур данных имеет свои уникальные характеристики и используется для разных целей.

Set

Set — это коллекция уникальных значений любого типа, как примитивных, так и объектных. Основные особенности:

  • Каждое значение в Set может появляться только один раз; это обеспечивает уникальность хранимых значений.
  • Значения в Set не индексируются; в отличие от массивов, у вас нет прямого доступа к значению по индексу.
let mySet = new Set();

mySet.add(1);
mySet.add(5);
mySet.add(5); // Дубликаты не будут добавлены

console.log(mySet.has(1)); // true
console.log(mySet.size); // 2

mySet.delete(5);

Map

Map — это коллекция ключ/значение, где как ключи, так и значения могут быть любого типа. Отличия от обычного объекта JavaScript ({}) включают:

  • Ключи могут быть любого типа (в объектах JavaScript ключи преобразуются в строки).
  • Map сохраняет порядок вставки элементов, что может быть важно для определенных алгоритмов.
let myMap = new Map();

myMap.set('key1', 'value1');
myMap.set('key2', 'value2');

console.log(myMap.get('key1')); // "value1"
console.log(myMap.has('key2')); // true

myMap.delete('key1');

WeakSet

WeakSet — это коллекция, которая позволяет хранить только объекты в качестве уникальных значений. Основные отличия от Set:

  • Не предотвращает сборку мусора для своих элементов. Это значит, что если на объект в WeakSet нет других ссылок, он может быть удален сборщиком мусора.
  • Не поддерживает перебор своих элементов, что делает WeakSet менее предсказуемым для использования.
let ws = new WeakSet();
let obj = {};

ws.add(obj);

console.log(ws.has(obj)); // true

obj = null; // теперь obj может быть собран сборщиком мусора

WeakMap

WeakMap — это коллекция пар ключ/значение, где каждый ключ является объектом, а значение может быть любого типа. Аналогично WeakSet, ключи в WeakMap:

  • Не предотвращают сборку мусора, если на ключ не существует других ссылок.
  • Коллекция не поддерживает перебор или получение списка ключей/значений.
let wm = new WeakMap();
let key = {};

wm.set(key, 'value');

console.log(wm.get(key)); // "value"

key = null; // теперь ключ и значение могут быть собраны сборщиком мусора

Использование WeakSet и WeakMap полезно для оптимизации памяти и предотвращения утечек памяти в приложениях, где управление жизненным циклом объектов критично.

Преобразование Типов (Type Casting) и Принудительное Преобразование Типов (Type Conversion)

В JavaScript часто возникает необходимость в преобразовании типов — явном или неявном изменении данных из одного типа в другой. Вот несколько советов и примеров, как решать задачи, связанные с преобразованием типов:

Явное преобразование (Type Casting)

Вы можете явно преобразовывать типы, используя встроенные функции и конструкторы, такие как String(), Number(), и Boolean().

  • Преобразование в строку:
    String(123); // "123"
    (123).toString(); // "123"
  • Преобразование в число:
    Number("123"); // 123
    parseInt("123", 10); // 123, с указанием системы счисления
    parseFloat("123.45"); // 123.45
  • Преобразование в булев тип:
    Boolean(1); // true
    !!1; // true, используя двойное отрицание

Неявное преобразование (Type Coercion)

JavaScript автоматически преобразует типы при выполнении арифметических операций, сравнений или в логическом контексте.

  • Строковое преобразование происходит, когда вы используете оператор + с одним из операндов в виде строки:
    "number: " + 123 // "number: 123"
  • Численное преобразование происходит в математических операциях, кроме сложения, если один из операндов не является числом:
    "123" - 23 // 100
  • Логическое преобразование происходит в логическом контексте, например, в условных операторах:
    if ("123") { ... } // true, потому что непустая строка преобразуется в true

Советы по Решению Задач на Преобразование Типов

  1. Ожидайте неочевидное: JavaScript может вести себя неинтуитивно при неявном преобразовании типов, особенно при использовании == или при сложении строк и чисел. Лучше всегда использовать строгое сравнение (===) и явное преобразование типов.
  2. Проверяйте типы данных: Используйте typeof для проверки и отладки типов данных переменных, особенно когда результат операции не соответствует ожиданиям.
  3. Используйте явное преобразование: Чтобы избежать неожиданных результатов от неявного преобразования, предпочтительнее явно преобразовывать типы с помощью соответствующих функций и методов.

Понимание преобразования типов и умение контролировать этот процесс — ключевые навыки для успешного решения многих задач в JavaScript.

Использование "typeof"

Оператор typeof в JavaScript позволяет определить тип значения переменной или выражения. Это может быть полезно для проверки типов данных во время выполнения программы.

typeof "Hello, world!"; // "string"
typeof 123; // "number"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof {}; // "object"
typeof Symbol("id"); // "symbol"
typeof null; // "object" (это известная особенность JavaScript)
typeof function(){}; // "function"

Как работает прототипное наследование

В JavaScript каждый объект имеет специальное скрытое свойство [[Prototype]] (как правило, доступное как __proto__ или через методы Object.getPrototypeOf()/Object.setPrototypeOf()), которое ссылается на другой объект — его прототип. Когда вы пытаетесь получить доступ к свойству или методу объекта, и это свойство/метод в самом объекте отсутствует, JavaScript автоматически ищет его в прототипе. Если свойство/метод не найдено в прототипе, поиск продолжается по цепочке прототипов, пока не будет достигнут конец цепочки (обычно Object.prototype), после чего возвращается undefined.

Пример прототипного наследования

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

rabbit.walk(); // Animal walk
alert(rabbit.eats); // true

В этом примере объект rabbit наследует свойство eats и метод walk от объекта animal через прототипное наследование.

Зачем нужно прототипное наследование

  1. Повторное использование кода: Прототипное наследование позволяет объектам делиться свойствами и методами, что способствует повторному использованию кода и уменьшает дублирование.

  2. Экономия памяти: Объекты, наследующие свойства и методы через прототипы, используют меньше памяти, поскольку они не хранят собственные копии наследуемых свойств и методов, а используют общие, определённые в прототипе.

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

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

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

Цепочка прототипов

Цепочка прототипов (prototype chain) в JavaScript — это механизм, благодаря которому объекты могут наследовать свойства и методы друг от друга. Этот механизм основан на том, что каждый объект имеет ссылку на свой прототип, а прототип, в свою очередь, также может иметь ссылку на свой прототип, создавая тем самым "цепочку" прототипов. Эта цепочка продолжается до тех пор, пока не будет достигнут прототип, который не имеет собственного прототипа (например, Object.prototype), и в этом случае поиск свойства или метода завершается.

Как работает цепочка прототипов

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

Если после достижения конца цепочки свойство или метод так и не было найдено, операция возвращает undefined.

Пример цепочки прототипов

let animal = {
  eats: true,
  walk() {
    console.log("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

let longEar = {
  earLength: 10,
  __proto__: rabbit
};

// вызов метода walk пройдет по цепочке прототипов: longEar -> rabbit -> animal
longEar.walk(); // Animal walk
console.log(longEar.jumps); // true (найдено в rabbit)

Значение цепочки прототипов

Цепочка прототипов является фундаментальным механизмом наследования в JavaScript и имеет несколько ключевых преимуществ:

  • Повторное использование кода: Объекты могут наследовать общие свойства и методы от своих прототипов, что способствует сокращению дублирования кода.
  • Динамическое наследование: Свойства и методы, добавленные или измененные в прототипе, автоматически становятся доступны всем объектам, находящимся ниже по цепочке прототипов.
  • Гибкость: Цепочка прототипов позволяет создавать сложные иерархии наследования, обеспечивая при этом эффективное управление памятью и производительностью.

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

Cвойства __proto__ и prototype

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

__proto__

Свойство __proto__ — это геттер/сеттер, который позволяет читать или изменять прототип (т.е., внутреннее свойство [[Prototype]]) конкретного объекта. Это свойство не является частью стандарта ECMAScript до ES6, однако было реализовано во многих JavaScript движках как средство доступа к прототипу объекта.

let animal = {
  eats: true
};

let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // Устанавливаем animal как прототип для rabbit

console.log(rabbit.eats); // true, так как свойство наследуется от animal

Важно отметить, что использование __proto__ считается устаревшим, и рекомендуется использовать функции Object.getPrototypeOf() и Object.setPrototypeOf() для работы с прототипами объектов.

prototype

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

function Animal(name) {
  this.name = name;
}

Animal.prototype.walk = function() {
  console.log(`${this.name} walks.`);
};

let animal = new Animal("Elephant");

animal.walk(); // Elephant walks.

Когда вы создаете новый объект с помощью new Animal(), созданный объект автоматически наследует свойства и методы из Animal.prototype, что позволяет всем объектам, созданным с использованием Animal, разделять метод walk.

Основные различия

  • __proto__ — это свойство любого объекта, указывающее на его прототип. С его помощью можно получить или установить прототип объекта.
  • prototype — это свойство функций-конструкторов, определяющее объект, который будет назначен в качестве прототипа для объектов, созданных с использованием этой функции-конструктора.
  • Использование __proto__ считается устаревшим. Для работы с прототипами объектов предпочтительнее использовать Object.getPrototypeOf() и Object.setPrototypeOf().
  • prototype используется только в контексте функций-конструкторов для наследования свойств и методов новыми объектами.

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

Создание объекта с прототипом

В JavaScript существует несколько способов создания объекта с определенным прототипом:

С помощью Object.create

Метод Object.create(proto, [propertiesObject]) создает новый объект с указанным объектом proto в качестве его прототипа и необязательным объектом propertiesObject для добавления собственных свойств.

let animal = {
  eats: true,
  walk() {
    console.log("Animal walk");
  }
};

let rabbit = Object.create(animal, {
  jumps: {
    value: true,
    enumerable: true // делаем свойство перечисляемым
  }
});

rabbit.walk(); // Animal walk
console.log(rabbit.jumps); // true

С использованием конструктора и prototype

Этот способ подразумевает создание функции-конструктора и определение свойств/методов в её свойстве prototype. Объекты, созданные с помощью оператора new и этой функции-конструктора, будут иметь указанный прототип.

function Animal(name) {
  this.name = name;
}

Animal.prototype.walk = function() {
  console.log(`${this.name} walks.`);
};

let animal = new Animal("Elephant");
animal.walk(); // Elephant walks.

Использование классов в ES6

В ES6 введена синтаксическая обертка вокруг прототипного наследования — классы, которые позволяют более удобно работать с прототипами.

class Animal {
  constructor(name) {
    this.name = name;
  }

  walk() {
    console.log(`${this.name} walks.`);
  }
}

class Rabbit extends Animal {
  jump() {
    console.log(`${this.name} jumps.`);
  }
}

let rabbit = new Rabbit("White Rabbit");
rabbit.walk(); // White Rabbit walks.
rabbit.jump(); // White Rabbit jumps.

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

Различие между прототипным и функциональным наследованием

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

Прототипное наследование

Прототипное наследование в JavaScript позволяет объектам наследовать свойства и методы от других объектов через цепочку прототипов. Это достигается за счёт использования внутреннего свойства объекта [[Prototype]], которое ссылается на другой объект, прототип.

  • Эффективность: Способствует экономии памяти, так как методы хранятся в одном экземпляре в прототипе и доступны всем объектам, наследующим этот прототип.
  • Гибкость: Позволяет динамически изменять прототипы, добавлять или удалять свойства/методы в реальном времени.

Пример:

let animal = {
  walk() {
    console.log("Animal walk");
  }
};

let rabbit = Object.create(animal);
rabbit.walk(); // Animal walk

Функциональное наследование

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

  • Инкапсуляция: Позволяет скрыть детали реализации и создать приватные свойства и методы, недоступные извне.
  • Наследование: Осуществляется путём расширения и дополнения объекта новыми свойствами и методами.

Пример:

function Animal(name) {
  let animal = {};
  animal.name = name;
  animal.walk = function() {
    console.log(`${animal.name} walks`);
  };
  return animal;
}

let rabbit = Animal("Rabbit");
rabbit.walk(); // Rabbit walks

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

Получение и установка прототипа объекта

Для работы с прототипами объектов в JavaScript существуют специализированные методы:

Получение прототипа объекта

  • Object.getPrototypeOf(obj): Возвращает прототип указанного объекта obj.

Установка прототипа объекта

  • Object.setPrototypeOf(obj, prototype): Устанавливает прототип (prototype) указанному объекту obj.

Пример использования:

let animal = {
  eats: true
};

let rabbit = {
  jumps: true
};

// Установка прототипа для rabbit
Object.setPrototypeOf(rabbit, animal);

// Получение прототипа объекта rabbit
console.log(Object.getPrototypeOf(rabbit) === animal); // true

rabbit.eats; // true, наследуется от animal

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

Где хранится ссылка на прототип

В JavaScript каждый объект имеет скрытое свойство [[Prototype]], которое является его прототипом. Это свойство не доступно напрямую из JavaScript кода начиная с ECMAScript 5, но до этого стандарта многие движки JavaScript предоставляли доступ к нему через свойство __proto__. В современном JavaScript рекомендуется использовать методы Object.getPrototypeOf() и Object.setPrototypeOf() для чтения и изменения прототипов объектов соответственно.

Создание недоступного объекта

В ECMAScript 5 был введен метод Object.create(null), который создает объект без прототипа. Такой объект не наследует ничего от Object.prototype, что делает его "чистым" словарем, идеально подходящим для использования в качестве простого ключ-значение хранилища без риска случайного наследования методов или свойств.

Пример создания недоступного объекта:

let map = Object.create(null);
map.key = "value";
console.log(map.key); // "value"
// map.toString не существует, так как нет наследования от Object.prototype

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

Методы из Object.prototype

Object.prototype является прототипом для всех объектов и предоставляет базовый набор методов, доступных для всех объектов в JavaScript. Некоторые из этих методов включают:

  • toString(): Возвращает строковое представление объекта. По умолчанию возвращает "[object Type]", где Type — тип объекта.
  • hasOwnProperty(propName): Возвращает true, если свойство с именем propName существует в объекте как собственное (не унаследованное через цепочку прототипов) свойство.
  • isPrototypeOf(object): Проверяет, является ли объект прототипом для другого объекта.
  • valueOf(): Возвращает примитивное значение указанного объекта. В большинстве случаев просто возвращает сам объект.

Эти методы обеспечивают основную функциональность для всех объектов в JavaScript и могут быть переопределены для изменения поведения наследуемых объектов. Важно помнить, что переопределение этих методов, особенно toString() и valueOf(), может влиять на работу вашего кода в разных контекстах, например, при приведении типов или строковом представлении объектов.

Замыкание (closure) в программировании — это функция, со всеми переменными, которые были в её области видимости в момент создания. Это одна из ключевых концепций JavaScript и многих других языков программирования, позволяющая функции доступ к переменным из области, в которой она была объявлена, даже после того, как эта область выполнения завершила свою работу.

Как работают замыкания

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

Пример замыкания

function createCounter() {
    let count = 0;
    return function() {
        count++;
        return count;
    };
}

let counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

В этом примере, createCounter возвращает функцию, которая увеличивает переменную count и возвращает её значение. Каждый раз, когда мы вызываем counter(), переменная count увеличивается, и это работает благодаря механизму замыканий. Функция возвращаемая из createCounter сохраняет ссылку на переменную count своей "родительской" функции.

Проблемы, которые решают замыкания

  1. Инкапсуляция данных: Замыкания позволяют скрыть переменные внутри функции, делая их недоступными извне и тем самым предоставляя приватность данных.

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

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

  4. Модульность и управление зависимостями: Замыкания могут использоваться для создания модулей и "классов" в JavaScript, обеспечивая упорядоченную и легко управляемую структуру кода.

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

Что такое контекст

В программировании, и особенно в JavaScript, контекст (часто называемый "контекст выполнения") относится к окружению, в котором выполняется текущий код. В контекст включена информация о том, как и где была вызвана функция, а также о текущем значении ключевого слова this.

Ключевое слово this в JavaScript используется для доступа к текущему контексту исполнения и может ссылаться на различные объекты в зависимости от того, как и где функция была вызвана. this может указывать на глобальный объект, на экземпляр объекта (в методе объекта), на новый объект (при вызове функции с оператором new) или на значение, указанное явно при использовании методов call, apply, и bind.

Что такое глобальный контекст

Глобальный контекст в JavaScript — это контекст выполнения, который существует за пределами любых функций. В браузере глобальным контекстом является объект window, тогда как в Node.js это объект global. В глобальном контексте переменные, объявленные с помощью ключевых слов var (на уровне скрипта), функции и другие данные становятся свойствами глобального объекта.

Пример в браузере:

var globalVar = "Я глобальная переменная";

console.log(window.globalVar); // "Я глобальная переменная"

В глобальном контексте значение this также ссылается на глобальный объект:

console.log(this === window); // true в браузере

В Node.js:

global.globalVar = "Я глобальная переменная в Node.js";

console.log(globalVar); // "Я глобальная переменная в Node.js"
console.log(global === this); // true в глобальном контексте Node.js

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

Как определяется контекст во время выполнения?

Контекст (this) функции в JavaScript зависит от условий её вызова:

  1. Глобальный контекст: Вне любой функции this указывает на глобальный объект (window в браузерах, global в Node.js).
  2. Вызов функции: При обычном вызове функции (например, myFunction()), this указывает на глобальный объект. В строгом режиме ('use strict';) this будет undefined.
  3. Метод объекта: Когда функция вызывается как метод объекта (например, obj.myMethod()), this указывает на объект obj.
  4. Конструктор: При вызове функции с оператором new (например, new MyFunction()), this указывает на новосозданный объект.
  5. Явное привязывание: Используя методы call, apply, или bind, можно явно установить значение this.

Как привязать контекст, возможно ли привязать его дважды?

Для явного привязывания контекста к функции используется метод bind. Этот метод создает новую функцию с привязанным контекстом и предварительно заданными аргументами, если они есть.

Пример использования bind:

function greet() {
  console.log(this.name);
}

const person = { name: "Alex" };
const greetPerson = greet.bind(person);
greetPerson(); // Выведет: Alex

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

const anotherPerson = { name: "John" };
const greetAnotherPerson = greetPerson.bind(anotherPerson);
greetAnotherPerson(); // Всё ещё выводит: Alex, не John

Когда контекст теряется?

Контекст может потеряться в нескольких ситуациях, в том числе:

  1. Обработчики событий: Когда метод объекта передается как обработчик события, this внутри метода будет указывать не на объект, а на элемент, для которого обработчик был установлен, если не использовать bind.
  2. Обратные вызовы: При передаче методов как обратных вызовов, например, в setTimeout или при итерации массивов (array.map, array.forEach), если не использовать bind или стрелочные функции для сохранения контекста.
  3. Отсоединенные методы: Когда ссылка на метод объекта присваивается переменной и вызывается отдельно от объекта.

Использование стрелочных функций (() => {}) является одним из способов избежать потери контекста, поскольку они не имеют собственного контекста выполнения и наследуют this из внешней области видимости.

Базовый синтаксис классов в JavaScript

Классы в JavaScript представляют собой синтаксический сахар над существующим прототипным наследованием. Они предоставляют более простой и понятный способ создания объектов и организации наследования. Вот как выглядит базовый синтаксис класса:

class MyClass {
  constructor(prop1, prop2) {
    this.prop1 = prop1;
    this.prop2 = prop2;
  }

  method1() {
    console.log(this.prop1);
  }

  method2() {
    console.log(this.prop2);
  }
}

const myObject = new MyClass('Значение свойства 1', 'Значение свойства 2');
myObject.method1(); // Выводит: Значение свойства 1
myObject.method2(); // Выводит: Значение свойства 2

Ключевое слово constructor используется для создания и инициализации объекта, созданного с класса. Методы, определенные внутри класса, добавляются в prototype этого класса.

Наследование классов

Наследование позволяет одному классу наследовать свойства и методы другого. Для наследования используется ключевое слово extends. В наследуемом классе для обращения к методам и конструктору родительского класса используется ключевое слово super.

Пример наследования классов:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} издает звук.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // вызов конструктора родительского класса
    this.breed = breed;
  }

  speak() {
    super.speak(); // вызов метода родительского класса
    console.log(`${this.name} лает.`);
  }
}

const dog = new Dog('Рекс', 'Овчарка');
dog.speak();
// Выводит: Рекс издает звук.
// Выводит: Рекс лает.

В этом примере класс Dog наследует от класса Animal. Мы расширили функциональность метода speak в классе Dog, используя метод speak родительского класса Animal с помощью super.speak().

Использование классов и наследование делает структуру объектно-ориентированного кода в JavaScript более понятной и упорядоченной, облегчая разработку и поддержку.

Использование super()

Ключевое слово super используется в классах JavaScript для обращения к родительскому классу. Оно имеет два основных применения:

  1. Для вызова конструктора родительского класса. В производном классе, перед тем как использовать this, вы должны сначала вызвать super(). Этот вызов выполняется в конструкторе производного класса для инициализации родительского класса перед выполнением любых других операций с экземпляром дочернего класса.

  2. Для обращения к методам родительского класса. super также используется для вызова функций родительского класса из дочернего класса.

Вызов конструктора родительского класса

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

class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Вызов конструктора класса Animal
    this.breed = breed;
  }
}

В этом примере, конструктор класса Dog вызывает конструктор родительского класса Animal, передавая ему имя, чтобы инициализировать базовую часть экземпляра Dog.

Обращение к методам родительского класса

super можно использовать в методах производного класса для вызова методов родительского класса.

class Animal {
  speak() {
    console.log("The animal makes a noise");
  }
}

class Dog extends Animal {
  speak() {
    super.speak(); // Вызов метода speak класса Animal
    console.log("The dog barks");
  }
}

let dog = new Dog();
dog.speak();
// Вывод:
// The animal makes a noise
// The dog barks

Здесь метод speak класса Dog сначала вызывает реализацию метода speak из класса Animal с помощью super.speak(), а затем добавляет своё поведение (выводит сообщение "The dog barks").

Использование super является важной частью работы с классами в JavaScript, особенно при реализации наследования и полиморфизма.

Приватные и защищенные свойства и методы

В JavaScript, начиная с ECMAScript 2015 (ES6), был введен синтаксис классов, который позволил более удобно использовать прототипное наследование. Однако до недавнего времени в языке отсутствовали средства для создания приватных и защищенных свойств и методов в классах, как это принято во многих других языках программирования. В последних версиях стандарта ECMAScript появилась поддержка приватных свойств и методов.

  • Приватные свойства и методы определяются с помощью символа # в начале их имени. Они доступны только внутри класса, где были объявлены.
class MyClass {
  #privateProperty = 'private value';

  #privateMethod() {
    return 'private method';
  }

  publicMethod() {
    console.log(this.#privateProperty); // Доступ к приватному свойству из метода класса
    console.log(this.#privateMethod()); // Доступ к приватному методу
  }
}

const myInstance = new MyClass();
myInstance.publicMethod(); // Выводит приватные значения
// myInstance.#privateProperty; // Ошибка: приватное свойство недоступно извне
  • Защищенные свойства и методы формально не входят в стандарт ECMAScript, но их можно имитировать с помощью соглашений об именовании, например, путем добавления префикса _ к имени (хотя это не обеспечивает настоящей защиты от доступа извне).
class MyClass {
  _protectedProperty = 'protected value';

  _protectedMethod() {
    return 'protected method';
  }

  publicMethod() {
    console.log(this._protectedProperty); // Технически доступно извне, но считается защищенным по соглашению
  }
}

Использование "instanceof"

Оператор instanceof проверяет, принадлежит ли объект к определенному классу или был ли объект создан с использованием конструктора данного класса (или конструктора из его прототипной цепочки).

class MyClass {}

const myInstance = new MyClass();

console.log(myInstance instanceof MyClass); // true
console.log(myInstance instanceof Object); // true, потому что MyClass наследуется от Object

Статические свойства и методы

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

class MyClass {
  static staticProperty = 'static value';

  static staticMethod() {
    return 'static method';
  }
}

console.log(MyClass.staticProperty); // 'static value'
console.log(MyClass.staticMethod()); // 'static method'

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

Что такое сборщик мусора?

Сборщик мусора (Garbage Collector, GC) — это процесс или механизм автоматического освобождения памяти, который идентифицирует и удаляет из памяти объекты, которые больше не используются в программе. Сборка мусора является ключевым аспектом управления памятью во многих современных языках программирования, включая JavaScript.

GC работает в фоновом режиме и отслеживает каждый объект в программе. Если объект становится недостижимым (то есть, нет способа, с помощью которого можно было бы обратиться к этому объекту из исполняемого кода), GC автоматически освобождает занимаемую им память. Это освобождение памяти делает её доступной для новых объектов.

Зачем нужен сборщик мусора?

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

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

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

  3. Безопасность и стабильность: Автоматическая сборка мусора помогает обеспечить более безопасную и стабильную работу программы, минимизируя риски, связанные с повреждением памяти, неправильным освобождением или двойным освобождением памяти.

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

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

Блокирующий код

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

Пример блокирующего кода:

const fs = require('fs');

// Синхронное чтение файла. До завершения чтения программа "заморожена".
const data = fs.readFileSync('/path/to/file');
console.log(data);
// Следующий код не будет выполнен, пока файл полностью не прочитан.
console.log('Файл прочитан');

Асинхронное программирование

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

Асинхронное программирование позволяет обрабатывать длительные операции, не блокируя выполнение остального кода. В JavaScript для работы с асинхронностью используются коллбэки, промисы и async/await.

Коллбэки

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

fs.readFile('/path/to/file', (err, data) => {
  if (err) throw err;
  console.log(data);
});
console.log('Файл будет прочитан позже');

Промисы

Промисы представляют собой объекты, которые обещают выполнить некоторую операцию в будущем. Они имеют методы .then(), .catch() и .finally(), позволяющие управлять последовательностью асинхронных операций.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Результат асинхронной операции');
  }, 1000);
});

promise.then(result => console.log(result));
console.log('Асинхронная операция запущена');

Async/Await

Async/await — синтаксический сахар над промисами, позволяющий писать асинхронный код, который выглядит как синхронный.

async function asyncFunction() {
  const result = await new Promise((resolve) => {
    setTimeout(() => resolve('Результат асинхронной операции'), 1000);
  });
  console.log(result); // Этот лог будет выполнен после завершения промиса
}
asyncFunction();
console.log('Асинхронная операция запущена');

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

XMLHttpRequest

XMLHttpRequest (XHR) — это JavaScript API, которое используется для создания асинхронных HTTP-запросов к серверам из браузеров. Он позволяет веб-страницам отправлять запросы к серверу и загружать данные без необходимости перезагружать страницу. Это делает XMLHttpRequest основным инструментом в разработке веб-приложений, особенно при создании одностраничных приложений (SPA), где требуется динамически обновлять содержимое страницы без её полной перезагрузки.

Основные характеристики:

  • Асинхронные и синхронные запросы: Хотя основное предназначение XMLHttpRequest — выполнение асинхронных запросов, API также поддерживает синхронные запросы. Однако использование синхронных запросов не рекомендуется, так как они блокируют выполнение кода и могут негативно сказаться на производительности и восприятии интерфейса пользователем.
  • Работа с различными данными: XMLHttpRequest может работать с различными типами данных, включая XML, JSON, HTML и текстовые файлы.
  • Контроль над HTTP-запросами: API предоставляет различные методы и свойства для настройки запросов, включая URL, метод запроса (GET, POST и другие), заголовки, тайм-ауты и обработку ответов от сервера.

Основной процесс использования XMLHttpRequest:

  1. Создание экземпляра XMLHttpRequest:
var xhr = new XMLHttpRequest();
  1. Настройка запроса с помощью метода open():
xhr.open('GET', 'https://example.com/api/data', true); // Метод, URL, асинхронный ли запрос
  1. Отправка запроса:
xhr.send();
  1. Обработка ответа сервера:
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4 && xhr.status == 200) {
    console.log(xhr.responseText); // Действия с полученными данными
  }
};

Пример использования:

var xhr = new XMLHttpRequest();
xhr.open("GET", "https://example.com/api/data", true);
xhr.onreadystatechange = function () {
  if (xhr.readyState == 4 && xhr.status == 200) {
    var response = JSON.parse(xhr.responseText);
    console.log(response);
  }
};
xhr.send();

В этом примере создаётся и отправляется GET-запрос к указанному URL. Когда ответ сервера полностью получен, содержимое ответа выводится в консоль.

Хотя XMLHttpRequest и остаётся важным инструментом для асинхронных HTTP-запросов, современные веб-разработчики всё чаще используют API fetch из-за его более простого синтаксиса и обещаний (promises), которые упрощают работу с асинхронным кодом.

Статические методы Promise

В JavaScript, объект Promise предоставляет несколько статических методов, которые позволяют выполнять различные операции с промисами. Эти методы полезны для создания, комбинирования и управления промисами в асинхронных операциях. Вот основные статические методы промисов:

Promise.resolve(value)

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

let promise = Promise.resolve(42);

promise.then((value) => {
  console.log(value); // 42
});

Promise.reject(reason)

Возвращает промис, который был отклонён по указанной причине. Это удобно для создания отклонённых промисов для тестирования цепочек обработки ошибок.

let promise = Promise.reject("Ошибка");

promise.catch((error) => {
  console.log(error); // "Ошибка"
});

Promise.all(iterable)

Принимает итерируемый объект (например, массив) промисов и возвращает новый промис, который:

  • Разрешается, когда все промисы в итерируемом объекте были успешно разрешены. Значение разрешённого промиса будет массивом значений разрешённых промисов, в том же порядке.
  • Отклоняется, как только любой из промисов отклоняется. Причина отклонения будет той же, что и у первого отклонённого промиса из списка.
Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])
  .then((values) => {
    console.log(values); // [1, 2, 3]
  });

Promise.allSettled(iterable)

Похож на Promise.all, но вместо того, чтобы ждать успешного выполнения всех промисов, возвращает промис, который разрешается после завершения всех промисов (разрешённых или отклонённых). Результатом будет массив объектов, каждый из которых представляет результат выполнения каждого промиса и содержит статус ("fulfilled" или "rejected") и значение или причину отклонения.

Promise.allSettled([Promise.resolve(1), Promise.reject("Ошибка"), Promise.resolve(3)])
  .then((results) => {
    console.log(results); // [{status: "fulfilled", value: 1}, {status: "rejected", reason: "Ошибка"}, {status: "fulfilled", value: 3}]
  });

Promise.race(iterable)

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

Promise.race([Promise.resolve(1), new Promise((resolve) => setTimeout(() => resolve(2), 100))])
  .then((value) => {
    console.log(value); // 1
  });

Promise.any(iterable)

Похож на Promise.race, но возвращает первый успешно разрешённый промис из всех переданных. Если все промисы отклонены, результатом будет отклонённый промис с агрегированной ошибкой AggregateError.

Promise.any([Promise.reject("Ошибка1"), Promise.resolve(2), Promise.resolve(3)])
  .then((value) => {
    console.log(value); // 2
  })
  .catch((error) => {
    console.log(error); // Не будет вызвано, так как есть успешно разрешенные промисы
  });

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

Концепция цикла событий (Event Loop)

Event Loop (цикл событий) — это фундаментальная концепция, лежащая в основе асинхронного программирования в JavaScript, особенно в контексте браузера. Основная задача Event Loop — следить за стеком вызовов (Call Stack) и очередью событий (Callback Queue), и когда стек вызовов пуст, перемещать задачи из очереди событий в стек вызовов для их выполнения.

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

Пример:

  1. Выполнение синхронного кода.
  2. Запуск асинхронных операций (например, таймеров).
  3. После выполнения асинхронной операции, результат или callback помещается в очередь событий.
  4. Цикл событий проверяет очередь на наличие задач и, когда основной поток свободен, передает задачу из очереди для выполнения.

Компоненты Event Loop

Для понимания работы Event Loop важно знать следующие компоненты:

  1. Стек вызовов (Call Stack): Однопоточная структура, которая отслеживает все операции, выполняемые в данный момент. Когда функция вызывается, она помещается на вершину стека. Когда JavaScript-движок заканчивает выполнение функции, он удаляет ее из стека. Это продолжается до тех пор, пока стек не опустеет.

  2. Куча (Heap): Неструктурированная область памяти, используемая для хранения объектов. Это место, где происходит выделение памяти под переменные и функции.

  3. Очередь событий (Callback Queue): Очередь, в которую помещаются коллбэки событий (например, от setTimeout, обработчиков событий DOM, AJAX-запросов и т.д.), ожидающих обработки. Когда стек вызовов опустеет, Event Loop начнет перемещать задачи из очереди событий в стек вызовов по одной.

  4. Event Loop: Циклический процесс, который отслеживает стек вызовов и очередь событий и перемещает задачи из очереди в стек, как только тот опустеет.

Как работает Event Loop в браузере

  1. Выполнение скрипта: Event Loop начинает с выполнения скрипта, которое попадает в стек вызовов. Когда в скрипте встречается асинхронная операция (например, setTimeout), коллбэк этой операции регистрируется и помещается в соответствующую очередь (например, в очередь таймеров).

  2. Освобождение стека вызовов: Event Loop ожидает, пока стек вызовов не опустеет, то есть все синхронные операции завершатся.

  3. Перемещение из очереди событий: Как только стек вызовов пуст, Event Loop начинает перемещать коллбэки из очереди событий в стек вызовов для их выполнения. Однако, важно отметить, что существуют разные типы очередей событий (например, для обработчиков событий DOM, таймеров, промисов), и они могут обрабатываться по разным правилам приоритета.

  4. Микрозадачи: Отдельно стоит отметить очередь микрозадач, куда попадают коллбэки промисов (Promise.then/catch/finally). Микрозадачи имеют более высокий приоритет, чем обычные задачи, и выполняются немедленно после текущего синхронного кода, даже если в очереди событий есть другие задачи.

  5. Бесконечный цикл: Процесс повторяется в бесконечном цикле: выполнение задач из стека вызовов, перенос задач из очереди событий в стек вызовов при его пустоте, обработка микрозадач.

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

Макрозадачи (Macrotasks)

Макрозадачи — это крупные задачи, которые управляются Event Loop. Каждая макрозадача добавляется в очередь макрозадач и выполняется одна за другой. Когда выполняется макрозадача, новые макрозадачи добавляются в конец очереди. Примеры макрозадач включают:

  • Обработчики событий DOM
  • AJAX запросы
  • Таймеры (setTimeout, setInterval)
  • Выполнение скриптов

Микрозадачи (Microtasks)

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

  • Обещания (Promises), включая .then(), .catch(), и .finally() обработчики
  • Объекты MutationObserver
  • queueMicrotask()

Как работают вместе

В контексте Event Loop процесс обработки задач выглядит следующим образом:

  1. Выполняется текущая макрозадача из очереди макрозадач.
  2. После завершения макрозадачи, перед выполнением следующей макрозадачи, выполняются все микрозадачи из очереди микрозадач. Если во время выполнения микрозадач добавляются новые микрозадачи, они также будут выполнены в этом же цикле.
  3. Если очередь микрозадач опустеет, Event Loop переходит к следующей макрозадаче.

Важность различия

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

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

setTimeout и setInterval

setTimeout и setInterval — это функции таймеров в JavaScript, которые позволяют выполнять код асинхронно после заданной задержки или периодически.

  • setTimeout используется для выполнения кода один раз после заданной задержки (в миллисекундах).

    setTimeout(() => {
      console.log('Прошло 2 секунды');
    }, 2000);
  • setInterval используется для выполнения кода повторно через равные интервалы времени.

    setInterval(() => {
      console.log('Это сообщение появляется каждые 3 секунды');
    }, 3000);

Зачем они нужны?

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

Очистка таймеров

Для остановки запланированных операций используются функции clearTimeout и clearInterval.

  • clearTimeout останавливает выполнение кода, запланированное с помощью setTimeout.
  • clearInterval останавливает повторяющиеся выполнения кода, установленные через setInterval.
const timeoutId = setTimeout(() => {
  console.log('Этот код не выполнится');
}, 1000);

clearTimeout(timeoutId);

const intervalId = setInterval(() => {
  console.log('Этот код не будет повторяться');
}, 1000);

clearInterval(intervalId);

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

setTimeout с нулевой задержкой

Использование setTimeout с задержкой в 0 миллисекунд (setTimeout(() => {}, 0)) — это распространенный прием в JavaScript для отложенного выполнения кода, который позволяет текущему стеку выполнения завершиться, прежде чем указанный коллбэк будет выполнен. Несмотря на то, что задержка указана как 0 миллисекунд, фактическое выполнение коллбэка произойдет не сразу. Это связано с особенностями работы цикла событий и очереди задач в JavaScript.

Как это работает?

Когда вы используете setTimeout с задержкой в 0 миллисекунд, вы, фактически, помещаете задачу (вызов коллбэка) в очередь задач (task queue) цикла событий. Однако выполнение этой задачи не начнется, пока не завершатся все текущие выполнения в основном стеке вызовов и цикл событий не получит возможности обработать задачу из очереди.

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

  1. Основной стек вызовов (Call Stack) должен быть пуст: JavaScript выполняет текущий код в стеке вызовов. Если стек не пуст, выполнение setTimeout с нулевой задержкой будет ожидать, пока стек очистится.

  2. Браузеры имеют минимальную задержку для setTimeout: Даже если указать задержку в 0 миллисекунд, большинство современных браузеров применяют минимальную задержку (обычно около 4 мс) для предотвращения злоупотребления и потенциального снижения производительности.

  3. Очередь задач (Task Queue): setTimeout помещает коллбэк в очередь задач, которая будет обработана только после того, как цикл событий обработает все синхронные задачи. Если в очереди уже есть задачи, коллбэк от setTimeout(() => {}, 0) будет ждать их выполнения.

Практическое применение

  • Разделение тяжелых вычислений: Если вам необходимо выполнить тяжелую задачу, которая может заблокировать основной поток, можно разделить ее на более мелкие части и использовать setTimeout(() => {}, 0) для их последовательного выполнения, позволяя другим событиям и отрисовке интерфейса обрабатываться между частями.
  • Отложенное выполнение: Позволяет убедиться, что код будет выполнен после того, как весь синхронный код и DOM-события обработаны, что может быть полезно для изменений в DOM, зависящих от полностью обработанных предыдущих изменений.

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

Methods of Object

Object.keys()

Метод Object.keys(obj) принимает объект obj в качестве аргумента и возвращает массив из собственных перечислимых свойств объекта в виде строк. Этот метод полезен, когда вам нужно работать только с ключами объекта.

const person = {
  name: "Алексей",
  age: 30,
  job: "Веб-разработчик"
};

const keys = Object.keys(person);
console.log(keys); // ["name", "age", "job"]

Object.values()

Метод Object.values(obj) похож на Object.keys, но возвращает массив значений собственных перечислимых свойств объекта.

const person = {
  name: "Алексей",
  age: 30,
  job: "Веб-разработчик"
};

const values = Object.values(person);
console.log(values); // ["Алексей", 30, "Веб-разработчик"]

for...in

Цикл for...in позволяет перебирать все перечислимые свойства объекта, включая унаследованные. В каждой итерации в переменную цикла помещается ключ (или имя свойства) объекта. Этот метод удобен, когда вам нужно выполнить операции с каждым свойством объекта.

const person = {
  name: "Алексей",
  age: 30,
  job: "Веб-разработчик"
};

for (const key in person) {
  console.log(key + ': ' + person[key]);
}
// Вывод:
// name: Алексей
// age: 30
// job: Веб-разработчик

Важно помнить, что цикл for...in также переберет унаследованные свойства, что не всегда желательно. Чтобы избежать перебора унаследованных свойств, можно использовать метод Object.hasOwnProperty() внутри цикла:

for (const key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key + ': ' + person[key]);
  }
}

Methods of Arrays

В JavaScript для работы с массивами предоставляется множество удобных методов. Вот подробное объяснение и сравнение некоторых из них:

map vs forEach

map():

  • Создает новый массив, результатом выполнения функции, предоставленной в аргументе, на каждом элементе исходного массива.
  • Не изменяет исходный массив.
  • Всегда возвращает новый массив той же длины, что и исходный.
const numbers = [1, 2, 3];
const squared = numbers.map(num => num * num);
console.log(squared); // [1, 4, 9]

forEach():

  • Выполняет заданную функцию один раз для каждого элемента в массиве.
  • Используется для выполнения операций над каждым элементом массива.
  • Не возвращает значение.
const numbers = [1, 2, 3];
numbers.forEach(num => console.log(num * num));
// Выводит 1, 4, 9 в консоль, но не возвращает массив

find, filter, some, includes

find():

  • Возвращает первый элемент в массиве, который соответствует предоставленному условию в функции-предикате.
  • Если элемент не найден, возвращает undefined.
const numbers = [1, 2, 3, 4];
const found = numbers.find(num => num > 2);
console.log(found); // 3

filter():

  • Создает новый массив со всеми элементами, прошедшими проверку, заданную в функции-предикате.
  • Возвращает новый массив, может быть пустым, если ни один элемент не прошел проверку.
const numbers = [1, 2, 3, 4];
const filtered = numbers.filter(num => num > 2);
console.log(filtered); // [3, 4]

some():

  • Проверяет, удовлетворяет ли какой-либо элемент массива условию, заданному в функции-предикате.
  • Возвращает true, если условие выполняется хотя бы для одного элемента, иначе false.
const numbers = [1, 2, 3, 4];
const hasGreaterThanTwo = numbers.some(num => num > 2);
console.log(hasGreaterThanTwo); // true

includes():

  • Проверяет, содержит ли массив определенный элемент.
  • Возвращает true, если элемент найден в массиве, и false - если нет.
const numbers = [1, 2, 3, 4];
const includesTwo = numbers.includes(2);
console.log(includesTwo); // true

reduce

reduce():

  • Применяет функцию-редуктор к каждому элементу массива (слева направо), сводя его к одному результирующему значению.
  • Принимает функцию-редуктор, которая получает аргументы: аккумулятор, текущий элемент, индекс текущего элемента, исходный массив.
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // 10

sort

sort():

  • Сортирует элементы массива на месте и возвращает отсортированный массив.
  • По умолчанию сортирует элементы как строки.
  • Для числовой сортировки необходимо предоставить функцию сравнения.
const numbers = [4, 2, 3, 1];
numbers.sort((a, b) => a - b);
console.log(numbers); // [1, 2, 3, 4]

Клонирование объектов в JavaScript

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

Поверхностное клонирование

Поверхностное (shallow) клонирование создает новый объект, копируя в него только значения свойств исходного объекта на первом уровне вложенности. Если свойство является объектом, то копируется ссылка на этот объект, а не сам объект. Это означает, что вложенные объекты будут разделяться между исходным объектом и его копией.

Способы реализации:

  • Использование Object.assign():
const original = { a: 1, b: { c: 2 } };
const clone = Object.assign({}, original);
console.log(clone); // { a: 1, b: { c: 2 } }
// Изменение вложенного объекта повлияет на оба объекта
clone.b.c = 3;
console.log(original.b.c); // 3
  • Оператор разворота (spread operator):
const original = { a: 1, b: { c: 2 } };
const clone = { ...original };
// Аналогично Object.assign, вложенные объекты не клонируются

Глубокое клонирование

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

Способы реализации:

  • Использование JSON.stringify() и JSON.parse() (работает только с JSON-совместимыми данными):
const original = { a: 1, b: { c: 2 } };
const clone = JSON.parse(JSON.stringify(original));
clone.b.c = 3;
console.log(original.b.c); // 2, исходный объект не изменился

Этот метод не подходит для объектов, содержащих методы, символы или циклические ссылки, так как такие данные теряются или приводят к ошибкам при сериализации/десериализации.

  • Библиотеки для глубокого клонирования:

Для более сложных случаев, когда требуется клонировать объекты с методами, циклическими ссылками или специальными типами данных (например, Date, RegExp), рекомендуется использовать библиотеки, такие как Lodash (_.cloneDeep):

// Предполагается, что Lodash уже подключен
const original = { a: 1, b: { c: 2 } };
const clone = _.cloneDeep(original);
clone.b.c = 3;
console.log(original.b.c); // 2

Каждый из этих методов имеет свои преимущества и недостатки, поэтому выбор метода зависит от конкретных требований к клонированию объекта в вашем приложении.

Promises vs Callbacks

Callbacks:

  • Колбэки — это функции, которые передаются как аргументы в другие функции и вызываются после выполнения асинхронной операции.
  • Основной недостаток колбэков — "callback hell" или "pyramid of doom", когда асинхронные операции вызываются последовательно. Это приводит к глубокой вложенности колбэков и снижению читаемости кода.
function asyncOperation(argument, callback) {
  // Асинхронная операция
  setTimeout(() => {
    const result = argument + 1;
    callback(result);
  }, 1000);
}

asyncOperation(1, result => {
  console.log(result); // 2
  // Для выполнения следующей асинхронной операции требуется вложенный колбэк
});

Promises:

  • Промисы представляют собой объекты, предназначенные для упрощения асинхронного программирования. Они позволяют обрабатывать результат асинхронной операции в будущем: успешное выполнение (fulfilled) или ошибку (rejected).
  • Промисы поддерживают цепочки вызовов (then), что позволяет избежать "callback hell" и делает код более читаемым и удобным для отладки.
function asyncOperation(argument) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const result = argument + 1;
      resolve(result);
    }, 1000);
  });
}

asyncOperation(1)
  .then(result => {
    console.log(result); // 2
    return asyncOperation(result);
  })
  .then(result => console.log(result)) // 3
  .catch(error => console.error(error));

Примеры асинхронных функций

Асинхронные функции (async/await) являются еще одним способом работы с асинхронным кодом в JavaScript, делая его более синхронно выглядящим и еще более удобным для чтения и написания.

async function asyncExample() {
  try {
    const result1 = await asyncOperation(1);
    console.log(result1); // 2
    const result2 = await asyncOperation(result1);
    console.log(result2); // 3
  } catch (error) {
    console.error(error);
  }
}

asyncExample();

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

Использование async/await позволяет элегантно обрабатывать асинхронные операции и ошибки с использованием try/catch, в отличие от колбэков и даже от цепочек промисов.

Состояния Promise

Каждый Promise может находиться в одном из трех состояний:

  1. Pending (ожидание): Начальное состояние; асинхронная операция не завершена.
  2. Fulfilled (выполнено): Операция завершена успешно.
  3. Rejected (отклонено): Операция завершена с ошибкой.

Метод finally

Метод finally() используется для выполнения действий после того, как Promise был выполнен (независимо от его результата, то есть как для fulfilled, так и для rejected состояний). Этот метод полезен для очистки ресурсов или выполнения каких-либо действий, которые необходимо провести после завершения асинхронной операции, например, скрыть индикатор загрузки.

fetch('/data.json')
  .then(data => data.json())
  .catch(error => console.error('Ошибка при загрузке данных:', error))
  .finally(() => console.log('Операция завершена. Скрываем индикатор загрузки.'));

Обработка исключений/ошибок

Для обработки ошибок в Promise используется метод catch(), который "ловит" любые исключения, произошедшие в предшествующих методах then, или же ошибки, возникшие в самом Promise.

new Promise((resolve, reject) => {
  throw new Error('Произошла ошибка!');
})
  .catch(error => {
    console.error(error.message);
  });

Или в случае асинхронных функций, обработка исключений может быть выполнена с помощью конструкции try...catch:

async function fetchData() {
  try {
    const response = await fetch('/data.json');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Ошибка при загрузке данных:', error);
  }
}

fetchData();

Использование catch позволяет централизованно обрабатывать ошибки от всех предшествующих шагов в цепочке Promise, что делает код более читаемым и удобным для отладки, в отличие от подходов с колбэками, где обработка ошибок часто размазывается по всему коду.

Использование then для обработки ошибок

Метод .then() принимает два аргумента: первый — коллбэк для случая успешного выполнения промиса (fulfilled), второй — для обработки ошибок (rejected). Использование второго аргумента .then() является одним из способов обработки ошибок в цепочке промисов.

doSomething()
  .then(result => {
    console.log('Success:', result);
  }, error => {
    console.error('Error:', error);
  });

Однако чаще для обработки ошибок используется метод .catch(), который позволяет ловить ошибки, возникшие на любом этапе выполнения цепочки промисов.

doSomething()
  .then(result => {
    console.log('Success:', result);
  })
  .catch(error => {
    console.error('Error:', error);
  });

Обработка исключений/ошибок

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

Промисы

fetch('some/url')
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .catch(error => {
    console.error('There has been a problem with your fetch operation:', error);
  });

Async/await

Асинхронные функции (async/await) позволяют использовать более традиционный синтаксис try...catch для обработки ошибок, что делает код более читаемым и понятным.

async function fetchData() {
  try {
    const response = await fetch('some/url');
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('There has been a problem with your fetch operation:', error);
  }
}

Что такое DOM?

DOM (Document Object Model) — это программный интерфейс, который представляет содержимое веб-страницы (или документа) в виде дерева объектов. Каждый элемент страницы, включая текст, ссылки, изображения и так далее, является узлом в этом дереве. DOM делает содержимое веб-страницы доступным и изменяемым через языки программирования, такие как JavaScript. * Основные аспекты DOM:*

  • Структура дерева: DOM организует элементы веб-страницы в иерархическую структуру дерева, где каждый элемент страницы представляет собой узел. Это позволяет легко навигировать по различным частям документа и управлять ими.
  • Динамическое изменение: С помощью DOM можно динамически изменять содержимое и структуру веб-страницы. Это включает добавление, удаление и изменение элементов и их атрибутов.
  • Языконезависимость: Хотя DOM чаще всего используется с JavaScript, он является независимым от языка и может быть использован с любым языком программирования, который может манипулировать страницей.

Пример использования DOM в JavaScript:

// Находит элемент по его ID и изменяет текстовое содержимое
document.getElementById('myElement').textContent = 'Новый текст';

// Создает новый элемент <div>, добавляет к нему текст и вставляет его в конец <body>
const div = document.createElement('div');
div.textContent = 'Новый элемент';
document.body.appendChild(div);

Зачем нужен DOM?

DOM необходим для динамического взаимодействия с веб-страницей. Он позволяет:

  • Изменять содержимое элементов.
  • Реагировать на действия пользователя через обработчики событий (например, клики мыши, нажатия клавиш).
  • Добавлять и удалять элементы, изменяя структуру страницы.
  • Изменять стили элементов для изменения внешнего вида страницы.

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

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

Структура DOM-дерева

DOM-дерево состоит из узлов (nodes), которые представляют собой элементы (elements), атрибуты (attributes), текст (text) и комментарии (comments) на веб-странице. Вот основные типы узлов в DOM-дереве:

  • Элементы (Element nodes): Соответствуют HTML-тегам и образуют структуру документа. Например, <div>, <p>, <a> и так далее.
  • Текстовые узлы (Text nodes): Содержат текст, находящийся непосредственно внутри элементов. Текстовый узел всегда является дочерним по отношению к элементу.
  • Атрибуты (Attribute nodes): Хранят информацию, ассоциированную с элементами, такую как class, id, href и другие атрибуты. В современном DOM атрибуты обычно рассматриваются не как отдельные узлы, а как свойства элементов.
  • Комментарии (Comment nodes): Представляют комментарии в HTML-коде.

Особенности DOM-дерева

  • Вложенность: Элементы в DOM-дереве могут содержать другие элементы, создавая вложенные структуры, подобно веткам и листьям дерева.
  • Родительские и дочерние узлы: Каждый узел в дереве, кроме корневого, имеет ровно один родительский узел. Узлы могут иметь несколько дочерних узлов.
  • Братья и сестры (Sibling nodes): Узлы, имеющие общего родителя, называются братьями и сестрами.

Пример DOM-дерева

Рассмотрим простой HTML-документ:

<!DOCTYPE html>
<html>
<head>
    <title>Пример страницы</title>
</head>
<body>
    <div>
        Привет, <span>мир!</span>
    </div>
</body>
</html>

DOM-дерево для этого документа будет иметь следующую структуру:

  • Document (корневой узел)
    • <!DOCTYPE html>
    • <html>
      • <head>
        • <title>
          • "Пример страницы" (текстовый узел)
      • <body>
        • <div>
          • "Привет, " (текстовый узел)
          • <span>
            • "мир!" (текстовый узел)

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

Инспектирование DOM — это процесс анализа и взаимодействия с структурой и содержимым веб-страницы через инструменты разработчика (Developer Tools), встроенные в современные веб-браузеры, такие как Chrome, Firefox, Safari и Edge. Эти инструменты позволяют разработчикам просматривать, изменять и отлаживать HTML, CSS и JavaScript код прямо в браузере.

Как использовать инструменты для инспектирования DOM

  1. Открытие инструментов разработчика:

    • Можно открыть их, нажав правой кнопкой мыши на элементе веб-страницы и выбрав "Просмотреть код" или "Inspect" в зависимости от браузера.
    • Также инструменты разработчика можно открыть с помощью горячих клавиш. Например, в большинстве браузеров это Ctrl+Shift+I на Windows/Linux или Cmd+Opt+I на macOS.
  2. Вкладка "Elements" (Chrome) / "Inspector" (Firefox):

    • Эта вкладка показывает структуру DOM-дерева текущей страницы, позволяя вам просматривать, расширять и сжимать узлы дерева для изучения вложенности элементов.
    • Вы можете наводить курсор на элементы в панели инструментов, чтобы выделить их на странице, или наоборот, наводить курсор на элементы страницы, чтобы найти их в DOM-дереве.
  3. Редактирование HTML и CSS:

    • В инструментах разработчика можно напрямую редактировать HTML и CSS, изменяя содержимое, структуру и стили элементов. Эти изменения отображаются в реальном времени, но не сохраняются после перезагрузки страницы.
  4. Использование консоли JavaScript:

    • Вы можете использовать консоль JavaScript для взаимодействия с DOM, выполняя JavaScript-код, который может создавать, изменять или удалять элементы DOM, добавлять обработчики событий и т.д.
  5. Просмотр и редактирование CSS:

    • В инструментах разработчика отображается также текущая таблица стилей для каждого элемента. Вы можете изменять существующие стили или добавлять новые, чтобы сразу видеть результаты своих действий.
  6. Отладка JavaScript:

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

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

Как получить DOM элемент в JS?

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

getElementById

Возвращает элемент по его идентификатору (атрибут id).

const element = document.getElementById('uniqueId');

getElementsByTagName

Возвращает живую коллекцию всех элементов с указанным тегом.

const elements = document.getElementsByTagName('div');

getElementsByClassName

Возвращает живую коллекцию всех элементов, которые имеют все указанные классы.

const elements = document.getElementsByClassName('someClass');

getElementsByName

Возвращает коллекцию всех элементов с указанным атрибутом name.

const elements = document.getElementsByName('name');

querySelector

Возвращает первый элемент, соответствующий указанному CSS-селектору.

const element = document.querySelector('.class');
const element = document.querySelector('#id');
const element = document.querySelector('div');

querySelectorAll

Возвращает статическую (не живую) NodeList всех элементов, соответствующих указанному CSS-селектору.

const elements = document.querySelectorAll('.class');
const elements = document.querySelectorAll('#id');
const elements = document.querySelectorAll('div');

Живые и не живые коллекции

  • Живая коллекция (например, возвращаемая getElementsByTagName) автоматически обновляется при изменении DOM. Это значит, что если вы добавите или удалите элементы, соответствующие критериям поиска, коллекция отразит эти изменения.
  • Не живая коллекция (например, возвращаемая querySelectorAll) представляет собой статический снимок состояния DOM в момент вызова метода. Последующие изменения в DOM не влияют на содержимое такой коллекции.

Выбор метода зависит от конкретной задачи и предпочтений разработчика. В современном JavaScript часто предпочитают использовать querySelector и querySelectorAll из-за их гибкости и удобства в работе с CSS-селекторами.

События DOM

События DOM — это сигналы, которые отправляются в результате взаимодействия пользователя с веб-страницей (например, клики, наведение курсора, нажатие клавиш) или в результате других браузерных действий (например, загрузка страницы, изменение размера окна).

Примеры событий:

  • click — Пользователь кликнул мышью.
  • load — Страница или изображение загружены.
  • mouseover и mouseout — Курсор мыши наведен на элемент или убран с него.
  • keydown, keypress, keyup — Пользователь нажал, удерживает или отпустил клавишу.
  • submit — Форма отправлена.
  • change — Изменено значение ввода.

Добавление обработчиков событий

Для отслеживания событий на элементах можно использовать метод addEventListener. Этот метод принимает три аргумента: тип события (строка), функцию-обработчик события и необязательный объект опций или булево значение, указывающее на использование захвата или всплытия события.

element.addEventListener('click', function(event) {
  console.log('Элемент был кликнут');
});

Удаление обработчиков событий

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

function handleClick(event) {
  console.log('Элемент был кликнут');
}

// Добавление обработчика события
element.addEventListener('click', handleClick);

// Удаление обработчика события
element.removeEventListener('click', handleClick);

Всплытие и захват событий

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

Предотвращение действия по умолчанию

В некоторых случаях действие, связанное с событием, может быть не желательным (например, отправка формы при клике на кнопку). Метод preventDefault объекта события позволяет отменить это действие.

element.addEventListener('click', function(event) {
  event.preventDefault();
  console.log('Клик без отправки формы');
});

Использование событий DOM и управление ими является основой для создания интерактивных веб-приложений.

Распространение событий (всплытие и перехват)

В DOM события распространяются через элементы по двум основным фазам: перехват (capturing) и всплытие (bubbling).

  1. Перехват (Capturing): Событие начинается от корня документа и движется вниз к целевому элементу, перехватываясь по пути всеми элементами, если для них установлены соответствующие слушатели событий на фазе перехвата. Эта фаза редко используется в повседневной практике веб-разработки.

  2. Целевая фаза: Событие достигает целевого элемента, на котором оно было инициировано.

  3. Всплытие (Bubbling): После достижения целевого элемента, событие начинает "всплывать" обратно к корню документа, последовательно вызывая обработчики на каждом элементе по пути.

Можно контролировать участие элемента в этих фазах при помощи третьего параметра в методе addEventListener. Установка его в true означает, что обработчик будет вызван во время фазы перехвата, а false (по умолчанию) — во время фазы всплытия.

// Ловит события на фазе всплытия
element.addEventListener('click', eventHandler, false);

// Ловит события на фазе перехвата
element.addEventListener('click', eventHandler, true);

Делегирование событий

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

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

// Установка слушателя событий на родительский элемент
document.getElementById('parentElement').addEventListener('click', function(event) {
    if (event.target && event.target.matches('li.targetClassName')) {
        console.log('Элемент списка был кликнут!');
    }
});

Здесь мы добавляем обработчик на родительский элемент (например, <ul> или <div>), а затем проверяем, соответствует ли элемент, на котором произошло событие (event.target), критериям выбора (например, является ли он элементом списка с классом targetClassName). Это позволяет обрабатывать события для всех текущих (и будущих) дочерних элементов, соответствующих критериям, без необходимости индивидуального назначения обработчиков.

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

Куки (cookies)

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

Установка куки

Для установки куки в JavaScript используется свойство document.cookie. Добавление куки состоит из присваивания строке document.cookie значения в формате ключ=значение. Также можно указать дополнительные атрибуты, такие как expires (срок действия), path (путь), domain (домен), secure (использовать ли только через HTTPS), и SameSite (ограничения на отправку куки с запросами между сайтами).

Пример установки куки:

document.cookie = "username=John Doe; expires=Thu, 18 Dec 2023 12:00:00 UTC; path=/";

Здесь куки с именем username и значением John Doe будет установлено с истечением срока действия 18 декабря 2023 года и будет доступно для всего сайта (path=/).

Получение куки

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

Пример чтения куки:

let cookies = document.cookie;
let cookiesArray = cookies.split('; ');
let cookieValue = cookiesArray.find(row => row.startsWith('username=')).split('=')[1];

Срок действия куки

Срок действия куки устанавливается через атрибут expires в формате UTC/GMT или через атрибут max-age, который определяет время жизни куки в секундах.

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

Пример с max-age:

document.cookie = "username=John Doe; max-age=3600; path=/"; // куки будет жить 1 час (3600 секунд)

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

Web Storages

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

LocalStorage

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

Пример использования localStorage:

// Сохранение данных
localStorage.setItem('key', 'value');

// Получение данных
let data = localStorage.getItem('key');

// Удаление одного ключа
localStorage.removeItem('key');

// Очистка всего localStorage для домена
localStorage.clear();

SessionStorage

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

Пример использования sessionStorage:

// Сохранение данных
sessionStorage.setItem('sessionKey', 'sessionValue');

// Получение данных
let sessionData = sessionStorage.getItem('sessionKey');

// Удаление одного ключа
sessionStorage.removeItem('sessionKey');

// Очистка всего sessionStorage для вкладки
sessionStorage.clear();

Сравнение LocalStorage и SessionStorage

  • Срок действия: localStorage хранит данные неограниченное время (до явного удаления), в то время как sessionStorage очищается при закрытии вкладки браузера.
  • Область видимости: Оба типа хранилища ограничены одним источником (одинаковым доменом и протоколом), но данные в sessionStorage не доступны между вкладками даже в рамках одного и того же сайта, в то время как данные в localStorage доступны во всех вкладках и окнах браузера для данного домена.

Выбор между localStorage и sessionStorage зависит от конкретных требований к приложению в плане продолжительности хранения данных и необходимости доступа к данным между вкладками или сессиями.

Веб-хранилище (Web Storage API) предоставляет два объекта для хранения данных на стороне клиента: localStorage и sessionStorage. Они оба имеют одинаковые методы для управления данными, но отличаются сроками хранения и областью видимости. Давайте рассмотрим основные операции с этими объектами и события, связанные с веб-хранилищем.

Установка, получение и очистка значений

Установка значений

Для сохранения данных используется метод setItem, который принимает два аргумента: ключ и значение. Значения могут быть только строками, так что если вы хотите сохранить объекты или массивы, вам нужно будет преобразовать их в строку, используя JSON.stringify.

localStorage.setItem('ключ', 'значение');
sessionStorage.setItem('ключ', 'значение');

Получение значений

Для извлечения данных используется метод getItem, который принимает один аргумент — ключ, под которым было сохранено значение.

let value = localStorage.getItem('ключ');
let sessionValue = sessionStorage.getItem('ключ');

Удаление значений

Для удаления конкретного элемента данных используется метод removeItem, а для полной очистки хранилища — метод clear.

localStorage.removeItem('ключ'); // Удалить конкретное значение
localStorage.clear(); // Очистить все локальное хранилище

sessionStorage.removeItem('ключ'); // Удалить конкретное значение
sessionStorage.clear(); // Очистить все сессионное хранилище

События хранилища

Событие storage запускается на объекте window, когда происходят изменения в данных localStorage или sessionStorage (кроме изменений в текущей вкладке для sessionStorage). Это позволяет отслеживать изменения хранилища в разных вкладках или окнах одного и того же браузера (для localStorage).

Событие storage не будет запускаться, если изменения произошли в той же вкладке/окне, что и слушатель события, для sessionStorage. Но для localStorage событие срабатывает и при изменениях в других вкладках/окнах.

window.addEventListener('storage', function(event) {
  console.log('Изменения в хранилище:', event);
  console.log(event.key); // Ключ, который был изменен
  console.log(event.oldValue); // Старое значение
  console.log(event.newValue); // Новое значение
  console.log(event.url); // URL документа, где были сделаны изменения
  console.log(event.storageArea); // Объект хранилища, где произошли изменения
});

Это событие содержит полезные данные, такие как key (ключ, по которому было произведено изменение), newValue и oldValue (новое и старое значения этого ключа), url (адрес страницы, на которой было произведено изменение) и storageArea (ссылка на объект localStorage или sessionStorage, где произошли изменения).

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

TypeScript

TS basic types

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

boolean

Логический тип данных, который принимает значения true или false.

let isCompleted: boolean = false;

number

Все числа в TypeScript являются числами с плавающей точкой. Этот тип данных включает в себя целые числа и числа с плавающей точкой.

let integer: number = 6;
let decimal: number = 6.4;
let hex: number = 0xf00d; // Шестнадцатеричное
let binary: number = 0b1010; // Двоичное
let octal: number = 0o744; // Восьмеричное

string

Тип данных для текстовых данных или строк.

let color: string = "blue";
color = 'red';

Вы можете использовать шаблонные строки, которые могут занимать несколько строк и иметь встроенные выражения.

let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }.
I'll be ${ age + 1 } years old next month.`;

array

TypeScript поддерживает два способа объявления массивов: либо с использованием типа элементов, за которым следует [], либо с использованием обобщенного массива Array<elemType>.

let list: number[] = [1, 2, 3];
let genericList: Array<number> = [1, 2, 3];

tuple

Кортежи позволяют выразить массив с фиксированным количеством элементов, чьи типы известны, но не обязательно одинаковы.

let x: [string, number];
x = ["hello", 10]; // OK
x = [10, "hello"]; // Error

enum

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

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

any

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

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

void

Противоположность any: отсутствие какого-либо типа. Обычно используется как тип возвращаемого значения функций, которые не возвращают значение.

function warnUser(): void {
    console.log("This is my warning message");
}

null и undefined

В TypeScript, null и undefined имеют свои типы названные null и undefined соответственно. По умолчанию они подтипы всех других типов. Это значит, что вы можете назначить null и undefined любому типу, хотя это не очень полезно.

never

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

function error(message: string): never {
    throw new Error(message);
}

Эти базовые типы являются строительными блоками в типизации TypeScript и позволяют разработчикам писать более безопасный и понятный код.

Enums

В TypeScript, enum (перечисление) — это способ организации набора связанных значений под одним именем. Перечисления могут быть числовыми, строковыми или гетерогенными (смешанными), хотя наиболее распространены числовые и строковые перечисления.

Числовые перечисления

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

enum Direction {
    Up = 1,
    Down,
    Left,
    Right,
}

console.log(Direction.Up); // 1
console.log(Direction.Left); // 3

Если первому элементу не присвоено значение, то нумерация начнётся с нуля.

Строковые перечисления

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

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

console.log(Direction.Up); // "UP"

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

Гетерогенные перечисления

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

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

Вычисляемые и константные члены

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

enum FileAccess {
    // константные члены
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // вычисляемый член
    G = "123".length,
}

Преимущества использования enum

  • Ясность намерений и документации. Использование перечислений делает код более читабельным и понятным.
  • Уменьшение ошибок. Так как значения строго определены, вероятность ошибок из-за неверного использования строк или чисел уменьшается.
  • Упрощение изменений. Если потребуется изменить значение одного из элементов перечисления, его достаточно обновить в одном месте.

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

Type / Interface, разница между ними

В TypeScript, type и interface являются двумя мощными средствами для объявления типов. Хотя они имеют много общего и во многих случаях могут быть использованы взаимозаменяемо, существуют ключевые различия, которые могут повлиять на выбор между ними в зависимости от конкретной ситуации.

Interface (Интерфейс)

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

interface User {
    name: string;
    age: number;
}

interface Admin extends User {
    permissions: string[];
}

const admin: Admin = {
    name: "John",
    age: 30,
    permissions: ["create", "edit", "delete"],
};

Интерфейсы поддерживают объединение (declaration merging), что означает, что вы можете объявить интерфейс несколько раз, и все объявления будут автоматически объединены в одно.

interface User {
    name: string;
}

interface User {
    age: number;
}

// Теперь User имеет и name, и age
const user: User = {
    name: "Alice",
    age: 25,
};

Type (Тип)

Type aliases (псевдонимы типов) позволяют задать типу собственное имя. Они могут быть использованы для примитивных типов, объединений (unions), пересечений (intersections) и для описания формы объектов, так же как и интерфейсы.

type User = {
    name: string;
    age: number;
};

type Permissions = "read" | "write" | "delete";

type Admin = User & {
    permissions: Permissions[];
};

const admin: Admin = {
    name: "Bob",
    age: 36,
    permissions: ["read", "write"],
};

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

Основные различия

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

Когда использовать каждый

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

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

Function types

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

Описание функции через типы параметров и возвращаемого значения

Можно явно указать типы для параметров функции и тип возвращаемого значения.

function add(x: number, y: number): number {
    return x + y;
}

В этом примере функция add принимает два параметра типа number и также возвращает значение типа number.

Использование типовой сигнатуры функции

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

type Add = (x: number, y: number) => number;

const add: Add = function(x, y) {
    return x + y;
};

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

Интерфейсы для функций

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

interface Add {
    (x: number, y: number): number;
}

const add: Add = function(x, y) {
    return x + y;
};

Опциональные и параметры по умолчанию

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

function buildName(firstName: string, lastName?: string): string {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

function buildNameDefault(firstName: string, lastName = "Smith"): string {
    return firstName + " " + lastName;
}

Остаточные параметры

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

function buildName(firstName: string, ...restOfName: string[]): string {
    return firstName + " " + restOfName.join(" ");
}

const employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

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

Generic types

Обобщения (Generic types) в TypeScript — это мощный инструмент, который позволяет создавать компоненты, способные работать с любым типом данных. Основная идея обобщений заключается в возможности создания классов, интерфейсов, функций и типов, которые не фиксируют конкретный тип данных на момент их объявления, но позволяют задать эти типы при использовании. Это делает код более гибким и повторно используемым, позволяя избежать дублирования и ошибок типизации.

Пример с функцией

Без обобщений:

function identity(arg: number): number {
    return arg;
}

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

function identity<T>(arg: T): T {
    return arg;
}

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

let output = identity<string>("myString");

Пример с интерфейсом

Без обобщений:

interface GenericIdentityFn {
    (arg: number): number;
}

С обобщениями:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

Теперь мы можем использовать интерфейс GenericIdentityFn с любым типом данных:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

Пример с классом

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

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

Преимущества использования обобщений

  • Повышение переиспользования кода: Мы можем писать универсальные компоненты, которые работают с различными типами данных.
  • Безопасность типов: TypeScript проверяет типы на этапе компиляции, предотвращая многие ошибки, связанные с неверным использованием типов.
  • Гибкость: Обобщения позволяют создавать более гибкие и мощные абстракции, чем это возможно при использовании конкретных типов данных.

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

Type guards

Type guards (иногда также называемые type predicates) - это конструкции в языках программирования, которые позволяют проверить тип данных во время выполнения программы и использовать эту информацию для принятия решений или изменения поведения кода.

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

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

Вот пример типичного использования type guards на TypeScript:

interface Cat {
    meow(): void;
}

interface Dog {
    bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
}

function makeSound(animal: Cat | Dog) {
    if (isCat(animal)) {
        animal.meow();
    } else {
        animal.bark();
    }
}

В этом примере isCat - это type guard, который проверяет, является ли переданное животное экземпляром Cat. Затем функция makeSound использует этот type guard для определения того, какой метод вызвать для переданного животного.

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

Testing Types

Testing Types (тестирование типов) - это процесс проверки соответствия типов данных в программе заявленным требованиям или спецификациям. В контексте программирования, особенно в языках с сильной статической типизацией, таких как TypeScript, тестирование типов становится важной частью процесса разработки.

Основные цели тестирования типов включают в себя:

  1. Обеспечение соответствия интерфейсам и соглашениям: Типы данных в программе должны соответствовать интерфейсам и ожиданиям других частей программы. Тестирование типов помогает обнаружить неправильные типы данных, которые могут привести к ошибкам выполнения программы.

  2. Предотвращение ошибок во время выполнения: Проверка типов данных во время компиляции или перед выполнением кода помогает выявить ошибки, которые могли бы произойти во время выполнения программы из-за несоответствия типов.

  3. Обеспечение безопасности и надежности: Правильные типы данных повышают безопасность и надежность программы, так как они помогают избежать ошибок и непредвиденного поведения.

  4. Улучшение читаемости и понимания кода: Хорошо определенные типы данных делают код более понятным и читаемым. Тестирование типов помогает убедиться, что типы данных правильно документированы и используются согласно ожиданиям.

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

Testing

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

Test Pyramid

Test Pyramid (Тестовая пирамида) - это концепция, представленная Мартином Фаулером (Martin Fowler), которая описывает оптимальную структуру тестирования программного обеспечения. Эта концепция предполагает, что тесты должны быть структурированы в виде пирамиды, где каждый уровень представляет собой различный тип тестов с разным уровнем детализации и скорости выполнения. Основная идея состоит в том, чтобы иметь большее количество тестов на нижних уровнях пирамиды (быстрые и дешевые тесты), и меньшее количество на верхних уровнях (медленные и дорогие тесты).

Вот основные уровни тестовой пирамиды:

  1. Unit Tests (Тесты модульные):

    • Это тесты, которые проверяют отдельные модули или компоненты программного обеспечения в изоляции от других частей системы.
    • Они обычно являются быстрыми в выполнении и дешевыми в создании.
    • Целью модульных тестов является проверка корректности работы отдельных функций или методов.
    • Этот уровень пирамиды должен содержать наибольшее количество тестов.
  2. Integration Tests (Тесты интеграционные):

    • Это тесты, которые проверяют взаимодействие между различными модулями или компонентами программного обеспечения.
    • Они обычно занимают больше времени на выполнение и требуют больше ресурсов, чем модульные тесты.
    • Целью интеграционных тестов является проверка корректности взаимодействия между компонентами и модулями.
  3. End-to-End Tests (Тесты конечных точек):

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

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

About