12 концепций, которые прокачают ваш JavaScript

Обложка поста

Перевод статьи «12 Concepts That Will Level Up Your JavaScript Skills»

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

Присвоения примитивных и ссылочных типов

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

JavaScript всегда использует присвоение по значению. И это очень важно: когда присваиваемое значение является одним из 5 JavaScript’овых примитивов (Boolean, null, undefined, String и Number) — присваивается фактическое значение. Однако, когда присваиваемое значение является типом Array, Function или Object, присваивается ссылка на объект в памяти.

Например, в коде ниже переменной var2 присваивается значение var1. Т. к. var1 является примитивом (String), то переменной var2 присваивается строковое значение var1, и она может рассматриваться как отдельная (независимая) переменная. Соответственно, изменения var2 никак не отразятся на var1.

let var1 = 'My string';
let var2 = var1;

var2 = 'My new string';

console.log(var1);
// 'My string'
console.log(var2);
// 'My new string'

А теперь попробуем то же самое с типом Object.

let var1 = { name: 'Jim' }
let var2 = var1;

var2.name = 'John';

console.log(var1);
// { name: 'John' }
console.log(var2);
// { name: 'John' }

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

Замыкания

Замыкания — важный паттерн для приватизации переменной. В примере ниже createGreeter возвращает анонимную функцию, которой доступна переданная переменная greeting со значением «Hello». После эта переменная будет доступна для sayHello.

function createGreeter(greeting) {
 return function(name) {
   console.log(greeting + ', ' + name);
 }
}

const sayHello = createGreeter('Hello');

sayHello('Joe');
// Hello, Joe

Или же более «правдоподобный» пример. У вас может быть некая функция apiConnect(apiKey), которая возвращает некоторые методы с использованием API ключа. В таком случае этот ключ нужно передать только один раз.

function apiConnect(apiKey) {
 function get(route) {
   return fetch(`${route}?key=${apiKey}`);
 }

 function post(route, params) {
   return fetch(route, {
     method: 'POST',
     body: JSON.stringify(params),
       headers: {
         'Authorization': `Bearer ${apiKey}`
       }
     })
 }
 return { get, post }
}

const api = apiConnect('my-secret-key');

// Больше передавать ключ не нужно
api.get('http://www.example.com/get-endpoint');
api.post('http://www.example.com/post-endpoint', { name: 'Joe' });

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

Деструктуризация — это просто способ извлечения свойств из объектов.

const obj = {
 name: 'Joe',
 food: 'cake'
}
const { name, food } = obj;
console.log(name, food);
// 'Joe' 'cake'

Если вам нужно извлечь свойство, дав ему другое имя, — делайте так:

const obj = {
 name: 'Joe',
 food: 'cake'
}

const { name: myName, food: myFood } = obj;
console.log(myName, myFood);

// 'Joe' 'cake'

В следующем примере деструктуризация применяется для «чистой» передачи объекта person в функцию introduce. Иначе говоря, деструктуризация может использоваться для непосредственного извлечения передаваемых параметров. Для тех, кто разрабатывает на React, это может показаться знакомым.

const person = {
 name: 'Eddie',
 age: 24
}

function introduce({ name, age }) {
 console.log(`I'm ${name} and I'm ${age} years old!`);
}

console.log(introduce(person));
// "I'm Eddie and I'm 24 years old!"

Spread

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

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

const arr = [4, 6, -1, 3, 10, 4];
const max = Math.max(...arr);

console.log(max);
// 10

Rest-параметры

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

function myFunc(...args) {
 console.log(args[0] + args[1]);
}

myFunc(1, 2, 3, 4);
// 3

Методы массивов

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

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

map, filter, reduce

