JavaScript

Пролетел еще один год, а JavaScript все продолжает меняться. Однако есть несколько советов по написанию чистого и эффективного кода, который будет масштабироваться даже (а может и особенно?) в 2019 году. Ниже приведен список из 9 практических советов, которые помогут вам достичь еще больших успехов в разработке.

1. async / await

Если вы все еще используете обратный вызов, то советую вам оставить его в 2014. Используйте обратные вызовы только в том случае, где требуется их наличие, как, например, в библиотеке или в целях повышения производительности. Промисы довольно неплохи, однако при увеличении кодовой базы их использование доставляет некоторые неудобства. Лично я решил использовать async / await, благодаря которой чтение и редактирование кода становится намного проще. Фактически, await можно использовать для каждого Промиса в JavaScript, если используемая вами библиотека возвращает Промис, проще говоря — await(дожидается) его. По сути, async/await представляет собой некий “синтаксический сахар”, окружающий Промисы. Для работы кода необходимо лишь добавить ключевое слово async перед функцией. Краткий пример:

async function getData() {
    const result = await axios.get('https://dube.io/service/ping')
    const data = result.data
    
    console.log('data', data)
    
    return data
}

getData()

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

Конструкция async / await была введена в ES2017, поэтому не забудьте перевести старый код на новую версию.

2. Асинхронный поток управления

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

for…of

Представим, что на нашей странице находится пара Покемонов, и нам необходимо получить о них подробную информацию. Ни у кого не возникает желания ждать завершения всех вызовов, особенно когда неизвестно их количество, однако хотелось бы обновить имеющиеся наборы данных при получении чего-либо взамен. Чтобы выполнить цикл через массив и запустить код async внутри блока, можно использовать for...of. В таком случае, выполнение будет приостановлено до тех пор, пока каждый вызов не будет успешно выполнен. Стоит отметить, что при выполнении подобных действий в коде могут возникать узкие места в производительности, однако этот инструмент стоит того, чтобы сохранить его в вашем наборе инструментов. Один из примеров:

import axios from 'axios'

let myData = [{id: 0}, {id: 1}, {id: 2}, {id: 3}]

async function fetchData(dataSet) {
    for(entry of dataSet) {
        const result = await axios.get(`https://ironhack-pokeapi.herokuapp.com/pokemon/${entry.id}`)
        const newData = result.data
        updateData(newData)
        
        console.log(myData)
    }
}

function updateData(newData) {
    myData = myData.map(el => {
        if(el.id === newData.id) return newData
        return el
    })
}
    
fetchData(myData)

Здесь представлены действительно рабочие примеры, так что не стесняйтесь копировать и вставлять их в любимую песочницу

Promise.all

Что нужно сделать, чтобы одновременно вызвать всех Покемонов? Поскольку await можно добавить в каждый Промис, просто используйте Promise.all:

import axios from 'axios' 

let myData = [{id: 0}, {id: 1}, {id: 2}, {id: 3}]

async function fetchData(dataSet) {
    const pokemonPromises = dataSet.map(entry => {
        return axios.get(`https://ironhack-pokeapi.herokuapp.com/pokemon/${entry.id}`)
    })

    const results = await Promise.all(pokemonPromises)
    
    results.forEach(result => {
        updateData(result.data)
    })
    
    console.log(myData) 
}

function updateData(newData) {
    myData = myData.map(el => {
        if(el.id === newData.id) return newData
        return el
    })
}
    
fetchData(myData)

Поскольку конструкции for...of и Promise.all были введены с ES6+, обязательно переведите старый код на новую версию.

3. Деструктурирование и значения по умолчанию

Вернемся к предыдущему примеру, где выполняются следующие действия:

const result = axios.get(`https://ironhack-pokeapi.herokuapp.com/pokemon/${entry.id}`)

const data = result.data

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

const { data } = await axios.get(...)

Мы обошлись одной строчкой кода! Ура! Можно также переименовать переменную:

const { data: newData } = await axios.get(...)

Еще один хороший прием — задать значения по умолчанию во время деструктурирования. Это способ гарантирует отсутствие undefined и необходимости проверки переменных вручную.

const { id = 5 } = {}

console.log(id) // 5

Эти приемы также можно использовать с параметрами функции, например:

function calculate({operands = [1, 2], type = 'addition'} = {}) {
    return operands.reduce((acc, val) => {
        switch(type) {
            case 'addition':
                return acc + val
            case 'subtraction':
                return acc - val
            case 'multiplication':
                return acc * val
            case 'division':
                return acc / val
        }
    }, ['addition', 'subtraction'].includes(type) ? 0 : 1)
}

console.log(calculate()) // 3
console.log(calculate({type: 'division'})) // 0.5
console.log(calculate({operands: [2, 3, 4], type: 'multiplication'})) // 24

Сначала пример может показаться немного запутанным, но не торопитесь делать выводы и поэкспериментируйте с ним. Если в качестве аргументов функции не передаются никакие значения, используются значения по умолчанию. Как только начинается передача значений, значения по умолчанию используются только для несуществующих аргументов.

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


4. Истинные и ложные значения

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

if(myBool === true) {
  console.log(...)
}

// OR
if(myString.length > 0) {
  console.log(...)
}

// OR
if(isNaN(myNumber)) {
  console.log(...)
}

Все это можно сократить подобным образом:

if(myBool) {
  console.log(...)
}

// OR
if(myString) {
  console.log(...)
}

// OR
if(!myNumber) {
  console.log(...)
}

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

Ложные значения

  • строки с нулевой длиной
  • индекс 0
  • false
  • undefined
  • null
  • NaN

Истинные значения

  • пустые массивы
  • пустые объекты
  • все остальное

Обратите внимание, что при проверке истинных или ложных значений не существует явного сравнения, которое приравнивается к проверке с двойными знаками равенства ==, а не с тремя ===. В целом, она происходит подобным образом, однако есть определенные ситуации, в которых происходит ошибка. В моем случае, это чаще всего происходит с индексом 0.

5. Логические и условные операторы

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

Логические операторы

Логические операторы представляют собой комбинацию двух выражений и возвращают как значение true, так и false, либо соответствующее значение, и представлены знаками &&, что означает “и”, а также ||, что означает “или”. Рассмотрим на примере:

console.log(true && true) // true
console.log(false && true) // false
console.log(true && false) // false
console.log(false && false) // false
console.log(true || true) // true
console.log(true || false) // true
console.log(false || true) // true
console.log(false || false) // false

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

  • &&: Возвращается первое ложное значение, а в случае, если его не существует, возвращается последнее истинное значение.
  • ||: Возвращается первое истинное значение, а в случае, если его не существует, операция будет равна последнему ложному значению.
console.log(0 && {a: 1}) // 0
console.log(false && 'a') // false
console.log('2' && 5) // 5
console.log([] || false) // []
console.log(NaN || null) // null
console.log(true || 'a') // true

Условный оператор

Условный оператор очень похож на логический оператор, но он состоит из трех частей:

  1. Сравнение, которое будет либо ложным, либо истинным
  2. Первое возвращаемое значение, если сравнение является истинным
  3. Второе возвращаемое значение, если сравнение является ложным

Один из примеров:

const lang = 'German'
console.log(lang === 'German' ? 'Hallo' : 'Hello') // Hallo
console.log(lang ? 'Ja' : 'Yes') // Ja
console.log(lang === 'French' ? 'Bon soir' : 'Good evening') // Good evening

6. Опциональное связывание

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

let data
if(myObj && myObj.firstProp && myObj.firstProp.secondProp && myObj.firstProp.secondProp.actualData) data = myObj.firstProp.secondProp.actualData

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

const data = myObj?.firstProp?.secondProp?.actualData

Этот способ хорошо применим для проверки вложенных свойств, а также делает код чище.

