8 плохих практик JavaScript, которые 100% приведут к сбоям
Разобрали восемь практик JavaScript, в рамках которых нужно быть максимально внимательным, ведь они могут вызвать сбой в вашем приложении.
Позаботьтесь о пользователях, и они позаботятся о вас
К некоторым реализациям кода нужно относиться осторожно. Рассмотрим 8 плохих практик JavaScript, которые могут привести к серьёзным сбоям.
1. Сбой из-за null по умолчанию
Любой краш в работе приложения ведёт к финансовым убыткам и требует незамедлительной отладки. Однажды у меня ушло много времени на отладку следующего кода:
const SomeComponent = ({ items = [], todaysDate, tomorrowsDate }) => { const [someState, setSomeState] = useState(null) return ( <div> <h2>Today is {todaysDate}</h2> <small>And tomorrow is {tomorrowsDate}</small> <hr /> {items.map((item, index) => ( <span key={`item_${index}`}>{item.email}</span> ))} </div> ) } const App = ({ dates, ...otherProps }) => { let items if (dates) { items = dates ? dates.map((d) => new Date(d).toLocaleDateString()) : null } return ( <div> <SomeComponent {...otherProps} items={items} /> </div> ) }
Если внутри компонента приложения dates
имеет значение falsey
, то при инициализации компонент будет иметь значение null.
Если items
имеет значение falsey
, то нужно инициализировать items
в пустой массив по умолчанию. Но при dates
со значением falsey
происходит сбой именно из-за items
со значением null.
Что делать?
Параметры функции по умолчанию позволяют именованным параметрам инициализироваться с дефолтными значениями, если значение не передано или не определено.
В нашем случае null является значением, несмотря на falsey
.
Таким образом, дважды подумайте, прежде чем использовать null в качестве значения по умолчанию. Вместо этого используйте пустой массив.
2. Заключение свойств в квадратные скобки
Наверное, самая не очевидная из всех плохих практик JavaScript.
Даже выбор скобок может вызвать сбой. Вот пример поиска объекта с квадратными скобками:
const someFunction = function() { const store = { people: { joe: { age: 16, gender: 'boy', }, bob: { age: 14, gender: 'transgender', } } } return { getPersonsProfile(name) { return store.people[name] }, foods: ['apple', 'pineapple'], } } const obj = someFunction() const joesProfile = obj.getPersonsProfile('joe') console.log(joesProfile) /* result: { age: 16, gender: boy, } */
В этом примере всё сделано верно. Минус в том, что такой код выполняется медленнее, чем поиск ключа объекта.
Настоящие проблемы начинаются, когда допущена какая-нибудь небольшая ошибка, например, опечатка:
const someFunction = function () { const store = { people: { joe: { age: 16, gender: 'boy', }, bob: { age: 14, gender: 'transgender', } } } return { getPersonsProfile(name) { return store.people[name] }, foods: ['apple', 'pineapple'], } } const obj = someFunction() const joesProfile = obj.getPersonsProfile('Joe') const joesAge = joesProfile.age console.log(joesAge)
Если при редактировании кода вы или ваш коллега допустили случайную ошибку — написали Joe с большой буквы вместо маленькой — JavaScript тут же возвращает undefined:
"TypeError: Cannot read property 'age' of undefined at tibeweragi.js:24:29 at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:13924 at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:10866"
Приложение вылетит в тот момент, когда начнётся поиск свойства.
Так, joesProfile (который затем окажется undefined
) находится в коде, и нет никакой возможности узнать об этом. Всё выяснится только при сбое, который произойдёт во время поиска какого-либо свойства. Например, joesProfile.age.
Некоторые разработчики инициализируют верное возвращаемое значение по умолчанию при неудачном поиске:
const store = { people: { joe: { age: 16, gender: 'boy', }, bob: { age: 14, gender: 'transgender', } } } return { getPersonsProfile(name) { return store.people[name] || {} }, foods: ['apple', 'pineapple'], } }
По крайней мере, так не произойдёт краша. Если используете поиск в квадратных скобках, продумайте, что должно произойти при неудачном поиске.
Приведу реалистичный пример. Я взял этот код из репозитория, выложенного восемь месяцев назад. В целях конфиденциальности я переименовал почти все переменные, но дизайн, синтаксис и архитектура кода остались неизменными:
import { createSelector } from 'reselect' // supports passing in the whole obj or just the string to correct the video type const fixVideoTypeNaming = (videoType) => { let video = videoType // If video is a video object if (video && typeof video === 'object') { const media = { ...video } video = media.videoType } // If video is the actual videoType string if (typeof video === 'string') { // fix the typo because brian is an idiot if (video === 'mp3') { video = 'mp4' } } return video } /* ------------------------------------------------------- ---- Pre-selectors -------------------------------------------------------- */ export const getOverallSelector = (state) => state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total .overall export const getSpecificWeekSelector = (state, props) => state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.weekly[ props.date ] /* ------------------------------------------------------- ---- Selectors -------------------------------------------------------- */ export const getWeeklyCycleSelector = createSelector( getSpecificWeekSelector, (weekCycle) => weekCycle || null, ) export const getFetchingTotalStatusSelector = createSelector( (state) => state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total .fetching, (fetching) => fetching, ) export const getFetchErrorSelector = createSelector( (state) => state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total .fetchError, (fetchError) => fetchError, )
fixVideoTypeNaming — это функция, которая определяет тип видео на основе значений, переданных в качестве аргументов. Если аргумент — видеофайл, то функция определит тип видео с помощью свойства .videoType
. Если аргумент — строка, то вызывающая функция переходит к videoType
, мы пропускаем первый шаг. Выяснилось, что videoType .mp4property
несколько раз в приложении было написано неправильно. Чтобы быстро исправить эту проблему с опечаткой, в качестве временного решения использовалось fixVideoTypeNaming.
Как вы уже догадались, приложение написано с помощью Redux — отсюда такой синтаксис.
Чтобы использовать эти селекторы, нужно импортировать их для использования в компонентах высшего порядка, затем прикрепить компонент к слушателю этого среза состояния.
const withTotalCount = (WrappedComponent) => { class WithTotalCountContainer extends React.Component { componentDidMount = () => { const { total, dispatch } = this.props if (total == null) { dispatch(fetchTotalVideoTypeCount()) } } render() { return <WrappedComponent {...this.props} /> } } WithTotalCountContainer.propTypes = { fetching: PropTypes.bool.isRequired, total: PropTypes.number, fetchError: PropTypes.object, dispatch: PropTypes.func.isRequired, } WithTotalCountContainer.displayName = `withTotalCount(${getDisplayName( WrappedComponent, )})` return connect((state) => { const videoType = fixVideoTypeNaming(state.app.media.video.videoType) const { fetching, total, fetchError } = state.app.media.video[ videoType ].options.total return { fetching, total, fetchError } })(WithTotalCountContainer) }
Компонент пользовательского интерфейса:
const TotalVideoCount = ({ classes, total, fetching, fetchError }) => { if (fetching) return <LoadingSpinner /> const hasResults = !!total const noResults = fetched && !total const errorOccurred = !!fetchError return ( <Typography variant="h3" className={classes.root} error={!!fetched && !!fetchError} primary={hasResults} soft={noResults || errorOccurred} center > {noResults && 'No Results'} {hasResults && `$${formatTotal(total)}`} {errorOccurred && 'An error occurred.'} </Typography> ) }
HOC передаёт все пропсы этому компоненту, он получает их и отображает информацию, адаптируя данных от пропсов. В теории всё должно отлично работать, но так происходит не всегда.
Если мы вернёмся к контейнерам и посмотрим, как селекторы выбирают значения, мы поймём, в чём здесь подвох:
export const getOverallSelector = (state) => state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total .overall export const getSpecificWeekSelector = (state, props) => state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.weekly[ props.date ]
При разработке приложений часто проводятся промежуточные тесты. Они помогают устранить ошибки и убедиться, что приложение работает как задумано. Эти сниппеты кода не были протестированы — впоследствии это вызовет вылет приложения.
Например, state.app.media.video.videoType
представляет собой цепочку из четырёх уровней. Допустим, что другой разработчик вносил правки в код и допустил ошибку — state.app.media.video
стал со значением undefined. В таком случае приложение зависнет, потому что оно не воспринимает videoType
со значением undefined.
Если вы допустите ещё одну опечатку в videoType
, если не устраните её с помощью fixVideoTypeNaming
, если возникнут проблемы с mp3 — во всех этих случаях произойдёт ещё один сбой. Проблема в том, что обнаружить все эти проблемы вы сможете только тогда, когда пользователь с ними столкнётся. К тому времени, может быть уже слишком поздно.
Будьте внимательнее. Не надейтесь, что в вашем приложении никогда таких багов не будет.
3. Краш приложения при проверке пустых объектов при рендеринге
Я тоже так делал, когда при условном рендеринге нужно было проверять, содержат ли данные какие-либо объекты. Для этого я использовал Object.keys
. Если в данных были объекты, то компонент продолжал рендеринг, если поставленное условие соблюдалось:
const SomeComponent = ({ children, items = {}, isVisible }) => ( <div> {Object.keys(items).length ? ( <DataTable items={items} /> ) : ( <h2>Data has not been received</h2> )} </div> )
Давайте представим, что мы вызвали какой-то API и получили в ответ items
в качестве объекта. На первый взгляд кажется, что всё нормально. Ожидаемый тип items
— объект, поэтому для работы с ним отлично подходит Object.keys
. Затем мы инициализируем items
в виде пустого объекта — на случай бага, который может вернуть items со значением falsey
.
Но что, если items
в будущем станет массивом? Object.keys(items)
не вызовет краш приложения, но вернёт что-то странное, например, ["0", "1", "2"]
. Как думаете, как поведут себя компоненты при рендеринге с такими данными?
Проблема сниппета в том, что если в пропсах у items
будет значение null, то items
не инициализируется с введённым вами дефолтным значением.
Таким образом, приложение вылетит, так что будьте осторожнее:
"TypeError: Cannot convert undefined or null to object at Function.keys (<anonymous>) at yazeyafabu.js:4:45 at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:13924 at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:10866"
4. Вылет при проверке наличия массивов перед рендерингом
Этот пункт похож на предыдущий. Если вы привыкли делать так, как показано ниже, то обязательно проводите модульные тесты или применяйте arr
перед рендерингом. В противном случае, если arr
является объектным литералом, приложение вылетит.
render() { const { arr } = this.props return ( <div> {arr && arr.map()...} </div> ) }
Оператор &&
; воспримет объектный литерал как truthy
и применит функцию .map
, что приведёт к сбою.
Будьте внимательны — внимательность сохранит вам время и силы, они понадобятся для решения более сложных и важных проблем.
5. Работа без линтера
Линтер — это чрезвычайно полезный инструмент. Я пользуюсь ESLint, популярным линтером для JavaScript: он помогает найти ошибки в коде, даже не выполняя его. Он помогает исправить ошибки в реальном времени, как бы выполняя роль наставника. Он объясняет, что с кодом не так и что нужно сделать, чтобы это исправить.
Приведу пример:
Ещё одна функция ESLint — если вы не согласны с какими-либо правилом проверки, можете просто отключить его. Тогда ESLint не будет помечать это как ошибку. Удобно, правда?
6. Деструктуризация при рендеринге списков
Такое иногда происходит, но этот баг не всегда легко обнаружить. Допустим, у вас есть список элементов, и вы хотите провести рендеринг группы компонентов для каждого элемента из этого списка. Если значение одного из элементов списка не соответствует ожидаемому, приложение вылетает — оно не знает, что делать с таким типом значения.
Приведу пример:
const api = { async getTotalFrogs() { return { data: { result: [ { name: 'bob the frog', tongueWidth: 50, weight: 8 }, { name: 'joe the other frog', tongueWidth: 40, weight: 5 }, { name: 'kelly the last frog', tongueWidth: 20, weight: 2 }, ], }, } }, } const getData = async ({ withTongues = false }) => { try { const response = await api.getTotalFrogs({ withTongues }) return response.data.result } catch (err) { throw err } } const DataList = (props) => { const [items, setItems] = useState([]) const [error, setError] = useState(null) React.useEffect(() => { getData({ withTongues: true }) .then(setItems) .catch(setError) }, []) return ( <div> {Array.isArray(items) && ( <Header size="tiny" inverted> {items.map(({ name, tongueWidth, weight }) => ( <div style={{ margin: '25px 0' }}> <div>Name: {name}</div> <div>Width of their tongue: {tongueWidth}cm</div> <div>Weight: {weight}lbs</div> </div> ))} </Header> )} {error && <Header>You received an error. Do you need a linter?</Header>} </div> ) }
Всё идёт хорошо, код работает. Перейдём к вызову API. И вместо этого…
const api = { async getTotalFrogs() { return { data: { result: [ { name: 'bob the frog', tongueWidth: 50, weight: 8 }, { name: 'joe the other frog', tongueWidth: 40, weight: 5 }, { name: 'kelly the last frog', tongueWidth: 20, weight: 2 }, ], }, } }, }
…мы получаем это. Что делать, если в клиенте API возникла проблема с обработкой потока данных, из-за которой код вернул этот массив?
const api = { async getTotalFrogs() { return { data: { result: [ { name: 'bob the frog', tongueWidth: 50, weight: 8 }, undefined, { name: 'kelly the last frog', tongueWidth: 20, weight: 2 }, ], }, } }, }
Приложение не знает, что делать с таким типом значения:
Uncaught TypeError: Cannot read property 'name' of undefined at eval (DataList.js? [sm]:65) at Array.map (<anonymous>) at DataList (DataList.js? [sm]:64) at renderWithHooks (react-dom.development.js:12938) at updateFunctionComponent (react-dom.development.js:14627)
Чтобы предотвратить сбой, установите объект по умолчанию на каждой итерации:
{ items.map(({ name, tongueWidth, weight } = {}) => ( <div style={{ margin: '25px 0' }}> <div>Name: {name}</div> <div>Width of their tongue: {tongueWidth}cm</div> <div>Weight: {weight}lbs</div> </div> )) }
Теперь пользователи не будут осуждать вас — всё работает:
Мы предотвратили сбой, но я бы посоветовал разобраться с пропущенными значениями. Алгоритм в этом случае похож на решение проблемы с возвращением null для целых элементов, так как в обоих случаях нет никаких данных.
7. Недостаточно подробное исследование перед реализацией
Другой случай из плохих практик JavaScript. Раньше я тоже допускал подобную ошибку — был слишком уверен в своих знаниях, когда реализовывал поисковый запрос.
На самом деле, реализовать компонент поискового запроса было достаточно просто. Проблема заключалась в символах в запросах.
Я был уверен, что при отправке ключевых слов в виде запросов в поисковый API, допустима каждая нажатая пользователем клавиша — они же зачем-то существуют на клавиатуре. Это не всегда так.
Убедитесь, что регулярное выражение работает именно так, как вы запланировали — тогда не произойдёт сбоя из-за недопустимых символов:
const hasInvalidChars = /^.*?(?=[\+\^#%&$\*:<>\?/\{\|\}\[\]\\\)\(]).*$/g.test( inputValue, )
Это пример самого распространённого и современного регулярного выражения для поискового API.
Раньше было так:
const hasInvalidChars = /^.*?(?=[\+\^#%&$\*:<>\?/\{\|\}\[\]\)\(]).*$/g.test( inputValue, ) const callApi = async (keywords) => { try { const url = `https://someapi.com/v1/search/?keywords=${keywords}/` return api.searchStuff(url) } catch (error) { throw error } }
Как вы видите, здесь нет слэша /
— это может привести к сбоям приложения. Угадайте, что случится, если отправить этот символ поисковому API.
Если честно, я бы не стал на сто процентов доверять примерам из интернета. Во-первых, большинство из них не были тщательно протестированы. Во-вторых, когда речь идёт о регулярных выражениях, нет никакого универсального решения, которое подойдёт во всех случаях.
8. Неограниченный размер загружаемых файлов
Ограничения размера загружаемых пользователями файлов — это хорошее решение. В большинстве случаев огромные файлы можно сжать без заметных потерь качества.
Мы с коллегами заметили, что иногда при загрузке изображение всё виснет. Не у всех наших пользователей мощный компьютер — нужно помнить об этом.
Покажу на примере, как ограничить размер файла 5 мегабайтами (5,000,000 байт):
import React, { useState, useEffect } from 'react' const useUploadStuff = () => { const [files, setFiles] = useState([]) // Limit the file sizes here const onChange = (e) => { const arrFiles = Array.from(e.target.files) const filesUnder5mb = arrFiles.filter((file) => { const bytesLimit = 5000000 if (file.size > bytesLimit) { // optionally process some UX about this file size } return file.size < bytesLimit }) setFiles(filesUnder5mb) } useEffect(() => { if (files.length) { // do something with files } }, [files]) return { files, onChange, } } const UploadStuff = () => { const { onChange } = useUploadStuff() return ( <div> <h2 style={{ color: '#fff' }}>Hi</h2> <div> <input style={{ color: '#fff' }} onChange={onChange} type="file" placeholder="Upload Stuff" multiple /> </div> </div> ) } export default UploadStuff
Оставляйте ровно столько места, сколько нужно. Мы же не хотим, чтобы вместо небольших документов пользователи загружали огромные видеоигры.