В методах map(), filter() и reduce() иногда можно запутаться. Они полезны для трансформации массива или возвращения его агрегатного значения.

  • map(): возвращает массив, в котором каждый элемент изменяется с помощью переданной функции.
    const arr = [1, 2, 3, 4, 5, 6];
    const mapped = arr.map(el => el + 20);
    
    console.log(mapped);
    // [21, 22, 23, 24, 25, 26]
  • filter(): возвращает массив с теми элементами, в которых переданная функция возвращает true.
    const arr = [1, 2, 3, 4, 5, 6];
    const filtered = arr.filter(el => el === 2 || el === 4);
    
    console.log(filtered);
    // [2, 4]
  • reduce(): работа с элементами с сохранением промежуточного результата.
    const arr = [1, 2, 3, 4, 5, 6];
    const reduced = arr.reduce((total, current) => total + current);
    
    console.log(reduced);
    // 21

find, findIndex, indexOf

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

  • find(): возвращает первый элемент массива, удовлетворяющий определенному условию.
    const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    const found = arr.find(el => el > 5);
    
    console.log(found);
    // 6

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

  • findIndex(): метод идентичен find(), но вместо возвращаемого значения здесь возвращается индекс первого подходящего элемента.
    const arr = ['Nick', 'Frank', 'Joe', 'Frank'];
    const foundIndex = arr.findIndex(el => el === 'Frank');
    
    console.log(foundIndex);
    // 1
  • indexOf(): метод идентичен findIndex(), но вместо функции он принимает искомое значение. Используйте этот метод в тех случаях, когда вам не нужна функция для проверки элемента на совпадение.
    const arr = ['Nick', 'Frank', 'Joe', 'Frank'];
    const foundIndex = arr.indexOf('Frank');
    
    console.log(foundIndex);
    // 1

push, pop, shift, unshift

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

  • push(): этот метод относительно простой. Он добавляет элемент в конец массива. Метод модифицирует массив и одновременно возвращает добавленный элемент.
    let arr = [1, 2, 3, 4];
    const pushed = arr.push(5);
    
    console.log(arr);
    // [1, 2, 3, 4, 5]
    
    console.log(pushed);
    // 5
    
  • pop(): метод удаляет последний элемент массива. Как и в прошлом случае, метод изменяет массив и одновременно возвращает удалённый элемент.
    let arr = [1, 2, 3, 4];
    const popped = arr.pop();
    
    console.log(arr);
    // [1, 2, 3]
    
    console.log(popped);
    // 4
  • shift(): этот метод удаляет первый элемент массива. Метод изменяет массив и возвращает удалённый элемент.
    let arr = [1, 2, 3, 4];
    const shifted = arr.shift();
    
    console.log(arr);
    // [2, 3, 4]
    
    console.log(shifted);
    // 1
  • unshift(): добавляет один или несколько элементов в начало массива. Как и прошлые методы, он изменяет массив, но возвращает новую длину массива.
    let arr = [1, 2, 3, 4];
    const unshifted = arr.unshift(5, 6, 7);
    
    console.log(arr);
    // [5, 6, 7, 1, 2, 3, 4]
    
    console.log(unshifted);
    // 7

splice, slice

Эти методы либо изменяют, либо возвращают подмассив элементов.

  • splice(): метод изменяет массив, удаляя или заменяя существующий элемент, и/или добавляет новый. Метод только изменяет массив. Код ниже можно объяснить так: в позиции 1 массива удалить 0 элементов и вставить b.
    let arr = ['a', 'c', 'd', 'e'];
    arr.splice(1, 0, 'b')
  • slice(): возвращает подмассив элементов массива, начиная и заканчивая на определённой позиции. Если конечная позиция не указана, возвращается остаток массива. Важно понимать, что этот метод не модифицирует массив, а только возвращает подмассив.
    let arr = ['a', 'b', 'c', 'd', 'e'];
    const sliced = arr.slice(2, 4);
    
    console.log(sliced);
    // ['c', 'd']
    
    console.log(arr);
    // ['a', 'b', 'c', 'd', 'e']

sort

