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 основы и концепции в комментариях.
Комментарии