Концепции JavaScript, которые необходимо знать перед изучением React
13975

Концепции JavaScript, которые необходимо знать перед изучением React


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

Многие разработчики выбирают подход "учиться по ходу дела" при изучении React. Но это часто не приводит к повышению эффективности, а наоборот, увеличивает пробелы в знаниях JavaScript. Такой подход делает усвоение каждой новой возможности JavaScript в 2 раза сложнее (вы можете вообще начать путать JavaScript с React).

React - это JavaScript-фреймворк для построения пользовательских интерфейсов на основе компонентов. Весь его код написан на JavaScript, включая HTML-разметку, которая пишется на JSX (это позволяет разработчикам легко писать HTML и JavaScript вместе).

В этом статье мы на практике рассмотрим все идеи и техники JS, которые вам необходимо усвоить перед изучением React.

React построен с использованием современных возможностей JavaScript, которые были представлены в основном в ECMAScript 6 (ES-2015). Именно об этом мы и поговорим в этой статье.

Функции обратного вызова в JavaScript


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

Обратные вызовы очень важно понимать, поскольку они используются в методах массивов (таких как map(), filter() и так далее), setTimeout(), слушателях событий (таких как click, scroll и так далее) и во многих других местах.

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

//HTML
<button class="btn">Click Me</button>

//JavaScript
const btn = document.querySelector('.btn');

btn.addEventListener('click', () => {
  let name = 'John doe';
  console.log(name.toUpperCase())
})

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

Обещания (promises, промисы) в JavaScript


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

Например, попробуем вывести в консоль 5 имен через 2 секунды каждое - то есть первое имя появляется через 2 секунды, второе через 4 секунды и так далее:

setTimeout(() => {
    console.log("Joel");
    setTimeout(() => {
        console.log("Victoria");
        setTimeout(() => {
            console.log("John");
            setTimeout(() => {
                console.log("Doe");
                setTimeout(() => {
                    console.log("Sarah");
                }, 2000);
            }, 2000);
        }, 2000);
    }, 2000);
}, 2000);

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

Основная причина использования промисов - предотвращение "ада обратных вызовов". С помощью промисов мы можем писать асинхронный код синхронно.

Промис - это объект, возвращающий значение, которое вы ожидаете увидеть в будущем, но не видите сейчас.

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

Синтаксис промисов JavaScript:

const myPromise = new Promise((resolve, reject) => {  
    // condition
});

Промисы имеют два параметра, один для успеха (resolve) и один для неудачи (reject). Каждый из них имеет условие, которое должно быть выполнено, чтобы промис был разрешен - в противном случае он будет отклонен:

const promise = new Promise((resolve, reject) => {  
    let condition;
    
    if(condition is met) {    
        resolve('Promise is resolved successfully.');  
    } else {    
        reject('Promise is rejected');  
    }
});

Существует 3 состояния объекта Promise:

  • Pending: по умолчанию это начальное состояние, до того, как промис станет успешным или неудачным.
  • Resolved: завершенный промис
  • Rejected: промис не выполнен

Наконец, давайте попробуем реализовать "ад обратного вызова" в виде промиса:

function addName(time, name) {
  return new Promise ((resolve, reject) => {
    if(name) {
      setTimeout(()=>{
        console.log(name)
        resolve();
      },time)
    } else {
      reject('No such name');
    }
  })
}

addName(2000, 'Joel')
  .then(()=>addName(2000, 'Victoria'))
  .then(()=>addName(2000, 'John'))
  .then(()=>addName(2000, 'Doe'))
  .then(()=>addName(2000, 'Sarah'))
  .catch((err)=>console.log(err))

Map() в JavaScript


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

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

let users = [
  { firstName: "Susan", lastName: "Steward", age: 14, hobby: "Singing" },
  { firstName: "Daniel", lastName: "Longbottom", age: 16, hobby: "Football" },
  { firstName: "Jacob", lastName: "Black", age: 15, hobby: "Singing" }
];

Мы можем выполнить цикл с использованием map() и изменить его вывод