Метод сортирует массив, основываясь на функции, которая принимает первый и второй элемент. Этот метод изменяет сам массив. Если в массиве порядок элементов не был изменен, метод возвращает 0, если изменён — 1.

let arr = [1, 7, 3, -1, 5, 7, 2];
const sorter = (firstEl, secondEl) => firstEl - secondEl;
arr.sort(sorter);

console.log(arr);
// [-1, 1, 2, 3, 5, 7, 7]

Генераторы

Не забываем и про них. Генератор определяет, какое значение будет возвращено при следующем вызове next().

function* greeter() {
 yield 'Hi';
 yield 'How are you?';
 yield 'Bye';
}

const greet = greeter();

console.log(greet.next().value);
// 'Hi'

console.log(greet.next().value);
// 'How are you?'

console.log(greet.next().value);
// 'Bye'

console.log(greet.next().value);
// undefined

А вот пример использования генератора для бесконечных значений:

function* idCreator() {
 let i = 0;
 while (true)
   yield i++;
}

const ids = idCreator();

console.log(ids.next().value);
// 0

console.log(ids.next().value);
// 1

console.log(ids.next().value);
// 2
// etc...

Разница операторов сравнения (===) и (==)

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

console.log(0 == '0');
// true

console.log(0 === '0');
// false

Сравнение объектов

Частая ошибка у новичков в JavaScript — это неправильное сравнение объектов. Дело в том, что переменные объектов хранят  в памяти ссылку на объект, а не сам объект. Один из способов сравнения двух объектов — предварительное преобразование их в JSON строку. Однако у этого способа есть недостаток: не факт, что порядок в объекте сохранится. Более безопасный способ сравнения объектов — использование специальной библиотеки, которая сравнивает объекты на более глубоком уровне (к примеру isEqual от loadash)

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

const joe1 = { name: 'Joe' };
const joe2 = { name: 'Joe' };

console.log(joe1 === joe2);
// false

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

const joe1 = { name: 'Joe' };
const joe2 = joe1;

console.log(joe1 === joe2);
// true

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

Callback-функции

Многие избегают callback-функции (функции обратного вызова). А зря — ведь это довольно просто! В следующем примере console.log() будет передан в myFunc() в качестве callback-функции.

function myFunc(text, callback) {
 setTimeout(function() {
   callback(text);
 }, 2000);
}

myFunc('Hello world!', console.log);
// 'Hello world!'

Промисы

Как только вы начнёте понимать работу callback’ов в JavaScript, возможно, совсем скоро вы окажетесь в «аду обратных вызовов». На помощь приходят промисы (англ. Promises). Оберните свою асинхронную логику в промисы: resolve — для успехов, reject — для фейлов. Используйте then для обработки успеха и catch — для обработки фейлов.

const myPromise = new Promise(function(res, rej) {
 setTimeout(function(){
   if (Math.random() < 0.9) {
     return res('Hooray!');
   }

   return rej('Oh no!');
 }, 1000);
});

myPromise.then(function(data) {
   console.log('Success: ' + data);
  })
  .catch(function(err) {
   console.log('Error: ' + err);
  });
 
// Если Math.random() возвращает меньше 0.9 то выведется:
// "Success: Hooray!"

// Если Math.random() возвращает 0.9 или больше, то выведется:
// "Error: On no!"

Async Await

Как только вы разберётесь в промисах, вам может понравиться async await — это«синтаксический сахар» поверх промисов. Ниже пример async функции с await и промисом.

const greeter = new Promise((res, rej) => {
 setTimeout(() => res('Hello world!'), 2000);
})

async function myFunc() {
 const greeting = await greeter;
 console.log(greeting);
}

myFunc();
// 'Hello world!'

Заключение

Если вы не знали ни об одной из этих 12 концепций — не стоит откладывать их изучение в долгий ящик. А если знали — практика лишней никогда не будет.

Не смешно? А здесь смешно: @ithumor