Лучший опыт

5 недооцененных возможностей JavaScript.

Прочитав эту статью, ответьте вопрос: сколько из приведенных здесь практик были вам известны? Вероятно, ни вы, ни многие из ваших знакомых не знали о них  —  проверьте это, поделившись данной информацией с коллегами. Итак, предлагаю ознакомиться с одними из самых недооцененных возможностей TypeScript. 1. FlatMap FlatMap в JavaScript  —  это отличная техника, с которой можно познакомиться здесь. FlatMap объединяет в себе функции map и метода массивов filter.
5 недооцененных возможностей JavaScript...

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

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

1. FlatMap

FlatMap в JavaScript  —  это отличная техника, с которой можно познакомиться здесь. FlatMap объединяет в себе функции map и метода массивов filter. Рекомендую использовать flatMap() вместо комбинации filter() и map().

В отличие от комбинации filter() и map(), flatMap делает один проход и не создает промежуточного массива.

// использование filterAndMap
console.time("filterAndMap")
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const squaredOddNumbers = numbers
.filter(num => num % 2 !== 0)
.map(num => num * num)

console.log(squaredOddNumbers); // [1, 9, 25, 49, 81]
console.timeEnd("filterAndMap")
Использование filter и Map
console.time("filterAndMap")
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const squaredOddNumbers = numbers.flatMap(num =>
num % 2 !== 0 ? [num * num] : []
);


console.log(squaredOddNumbers); // [1, 9, 25, 49, 81]
console.timeEnd("filterAndMap")
Использование FlatMap

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

Методы массивов  —  одни из самых важных методов, помогающих взаимодействовать с массивами. В JavaScript существует множество методов массивов. Наиболее популярные из них  —  .filter().find().map().reduce(). Их можно объединять в шаблоны:

// метод массива, который сортирует массив 
// только по нечетным числам и возводит их в 3-ю степень
numbers
.sort((a, b) => a - b)
.filter((n) => n % 2 !== 0)
.map((n) => n ** 3);

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

Оптимальный код для приведенного выше случая выглядит так:

const numbers = [9, 3, 6, 4, 8, 1, 2, 5, 7];

// метод массива, который сортирует массив
// только по нечетным числам и возводит их в 3-ю степень
numbers
.filter((n) => n % 2 !== 0)
.sort((a, b) => a - b)
.map((n) => n ** 3);

3. Метод reduce

Я наблюдал эту проблему у многих фронтенд-разработчиков. Когда пакет типа react-charts запрашивает данные в объектоподобной структуре, а реализация react-charts запрашивает данные в формате, сгруппированном по ключу, большинство разработчиков применяют метод .forEach() или некорректно используют метод map(). Например, так:

fetch("https://jsonplaceholder.typicode.com/todos/")
.then(res=>res.json())
.then(todos=>{

// использование Map
const todosForUserMap = {};
todos.forEach(todo=>{
if (todosForUserMap[todo.userId]){
todosForUserMap[todo.userId].push(todo);
}else{
todosForUserMap[todo.userId] = [todo];
}
})

console.log(todosForUserMap)
})

Этот метод хорош тем, что в нем используется метод forEach, а не map. Здесь не стоит использовать метод map, так как для каждого элемента будет создан массив. Скажем, если массив состоит из 1000 записей, то в map будет создан массив нулевой длины из 1000 записей, а в forEach() такого создания массива не произойдет.

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

fetch("https://jsonplaceholder.typicode.com/todos/")
.then(res=>res.json())
.then(todos=>{

// использование Map
const todosForUserMap = todos.reduce((accumulator, todo)=>{
if (accumulator[todo.userId]) accumulator[todo.userId].push(todo);
if (!accumulator[todo.userId]) accumulator[todo.userId] = [todo];
return accumulator;
},{})

console.log(todosForUserMap)
})

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

4. Генераторы

Генераторы и итераторы, пожалуй, относятся к элементам кода, не используемым JavaScript-разработчиками, знания которых ограничиваются вопросами для собеседования по программированию. В сценариях извлечения данных можно столкнуться с огромным объемом данных в БД/API, которые придется передавать во фронтенд. В этом случае наиболее часто используемым решением в react является бесконечная загрузка.

Как бы вы реализовали функцию бесконечной загрузки в nodejs-сервере или ванильном javascript?

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

async function *fetchProducts(){
while (true){
const productUrl = "https://fakestoreapi.com/products?limit=2";
const res = await fetch(productUrl)
const data = await res.json()
yield data;
// манипулируйте UI как здесь
// или сохраните его в БД или в другом месте;
// используйте его в качестве места для побочных эффектов;
// даже если какое-то условие совпадает, прерывайте поток
}
}

async function main() {
const itr = fetchProducts();
// должно вызываться в зависимости от взаимодействия с пользователем,
// или же используйте другие приемы, чтобы избежать бесконечной загрузки.
console.log( await itr.next() );
}
return main()

5. Нативные классы JavaScript

В комплект поставки JavaScript входят нативные классы, с помощью которых можно довольно легко создавать/инстанцировать такие элементы, как URL, заголовки и т. д. Вам наверняка доводилось видеть, как кто-то пытается запросить параметры в URL следующим образом:

async function getUrl(userId, limit, category){
return `https://fakestoreapi.com/products${category ? `/category/${category}` : ""}${limit ? Number(limit):""}${userId? Number(userId):""}`;
}

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

function constructURL(category, limit, userId) {
const baseURL = "https://fakestoreapi.com/products";
const url = new URL(baseURL);
const params = new URLSearchParams();

if (category) url.pathname += `/category/${category}`;
if (limit) params.append('limit', Number(limit).toString());
if (userId) params.append('userId', Number(userId).toString());

url.search = params.toString();
return url.toString();
}

Таким образом, можно обрабатывать сложные условия создания URL в рамках одного файла. Здесь объект URL следует BuilderPattern  —  одному из многих паттернов проектирования, который можно реализовать в коде, чтобы спрятать сложную логику в отдельном месте и улучшить читабельность.