Следует отметить, что:

  • map() всегда возвращает новый массив, даже если это пустой массив.
  • он не изменяет размер исходного массива по сравнению с методом filter()
  • при создании нового массива всегда используются значения из исходного массива.

Метод map() работает почти как любой другой итератор JavaScript, например forEach(), но правильнее всегда использовать метод map(), когда вы собираетесь возвращать значение.

Одна из основных причин, по которой мы используем map(), заключается в том, что мы можем инкапсулировать наши данные в HTML, в то время как в React это просто делается с помощью JSX.

Filter() и Find() в JavaScript


Filter() возвращает новый массив в зависимости от определенных критериев. В отличие от map(), он может изменять размер нового массива, тогда как find() возвращает только один экземпляр (это может быть объект или элемент). Если существует несколько совпадений, возвращается первое совпадение - в противном случае возвращается undefined.

Предположим, у вас есть массив с коллекцией зарегистрированных пользователей разного возраста:

let users = [
  { firstName: "Susan", age: 14 },
  { firstName: "Daniel", age: 16 },
  { firstName: "Bruno", age: 56 },
  { firstName: "Jacob", age: 15 },
  { firstName: "Sam", age: 64 },
  { firstName: "Dave", age: 56 },
  { firstName: "Neils", age: 65 }
];

Вы можете отсортировать эти данные по возрастным группам, например, молодые люди (возраст 1-15 лет), пожилые люди (возраст 50-70 лет) и так далее...

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

// for young people
const youngPeople = users.filter((person) => {
  return person.age <= 15;
});

//for senior people
const seniorPeople = users.filter((person) => person.age >= 50);

console.log(seniorPeople);
console.log(youngPeople); 

Создается новый массив. Если условие не выполнено (нет совпадений), создается пустой массив.

Find()


Метод find(), как и метод filter(), выполняет итерации по массиву в поисках экземпляра/элемента, удовлетворяющего заданному условию. Найдя его, он возвращает этот элемент массива и немедленно завершает цикл. Если совпадений не обнаружено, функция возвращает undefined.

Например:

const Bruno = users.find((person) => person.firstName === "Bruno");

console.log(Bruno);

Деструктурирующее присваивание в JavaScript


Деструктурирующее присваивание - это фича JavaScript, появившаяся в ES6, которая позволяет быстрее и проще получать и распаковывать переменные из массивов и объектов.

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

let fruits= ["Mango", "Pineapple" , "Orange", "Lemon", "Apple"];

let fruit1 = fruits[0];
let fruit2 = fruits[1];
let fruit3 = fruits[2];

console.log(fruit1, fruit2, fruit3); //"Mango" "Pineapple" "Orange"

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

let [fruit1, fruit2, fruit3] = fruits;

console.log(fruit1, fruit2, fruit3); //"Mango" "Pineapple" "Orange"

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

const [fruit1 ,,,, fruit5] = fruits;
const [,fruit2 ,, fruit4,] = fruits;

Разбор (деструктуризация) объекта в JavaScript


Давайте теперь посмотрим, как можно деструктурировать объект - потому что в React вы будете делать много деструктуризаций объектов.

Предположим, у нас есть объект user, который содержит имя, фамилию и многое другое

const Susan = {
  firstName: "Susan",
  lastName: "Steward",
  age: 14,
  hobbies: {
    hobby1: "singing",
    hobby2: "dancing"
  }
};

При старом способе получение этих данных могло быть полным повторений:

const firstName = Susan.firstName;
const age = Susan.age;
const hobby1 = Susan.hobbies.hobby1;

console.log(firstName, age, hobby1); //"Susan" 14 "singing"

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

const {firstName, age, hobbies:{hobby1}} = Susan;

console.log(firstName, age, hobby1); //"Susan" 14 "singing"

Мы также можем применить его внутри функции:

function individualData({firstName, age, hobbies:{hobby1}}){
  console.log(firstName, age, hobby1); //"Susan" 14 "singing"
}
individualData(Susan);

