[Перевод] Топ-10 ошибок из 1000+ JavaScript-проектов и рекомендации по их устранению
В компании Rollbar, которая занимается созданием инструментов для работы с ошибками в программах, решили проанализировать базу из более чем 1000 проектов на JavaScript и найти в них ошибки, которые встречаются чаще всего. В результате они сформировали список из 10 наиболее часто встречающихся ошибок, проанализировали причины их появления и рассказали о том, как их исправлять и избегать. Они полагают, что знакомство с этими ошибками поможет JS-разработчикам писать более качественный код.
Сегодня мы публикуем перевод их исследования.
Методика анализа
В наши дни данные — это всё, поэтому мы нашли, проанализировали и проранжировали ошибки, которые чаще всего встречаются в JavaScript-проектах. А именно, были собраны сведения об ошибках по каждому проекту, после чего было подсчитано количество ошибок каждого вида. Ошибки группировались по их контрольной сумме, методику вычисления которой можно найти здесь. При таком подходе, если, например, в одном проекте обнаружена некая ошибка, которая после этого найдена где-то ещё, такие ошибки группируют. Это позволяет, после анализа всех участвующих в исследовании проектов, получить краткую сводку по ошибкам, а не нечто вроде огромного лог-файла, с которым неудобно работать.
В ходе исследования особое внимание уделялось наиболее часто встречающимся ошибкам. Для того чтобы такие ошибки отобрать, их ранжировали по количеству проектов разных компаний, в которых они встречаются. Если бы в этот рейтинг входило лишь общее число появлений некоей ошибки, то ошибки, характерные для какого-нибудь очень крупного проекта, но редко встречающиеся в других проектах, исказили бы результаты.
Вот десять ошибок, которые были отобраны по результатам исследования. Они отсортированы по количеству проектов, в которых они встречаются.
Ошибки, которые встречаются в JS-проектах чаще всего
Названия ошибок представляют собой сокращённый вариант сообщения об ошибке, которое выдаёт система. Опора на системные сообщения позволяет легко идентифицировать ошибки при их возникновении. Сейчас мы проанализируем каждую из них, расскажем о том, что их вызывает, и о том, как с ними бороться.
1. Uncaught TypeError: Cannot read property
Если вы пишете программы на JavaScript, то вы, вероятно, встречались с этой ошибкой гораздо чаще, чем вам того хотелось бы. Подобная ошибка возникает, например, в Google Chrome при попытке прочитать свойство или вызвать метод неопределённой переменной, то есть той, которая имеет значение undefined
. Увидеть эту ошибку в действии можно с помощью консоли инструментов разработчика Chrome.
Ошибка Cannot read property
Эта ошибка может возникнуть по многим причинам, но чаще всего её вызывает неправильная инициализация состояния при рендеринге элемента пользовательского интерфейса. Взглянем на пример того, как подобное может произойти в реальном приложении. Тут мы используем React, но та же ошибка инициализации характерна для Angular, Vue и для любых других фреймворков.
class Quiz extends Component {
componentWillMount() {
axios.get('/thedata').then(res => {
this.setState({items: res.data});
});
}
render() {
return (
{this.state.items.map(item =>
- {item.name}
)}
);
}
}
Тут надо обратить внимание на две важные вещи:
- В самом начале состояние компонента (то есть —
this.state
) представлено значениемundefined
. - При асинхронной загрузке данных компонент будет выведен как минимум один раз до того, как данные будут загружены, вне зависимости от того, будет ли это выполнено в
componentWillMount
или вcomponentDidMount
. Когда элементQuiz
выводится в первый раз, вthis.state.items
записаноundefined
. Это, в свою очередь, означает, чтоitemList
получает элементы, которые так же представлены значениемundefined
. Как результат, мы видим в консоли следующую ошибку:"Uncaught TypeError: Cannot read property ‘map’ of undefined"
.
Эту ошибку исправить несложно. Проще всего инициализировать состояние в конструкторе подходящими значениями по умолчанию.
class Quiz extends Component {
// Добавляем это:
constructor(props) {
super(props);
// Инициализируем состояние и задаём значения элементов по умолчанию
this.state = {
items: []
};
}
componentWillMount() {
axios.get('/thedata').then(res => {
this.setState({items: res.data});
});
}
render() {
return (
{this.state.items.map(item =>
- {item.name}
)}
);
}
}
Код вашего приложения будет выглядеть иначе, но мы надеемся, что теперь вы знаете, как исправить эту ошибку в своём проекте и как избежать её появления. Если то, о чём шла речь, вам не подходит — возможно, вам поможет разбор следующих ошибок.
2. TypeError: «undefined» is not an object (evaluating…
Эта ошибка возникает в браузере Safari при попытке прочесть свойство или вызвать метод неопределённого объекта. Взглянуть на эту ошибку можно с помощью консоли инструментов разработчика Safari. На самом деле, тут перед нами та же самая проблема, которую мы разбирали выше для Chrome, но в Safari она приводит к другому сообщению об ошибке.
Ошибка «undefined» is not an object
Исправлять эту ошибку надо так же, как в предыдущем примере.
3. TypeError: null is not an object (evaluating
Эта ошибка возникает в Safari при попытке обратиться к методу или свойству переменной, представленной значением null
. Вот как это выглядит в консоли разработчика Safari.
Ошибка TypeError: null is not an object
Напомним, что в JavaScript null
и undefined
— это не одно и то же, именно поэтому мы видим разные сообщения об ошибках. Смысл значения undefined
, записанного в переменную, говорит о том, что переменной не назначено никакого значения, а null
указывает на пустое значение. Для того чтобы убедиться в том, что null
не равно undefined
, можно сравнить их с использованием оператора строгого равенства:
Сравнение undefined и null с помощью операторов нестрогого и строгого равенства
Одна из причин возникновения подобной ошибки в реальных приложениях заключается в попытке использования элемента DOM в JavaScript до загрузки элемента. Происходит это из-за того, что DOM API возвращает null
для ссылок на пустые объекты.
Любой JS-код, который работает с элементами DOM, должен выполняться после создания элементов DOM. Интерпретация JS-кода производится сверху вниз по мере появления его в HTML-документе. Поэтому если тег с программой окажется перед кодом, описывающим элементы DOM, программа будет выполнена в ходе разбора страницы до его завершения. Эта ошибка проявится, если элемент DOM, к которому обращаются из скрипта, не был создан до загрузки этого скрипта.
В следующем примере мы можем исправить проблему, добавив в код прослушиватель событий, который оповестит нас о том, что страница полностью загружена. После срабатывания обработчика события, добавленного с помощью addEventListener
, метод init()
сможет правильно работать с элементами DOM.
4. (unknown): Script error
Эта ошибка возникает в том случае, когда неперехваченная ошибка JavaScript пересекает границы доменов при нарушении политики кросс-доменных ограничений. Например, если ваш JS-код размещён на CDN-ресурсе, в сообщении о любой неперехваченной ошибке (то есть, об ошибке, которая не перехвачена в блоке try-catch
и дошла до обработчика window.onerror
) будет указано Script error
, а не полезная для целей устранения этой ошибки информация. Это — один из браузерных механизмов безопасности, направленный на предотвращение передачи данных между фрагментами кода, источниками которого являются разные домены, и которым в обычных условиях запрещено обмениваться информацией.
Вот последовательность действий, которая поможет увидеть эту ошибку.
1. Отправка заголовка Access-Control-Allow-Origin
.
Установка заголовка Access-Control-Allow-Origin
в состояние *
указывает на то, что к ресурсу можно получить доступ из любого домена.
Знак звёздочки можно, при необходимости, заменить на конкретный домен, например так: Access-Control-Allow-Origin: www.example.com
. Однако поддержка нескольких доменов — дело довольно сложное. Такая поддержка может не стоить затраченных на её обеспечение усилий, если вы используете CDN, из-за возможного возникновения проблем с кэшированием. Подробности об этом можно посмотреть здесь.
Вот примеры установки этого заголовка в различных окружениях.
Apache
В папке, из которой будут загружаться ваши JavaScript-файлы, создайте файл .htaccess
со следующим содержимым:
Header add Access-Control-Allow-Origin "*"
Nginx
Добавьте директиву add_header
к блоку location
, который отвечает за обслуживание ваших JS-файлов:
location ~ ^/assets/ {
add_header Access-Control-Allow-Origin *;
}
HAProxy
Добавьте следующую настройку к параметрам системы, ответственной за поддержку JS-файлов:
rspadd Access-Control-Allow-Origin:\ *
2. Установите crossorigin="anonymous"
в теге .
В вашем HTML-файле для каждого из скриптов, для которого установлен заголовок Access-Control-Allow-Origin
, установите crossorigin="anonymous"
в теге . Перед добавлением свойства
crossorigin
к тегу проверьте отправку заголовка для файла скрипта. В Firefox, если атрибут
crossorigin
присутствует, а заголовок Access-Control-Allow-Origin
— нет, скрипт выполнен не будет.
5. TypeError: Object doesn«t support property
Эта ошибка возникает в IE при попытке вызова неопределённого метода. Увидеть эту ошибку можно в консоли разработчика IE.
Ошибка Object doesn«t support property
Эта ошибка эквивалентна ошибке "TypeError: ‘undefined’ is not a function"
, которая возникает в Chrome. Обращаем ваше внимание на то, что речь идёт об одной и той же логической ошибке, о которой различные браузеры сообщают по-разному.
Это — обычная для IE проблема, возникающая в веб-приложениях, которые используют возможности пространств имён JavaScript. Когда возникает эта ошибка, то в 99.9% случаев её причиной является неспособность IE привязывать методы, расположенные в текущем пространстве имён, к ключевому слову this
. Например, предположим, что имеется объект Rollbar
с методом isAwesome
. Обычно, находясь в пределах этого объекта, метод isAwesome
можно вызвать так:
this.isAwesome();
Chrome, Firefox и Opera нормально воспримут такую команду. IE же её не поймёт. Таким образом, лучше всего, при использовании подобных конструкций, всегда предварять имя метода именем объекта (пространства имён), в котором он определён:
Rollbar.isAwesome();
6. TypeError: «undefined» is not a function
Эта ошибка возникает в Chrome при попытке вызова неопределённой функции. Взглянуть на эту ошибку можно в консоли инструментов разработчика Chrome и в аналогичной консоли Firefox.
Ошибка TypeError: «undefined» is not a function
Так как подходы к программированию на JavaScript и шаблоны проектирования постоянно усложняются, наблюдается и соответствующий рост числа ситуаций, в которых, внутри функций обратного вызова и замыканий, появляются области видимости, в которых используются ссылки на собственные методы и свойства с использованием ключевого слова this
, что является довольно распространённым источником путаницы и ошибок.
Рассмотрим следующий пример:
function testFunction() {
this.clearLocalStorage();
this.timer = setTimeout(function() {
this.clearBoard(); // что такое "this"?
}, 0);
};
Выполнение вышеприведённого кода приведёт к следующей ошибке: "Uncaught TypeError: undefined is not a function."
Причина появления этой ошибки заключается в том, что при вызове setTimeout()
мы, на самом деле, вызываем window.setTimeout()
. Как результат, анонимная функция, которая передаётся setTimeout()
, оказывается определена в контексте объекта window
, у которого нет метода clearBoard()
.
Традиционный подход к решению этой проблемы, совместимый со старыми версиями браузеров, заключается в том, чтобы просто сохранить ссылку на this
в некоей переменной, к которой потом можно будет обратиться из замыкания. Например, это может выглядеть так:
function testFunction () {
this.clearLocalStorage();
var self = this; // сохраним ссылку на 'this' пока оно является тем, чем мы его считаем!
this.timer = setTimeout(function(){
self.clearBoard();
}, 0);
};
В более современных браузерах можно использовать метод bind()
для передачи необходимой ссылки:
function testFunction () {
this.clearLocalStorage();
this.timer = setTimeout(this.reset.bind(this), 0); // осуществляем привязку к 'this'
};
function testFunction(){
this.clearBoard(); //возвращаемся к контексту правильного 'this'!
};
7. Uncaught RangeError: Maximum call stack
У возникновения этой ошибки, например, в Chrome, есть несколько причин. Одна из них — бесконечный вызов рекурсивной функции. Вот как выглядит эта ошибка в консоли разработчика Chrome:
Ошибка Maximum call stack size exceeded
Подобное может произойти и в том случае, когда функции передают значение, находящееся за пределами некоего допустимого диапазона значений. Многие функции принимают лишь числа, находящиеся в определённом диапазоне. Например, функции Number.toExponential(digits)
и Number.toFixed(digits)
принимают аргумент digits
, представленный числом от 0 до 20, а функция Number.toPrecision(digits)
принимает числа от 1 до 21. Взглянем на ситуации, в которых вызов этих и некоторых других функций приводит к ошибкам:
var a = new Array(4294967295); //OK
var b = new Array(-1); // ошибка!
var num = 2.555555;
document.writeln(num.toExponential(4)); //OK
document.writeln(num.toExponential(-2)); //ошибка!
num = 2.9999;
document.writeln(num.toFixed(2)); //OK
document.writeln(num.toFixed(25)); // ошибка!
num = 2.3456;
document.writeln(num.toPrecision(1)); //OK
document.writeln(num.toPrecision(22)); // ошибка!
8. TypeError: Cannot read property «length»
Эта ошибка возникает в Chrome при попытке прочесть свойство length
переменной, в которую записано undefined
. Взглянем на эту ошибку в консоли инструментов разработчика Chrome.
Ошибка Cannot read property «length»
Обычно, обращаясь к свойству length
, узнают длину массивов, но вышеописанная ошибка может возникнуть если массив не инициализирован, или если имя переменной скрыто в области видимости, недоступной из того места, где к этой переменной пытаются обратиться. Для того чтобы лучше понять сущность этой ошибки, рассмотрим следующий пример:
var testArray= ["Test"];
function testFunction(testArray) {
for (var i = 0; i < testArray.length; i++) {
console.log(testArray[i]);
}
}
testFunction();
При объявлении функции с параметрами эти параметры становятся для неё локальными переменными. В нашем примере это означает, что даже если в области видимости, окружающей функцию, есть переменная testArray
, параметр с таким же именем скроет эту переменную и будет восприниматься как локальная переменная функции.
Для того чтобы решить эту проблему, в нашем случае можно пойти одним из следующих двух путей:
- Удаление параметра, заданного при объявлении функции (как видно из примера, мы хотим работать с помощью функции с массивом, который объявлен за её пределами, поэтому тут можно обойтись и без параметра функции):
var testArray = ["Test"]; /* Предварительное условие: определение testArray за пределами функции */ function testFunction(/* без параметров */) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction();
- Вызов функции с передачей ей ранее объявленного массива:
var testArray = ["Test"]; function testFunction(testArray) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction(testArray);
9. Uncaught TypeError: Cannot set property
Когда мы пытаемся получить доступ к неопределённой переменной, то мы, фактически, работаем со значением типа undefined
, а этот тип не поддерживает чтение или запись свойств. В подобном случае приложение выдаст следующую ошибку:
"Uncaught TypeError cannot set property of undefined."
Взглянем на неё в браузере Chrome.
Ошибка Cannot set property
Если объект test
не существует, будет выдана ошибка "Uncaught TypeError cannot set property of undefined."
10. ReferenceError: event is not defined
Эта ошибка возникает при попытке получить доступ к неопределённой переменной, или к переменной, которая находится за пределами текущей области видимости. Взглянем на неё в консоли Chrome:
Ошибка ReferenceError: foo is not defined
Если вы сталкиваетесь с этой ошибкой при использовании системы обработки событий, убедитесь, что вы работаете с объектом события, переданным в качестве параметра. Более старые браузеры, вроде IE, предлагают глобальный доступ к событиям, но это не характерно для всех браузеров. Эту ситуацию пытаются исправить библиотеки вроде jQuery. В любом случае рекомендуется использовать именно тот объект события, которые передан в функцию обработки событий.
function myFunction(event) {
event = event.which || event.keyCode;
if(event.keyCode===13){
alert(event.keyCode);
}
}
Итоги
Надеемся, вы узнали из нашего рассказа об ошибках что-нибудь новое, такое, что поможет вам избежать ошибок в будущем, а может быть — уже помогло найти ответ на вопрос, который давно не давал вам покоя.
Уважаемые читатели! С какими JS-ошибками вы сталкивались в продакшне?