В настоящее время дополнительное связывание не является частью официальной спецификации, оно находится на первой стадии разработки в качестве экспериментальной функции. Чтобы использовать его, нужно добавить @babel/plugin-proposal-optional-chaining в babelrc.


7. Свойства классов и функция bind

Функции bind в JavaScript представляют собой общую задачу. С введением стрелочных функций в спецификации ES6, появился способ автоматически привязывать функции к контексту объявления. Этот способ очень эффективен и широко используется среди разработчиков JavaScript. После первого введения классов, использовать стрелочные функции стало невозможно, поскольку методы класса необходимо объявлять определенным образом. Связывать функции нужно было в другом месте, например в конструкторе (самая оптимальная практика с React.js). Меня всегда досаждал процесс первого определения методов класса, а затем и их связывания, спустя некоторое время это кажется просто нелепым. Благодаря синтаксису свойств класса можно снова использовать стрелочные функции и насладиться преимуществами автоматической привязки. Теперь стрелочную функцию можно использовать внутри класса. Один из примером с привязкой _increaseCount:

class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state = { count: 0 }
    }
    
    render() {
        return(
            <div>
                <h1>{this.state.count}</h1>  
                <button onClick={this._increaseCount}>Increase Count</button>
            </div>
        )
    }
    
    _increaseCount = () => {
        this.setState({ count: this.state.count + 1 })
    }
}

В настоящее время свойства класса не являются частью официальной спецификации, они находятся на третьей стадии разработки в качестве экспериментальной функции. Чтобы использовать их, нужно добавить @babel/plugin-proposal-optional-chaining в файл babelrc.

8. Используйте Parsel

Каждый фронтенд-разработчик наверняка сталкивался с пакетированием и транспилированием кода. Фактическим стандартом для выполнения этих действий долгое время являлся webpack. Первое время я использовал webpack в версии 1, которая доставляла тогда множество проблем. Разбираясь с различными параметрами конфигурации, я потратил бесчисленное количество часов, пытаясь собрать и запустить пакет. Если бы это было возможно, я бы никогда больше к нему не притронулся, чтобы ничего не испортить. Пару месяцев назад я наткнулся на parcel, благодаря которому наконец облегченно вздохнул. Он выполняет почти все действия из коробки, при этом давая возможность вносить необходимые изменения. Он также поддерживает систему плагинов, аналогичную webpack или babel, и работает на невероятно быстрой скорости. Если вы еще не ознакомились с parcel, то советую вам сделать это прямо сейчас!

9. Пишите большую часть кода самостоятельно

Это хорошая тема. Я затрагивал ее уже множество раз. Как правило, даже для CSS, многие используют библиотеку компонентов, такую как bootstrap. Среди разработчиков JavaScript все еще встречаются те, кто использует jQuery и небольшие библиотеки для валидации, слайдеры и тому подобное. Несмотря на то, что использование библиотек имеет значение, я настоятельно рекомендую писать большую часть кода самостоятельно, а не бездумно устанавливать пакеты npm. При наличии больших библиотек (или даже фреймворков), над которыми трудится целая команда разработчиков, таких как moment.js или react-datepicker, попытка написать код самостоятельно не имеет смысла. Однако большую часть кода для собственного использования можно написать собственноручно. Это даст вам три основных преимущества:

  1. Вы точно знаете, что происходит в коде
  2. Рано или поздно вы начнете по-настоящему разбираться в программировании и работе всех внутренних процессов
  3. Вы предотвращаете раздувание кодовой базы

Для новичков проще использовать пакет npm. Чтобы реализовать некоторые функции самостоятельно, потребуется больше времени. Однако бывают случаи, когда пакет действительно не работает согласно ожиданиям, и приходится переключить его на другой, при этом тратя еще больше времени на изучение настроек API. Однако при самостоятельной реализации, можно полностью адаптировать его для вашего конкретного сценария использования.


Перевод статьи Lukas Gisder-Dubé9 Tricks for Kickass JavaScript Developers in 2019