Остаточные параметры и оператор расширения в JavaScript


В JavaScript операторы spread и rest используют три точки ... . Оператор rest собирает элементы - он помещает "остаток" некоторых определенных значений, предоставленных пользователем, в массив/объект JavaScript.

Предположим, у вас есть массив фруктов:

let fruits= ["Mango", "Pineapple" , "Orange", "Lemon", "Apple"];

Мы можем деструктурировать массив, чтобы получить первый и второй фрукты, а затем поместить "остальные" фрукты в массив, используя оператор rest.

const [firstFruit, secondFruit, ...rest] = fruits

console.log(firstFruit, secondFruit, rest); //"Mango" "Pineapple" ["Orange","Lemon","Apple"]

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

const chosenFruit = rest.find((fruit) => fruit === "Apple");

console.log(`This is an ${chosenFruit}`); //"This is an Apple"

Оператор расширения (Spread) в JavaScript


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

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

Например, предположим, что у вас есть массивы различных видов животных:

let pets = ["cat", "dog" , "rabbits"];

let carnivorous = ["lion", "wolf", "leopard", "tiger"];

Возможно, вы захотите объединить эти два массива в один массив животных. Давайте попробуем это сделать:

let animals = [pets, carnivorous];

console.log(animals); //[["cat", "dog" , "rabbits"], ["lion", "wolf", "leopard", "tiger"]]

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

let animals = [...pets, ...carnivorous];

console.log(animals); //["cat", "dog" , "rabbits", "lion", "wolf", "leopard", "tiger"]

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

Например:

let name = {firstName:"John", lastName:"Doe"};
let hobbies = { hobby1: "singing", hobby2: "dancing" }
let myInfo = {...name, ...hobbies};

console.log(myInfo); //{firstName:"John", lastName:"Doe", hobby1: "singing", hobby2: "dancing"}

Reduce() в JavaScript


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

Когда вы соединяете метод map() и метод filter() вместе, вы выполняете работу дважды - сначала фильтруете каждое отдельное значение, а затем сопоставляете оставшиеся значения. С другой стороны, reduce() позволяет фильтровать и отображать данные за один проход. Этот метод является мощным, но он также немного сложнее и замысловатее.

Мы итерируем наш массив, а затем получаем функцию обратного вызова, которая похожа на map(), filter(), find() и другие. Главное отличие заключается в том, что она сводит наш массив к одному значению, которое может быть числом, массивом или объектом.

Еще одна вещь, которую следует помнить о методе reduce(), заключается в том, что мы передаем два аргумента.

Первый аргумент - это сумма/итог всех вычислений, а второй - текущее значение итерации.

Например, предположим, что у нас есть список зарплат для наших сотрудников:

let staffs = [
  { name: "Susan", age: 14, salary: 100 },
  { name: "Daniel", age: 16, salary: 120 },
  { name: "Bruno", age: 56, salary: 400 },
  { name: "Jacob", age: 15, salary: 110 },
  { name: "Sam", age: 64, salary: 500 },
  { name: "Dave", age: 56, salary: 380 },
  { name: "Neils", age: 65, salary: 540 }
];

И мы хотим рассчитать налог в размере 10% для всех сотрудников. Мы могли бы легко сделать это с помощью метода reduce(), но перед этим давайте поступим проще: сначала рассчитаем общую зарплату.

const totalSalary = staffs.reduce((total, staff) => {
  total += staff.salary;
  return total;
},0)
console.log(totalSalary); // 2150

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

const salaryInfo = staffs.reduce(
  (total, staff) => {
    let staffTithe = staff.salary * 0.1;
    total.totalTithe += staffTithe;
    total['totalSalary'] += staff.salary;
    return total;
  },
  { totalSalary: 0, totalTithe: 0 }
);

console.log(salaryInfo); // { totalSalary: 2150 , totalTithe: 215 }

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

Опциональная цепочка в JavaScript


Опциональная цепочка - это безопасный способ доступа к вложенным свойствам объектов в JavaScript, который позволяет избежать многократной проверки null при доступе к длинной цепочке свойств объекта. Это новая возможность, введенная в ES2020.

