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

1. Значения и ссылки
Не разобравшись, как язык JavaScript работает с переменными, вы не сможете избежать багов в ваших программах.
При присваивании примитивного типа данных (булево значение, число, строка) происходит его полное копирование. В переменную помещается актуальное значение, равное исходному, но никак с ним не связанное.
А вот объекты, в том числе массивы и функции, всегда передаются по ссылке. После присваивания такого значения вы найдете в переменной ссылку на область памяти, где хранится исходный объект. Таким образом, полная копия не создается.
let variable1 = 'My string'; let variable2 = variable1;
В переменной variable2 теперь лежит примитивное строковое значение 'My string', полностью скопированное с variable1. Эти переменные равны, но независимы друг от друга. Изменение variable2 никак не повлияет на variable1.
variable2 = 'My new string'; console.log(variable1); // My string console.log(variable2); // My new string
А что там с объектами?
let variable1 = { name: 'Jim' }
let variable2 = variable1;
Сейчас в variable2 лежит объект, очень похожий на variable1. На самом деле это один и тот же объект, что очень просто проверить.
variable2.name = 'John';
console.log(variable1); // { name: 'John' }
console.log(variable2); // { name: 'John' }
Представьте, какие ошибки может вызвать такое поведение, если вы про него не думаете. Чаще всего проблемы начинаются в функциях, которые принимают объекты как входные данные и изменяют их.
Хотите глубже изучить JavaScript основы языка? Тогда держите гайд по базовым концепциям и руководство для джуна:
- JS-гайд: основные концепции JavaScript с примерами кода
- Путь JavaScript Junior: подборка лучших ресурсов для обучения
2. Замыкания
Замыкание – это одна из важнейших JavaScript-концепций, которая позволяет запретить доступ извне к переменным и функциям.
Из функции createGreeter возвращается анонимная функция, которая всегда будет иметь доступ к параметру greeting. Но никто кроме нее уже не сможет к нему обратиться.
function createGreeter(greeting) {
return function(name) {
console.log(greeting + ', ' + name);
}
}
const sayHello = createGreeter('Hello');
sayHello('Joe'); // Hello, Joe
В реальной разработке замыкания могут пригодиться в 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-ключ,
// он сохранен в замыкании функции api
api.get('http://www.example.com/get-endpoint');
api.post('http://www.example.com/post-endpoint', { name: 'Joe' });
Если замыкания для вас – все еще темный лес, мы подготовили подробный разбор этой концепции:
- Пора понять замыкания в JavaScript! Часть 1. Готовим фундамент
- Пора понять замыкания в JavaScript! Часть 2. Переходим к делу
3. Деструктуризация
Деструктуризация (деструктурирующее присваивание) в JavaScript – это очень крутой способ извлечения данных, упакованных в объекты.
const obj = {
name: 'Joe',
food: 'cake'
}
const { name, food } = obj;
console.log(name, food); // 'Joe' 'cake'
Язык JavaScript позволяет даже сохранять свойства в переменных с другими именами.
const obj = {
name: 'Joe',
food: 'cake'
}
const { name: myName, food: myFood } = obj;
console.log(myName, myFood); // 'Joe' 'cake'
С помощью деструктурирующего присваивания можно "чисто" передавать параметры в функцию, избегая непредвиденных мутаций. Этот прием используется в 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!"
4. Спред-синтаксис
Эта одна из тех JavaScript-концепций, которые часто сбивают с толку, но на самом деле довольно просты.
Например, метод max объекта Math принимает для сравнения несколько аргументов и не умеет работать с массивами. Но спред-оператор легко поправит это дело, ведь он может разделить массив на отдельные элементы:
const arr = [4, 6, -1, 3, 10, 4]; const max = Math.max(...arr); console.log(max); // 10
А вообще, переходите на Set.
5. Rest-синтаксис
Этот оператор – брат-близнец предыдущего, но с совершенно другим характером. Rest-синтаксис (оставшиеся параметры) собирает несколько значений в один массив:
function myFunc(...args) {
console.log(args[0] + args[1]);
}
myFunc(1, 2, 3, 4); // 3
6. Методы массивов
Встроенные методы массивов в JavaScript предоставляют удобный и элегантный способ обработки и трансформации данных. И это круто, правда? В языке для них есть огромное множество возможных применений.
Этих супер-методов очень много. Полный список вы найдете на MDN.
map, filter, reduce
Это одни из самых популярных методов работы с массивами, обеспечивающих изменение данных или агрегацию нескольких элементов в одно значение.
map трансформирует все элементы массива с помощью переданной функции. Обработанные элементы собираются в новый массив, который и возвращается из метода.
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 по одному перебирает элементы и возвращает первый из них, для которого функция возвращает true. После этого поиск останавливается.
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const found = arr.find(el => el > 5); console.log(found); // 6
После элемента 6 поиск останавливается, хотя за ним есть еще несколько подходящих элементов. Это напоминает оператор break, прерывающий цикл. Очень удобный метод, если не нужно перебирать массив целиком.
findIndex – это очень похожий метод, только возвращает он не сам элемент, а его индекс. Для примера возьмем массив строковых значений, чтобы было яснее.
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 length = arr.push(5); console.log(arr); // [1, 2, 3, 4, 5] console.log(length); // 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
Краткое резюме:
- эти четыре метода изменяют исходный массив на месте;
popиshiftвозвращают удаленные элементы;pushиunshiftвозвращают новую длину массива;- методы
shiftиunshift, работающие с началом массива, более трудозатратны, так как им приходится сдвигать индексы всех элементов.
splice, slice
Еще пара методов с похожими названиями, в которых легко запутаться. А тем не менее между ними есть существенная разница.
splice – метод-швейцарский нож. Он умеет удалять и добавлять элементы массива, а также заменять уже существующие элементы на другие. Самое главное, он изменяет массив прямо на месте.
let arr = ['a', 'c', 'd', 'e']; let result = arr.splice(1, 2, 'b1', 'b2', 'b3')
Как работает этот метод?
- Первый аргумент устанавливает позицию, с которой начинается работа.
- Второй – число удаляемых элементов.
- Третий и дальше – элементы, которые нужно добавить.
Таким образом, мы начинаем с позиции 1, удаляем 'c' и 'd' и вставляем сразу три элемента. Метод вернет в переменную result массив из удаленных элементов, а сам исходный массив изменится:
console.log(result); // ['c', 'd'] console.log(arr); // ['a', 'b1', 'b2', 'b3', 'e']
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
Несложно догадаться, что делает этот метод – да, он сортирует исходный массив. По умолчанию все элементы преобразуются в строке и сравниваются посимвольно в соответствии с таблицами Unicode.
Обратите внимание, все элементы преобразуются в строки! Включая обычные числа. Это вызывает неожиданные для многих эффекты и порождает "нелогичные" JavaScript примеры, вроде этого:
let arr = [1, 2, 10, 21] arr.sort(); // [1, 10, 2, 21]
Впрочем, вы вполне можете управлять порядком сортировки, передав в метод sort собственную сортирующую функцию. Она будет попарно получать элементы и определять, какой из них должен идти первым. Порядок сравнения метод sort определит сам.
let arr = [1, 7, 3, -1, 5, 7, 2]; const sorter = (firstEl, secondEl) => firstEl - secondEl; arr.sort(sorter);
Сортирующая функция должна вернуть число:
- если оно будет положительным, первый и второй элемент поменяются местами;
- в ином случае – порядок не изменится.
Обратите внимание: устойчивость сортировки не гарантируется. Если коллбэк вернет 0, элементы не должны меняться местами, но полагаться на это поведение не следует.
Вот пример более сложной сортировки для массива объектов:
let john = { name: "John", age: 23 };
let jane = { name: "Jane", age: 18 };
let jerry = { name: "Jerry", age: 34 };
let people = [john, jane, jerry];
let sorter = (a, b) => a.age - b.age;
people.sort(sorter);
Методы массивов – прямая демонстрация возможностей функционального программирования. Почитайте об этой концепции подробнее:
- Функциональное программирование и его применение в JavaScript
- Основы функционального программирования в JavaScript
7. Генераторы
Прошло уже немало времени с момента появления в JavaScript функций-генераторов, однако многие разработчики все еще с опаской поглядывают на эту непонятную звездочку (астериск) *.
Генератор – это просто функция, которую можно остановить в любой момент, чтобы получить промежуточное значение. Для этого есть метод next().
Генератор может быть конечным. В определенный момент промежуточные точки кончатся, и работа функции будет остановлена, а вызов next вернет undefined.
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...
Обратите внимание: первый вызов функции только создает объект-генератор с методом next(), но не возвращает никакого значения из самой функции.
Генераторы тесно связаны с другими нововведениями стандарта ES6 – символами и итераторами. Держите подробное руководство для всех этих JavaScript-концепций сразу:
8. Обычное равенство (==) против строгого (===)
Вы точно понимаете, в чем разница между двумя этими операторами? Если нет, ждите ошибок.
На самом деле, все очень просто.
- Оператор
===при сравнении учитывает тип операндов. Строка у него никогда не будет равна числу, даже если выглядят они очень похоже. - Оператор
==по возможности пытается привести операнды к одному типу. Он допускает, что число1для вас означает ровно то же самое, что и строка"1".
console.log(0 == '0'); // true console.log(0 === '0'); // false
Это одна из мощных и удобных JavaScript-концепций, если, конечно, вы понимаете, что делаете. Берите на вооружение. Оператор == освобождает от ручного преобразования типов в тех ситуациях, когда тип не имеет значения. Но убедитесь, что это действительно так.
9. Сравнение объектов
Начинающие JavaScript-программисты очень часто пытаются сравнивать объекты напрямую. Они ожидают, что объекты с идентичными свойствами и одинаковыми их значениями будут равны – и ловят уйму ошибок.
Возвращаемся к самой первой из перечисленных в этой статье JavaScrpipt-концепций. В переменной не содержится физическое воплощение конкретного объекта, а только ссылка на место в памяти, где он хранится. Значит, сравнивая две переменные с объектами, вы сравниваете ссылки! А разные объекты, даже если они абсолютно одинаковы, всегда хранятся в разных местах памяти.
const joe1 = { name: 'Joe' };
const joe2 = { name: 'Joe' };
const joe3 = joe1;
console.log(joe1 === joe2); // false
console.log(joe1 === joe3); // true
Для "глубокого" сравнения, то есть перебора свойств и сравнения их значений, можно использовать JSON-сериализацию. Объекты преобразуются в строки, которые можно сравнивать напрямую. Но при этом не гарантируется сохранение порядка свойств, а значит, результат может быть ошибочным.
JavaScript фреймворки и библиотеки обычно уже имеют специальные методы для "глубокого" сравнения, например, в lodash есть метод isEqual.
10. Функции обратного вызова
Если слово "коллбэк" вызывает у вас приступ паники, значит, вы просто не умеете его готовить. Функции обратного вызова очень просты, в 6 пункте мы вдоволь с ними наработались.
Коллбэк – это функция, которая передается в другую функцию в качестве аргумента. Эта вторая функция будет делать свои дела, а потом вызовет коллбэк, сигнализируя, что ее работа закончена. Это весьма упрощенное описание, но тем не менее коллбэки до ES6 были одной из важнейших JavaScript-концепций асинхронного программирования.
function myFunc(text, callback) {
setTimeout(function() {
callback(text);
}, 2000);
}
myFunc('Hello world!', console.log); // 'Hello world!'
Здесь мы используем функцию обратного вызова, чтобы узнать, когда закончится таймаут, и что-то после этого сделать.
11. Промисы
Да-да, коллбэки не всегда удобны, они громоздки и с трудом выстраиваются в цепочку. Поэтому мы и пришли к промисам – удобному воплощению асинхронности.
Впрочем, callback-концепция и тут никуда не уходит, однако становится намного удобнее.
const myPromise = new Promise(function(res, rej) {
setTimeout(function(){
if (Math.random() < 0.9) {
return res('Hooray!');
}
return rej('Oh no!');
}, 1000);
});
Внутри промиса может быть любая асинхронная логика, setTimeout использован просто для примера.
Сам по себе промис (обещание) – это просто объект, имеющий определенное состояние:
- ожидание – функция, переданная в конструктор, только начала выполняться;
- выполнено – операция закончилась успешно;
- отклонено – возникла какая-либо ошибка.
Если все прошло хорошо, и вызвана функция res, выполняется обработчик, переданный в метод промиса then. А если выпала ошибка управление перейдет в обработчик из catch.
myPromise
.then(function(data) {
console.log('Success: ' + data);
})
.catch(function(err) {
console.log('Error: ' + err);
});
// Если Math.random() вернул значение меньше 0.9 считаем, что функция выполнена удачно
// "Success: Hooray!"
// Иначе имитируем ошибку
// "Error: On no!"
Асинхронное программирование – не самая простая из JavaScript-концепций. Если у вас есть желание изучить ее немного глубже, загляните сюда:
- Асинхронное программирование: концепция, реализация, примеры
- 9 полезных советов по Promise.resolve и Promise.reject
12. 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!'
В чем магия асинхронной функции myFunc? В том, что она самостоятельно дождется выполнения промиса greeter и только после этого продолжит выполнение. Оператор await остановит поток выполнения этой функции до получения результата.
Более подробный разбор этих интереснейших JavaScript-концепций вы найдете здесь:
12 JavaScript-концепций
На этих 12 концепциях стоит современный JavaScript – удобный и мощный язык программирования. Понимание каждой из них критично для хорошего разработчика.
Оригинал: 12 Concepts That Will Level Up Your JavaScript Skills
А по-вашему, на каких китах стоит JS? Давайте обсудим JavaScript основы и концепции в комментариях.
Комментарии