Например:

let users = [
{
    name: "Sam",
    age: 64,
    hobby: "cooking",
    hobbies: {
      hobb1: "cooking",
      hobby2: "sleeping"
    }
  },
  { name: "Bruno", age: 56 },
  { name: "Dave", age: 56, hobby: "Football" },
  {
    name: "Jacob",
    age: 65,
    hobbies: {
      hobb1: "driving",
      hobby2: "sleeping"
    }
  }
];

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

users.forEach((user) => {
  console.log(user.hobbies.hobby2);
});

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

Выходные данные:

"sleeping"
error: Uncaught TypeError: user.hobbies is undefined

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

Метод рендеринга с помощью условий:

users.forEach((user) => {
  console.log(user.hobbies && user.hobbies.hobby2);
});

Опциальная цепочка:

users.forEach((user) => {
  console.log(user ?.hobbies ?.hobby2);
});

Вывод

"sleeping"
undefined
undefined
"sleeping"

Fetch API и ошибки в JavaScript


Fetch API, как следует из названия, используется для получения данных из API. Это API браузера, который позволяет вам использовать JavaScript для выполнения основных запросов AJAX (асинхронный JavaScript и XML).

Поскольку он предоставляется браузером, вы можете использовать его без необходимости установки или импорта каких-либо пакетов или зависимостей (как, например, axios). Его конфигурация довольно проста для понимания. По умолчанию API fetch доставляет промис (об обещаниях/промисах я рассказывал ранее в этой статье).

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

fetch("https://type.fit/api/quotes")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

Мы сделали следующее:

Строка 1: мы получили данные из API, которые вернули промис

Строка 2: Затем мы получили формат данных .json(), который также является промисом.

Строка 3: Мы получили наши данные, которые теперь возвращают JSON

Строка 4: Мы получили ошибки, если таковые имеются

Мы увидим, как это можно сделать с помощью async/await в следующем разделе.

Как обрабатывать ошибки в API Fetch


Теперь давайте рассмотрим, как мы можем обрабатывать ошибки в fetch API, не прибегая к помощи ключевого слова catch. Функция fetch() автоматически выбрасывает ошибку при сетевых ошибках, но не при ошибках HTTP, таких как ответы 400-5xx.

Хорошей новостью является то, что fetch предоставляет простой флаг response.ok, который указывает на то, что запрос не прошел или код состояния HTTP-ответа находится в диапазоне успешных.

Это очень просто реализовать:

fetch("https://type.fit/api/quotes")
  .then((response) => {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response.json();
  })
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

Async/Await в JavaScript


Async/Await позволяет нам писать асинхронный код синхронно. Это означает, что вам не нужно делать вложение обратных вызовов.

Асинхронная функция всегда возвращает промис.

Возможно, вы ломаете голову над тем, что означает разница между синхронным и асинхронным. Проще говоря, синхронный означает, что задания выполняются одно за другим. Асинхронный означает, что задания выполняются независимо друг от друга.

Обратите внимание, что перед функцией всегда стоит async, и мы можем использовать await только тогда, когда у нас есть async. Скоро вы все поймете!

Давайте теперь реализуем код Fetch API, над которым мы работали ранее, используя async/await:

const fetchData = async () =>{
  const quotes = await fetch("https://type.fit/api/quotes");
  const response = await quotes.json();
  console.log(response);
}

fetchData();

Так гораздо легче читать, правда?

Возможно, вам интересно, как мы можем обрабатывать ошибки с помощью async/await. Да! Вы используете ключевые слова try и catch:

const fetchData = async () => {
  try {
    const quotes = await fetch("https://type.fit/api/quotes");
    const response = await quotes.json();
    console.log(response);
  } catch (error) {
    console.log(error);
  }
};

fetchData();

Заключение


В этой статье мы изучили более 10 методов и концепций JavaScript, которые каждый разработчик должен досконально понять и освоить перед изучением React.