Топ-10 найпоширеніших помилок JavaScript (та як їх уникнути)

12 хв. читання

Щоб визначити десять найрозповсюдженіших помилок JavaScript, команда Rollbar переглянула понад тисячу проектів своїх клієнтів. Ось які результати вони отримали:

Топ-10 найпоширеніших помилок JavaScript (та як їх уникнути)

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

1. Uncaught TypeError: Cannot read property

Якщо ви JavaScript розробник, то ви бачили цю помилку частіше, ніж вам хотілося. Вона виникає у Chrome, коли ви намагаєтесь прочитати дані з властивості, або викликати методи для об'єкта, що не існує. Можна легко протестувати її в консолі розробника Chrome:

Uncaught TypeError: Cannot read property

Ця помилка може виникати з багатьох причин, проте зазвичай ми отримуємо її внаслідок неправильної ініціалізації стану під час рендерингу компонентів UI.

Розглянемо реальний приклад. Скористаємося React, проте представлені принципи ініціалізації відносяться і до інших популярних фреймворків: Angular, Vue тощо.

class Quiz extends Component {
  componentWillMount() {
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item =>
          <li key={item.id}>{item.name}</li>
        )}
      </ul>
    );
  }
}

У прикладі вище реалізовано дві важливі речі:

  1. Стан компонентів (this.state) при ініціалізації містить undefined.
  2. Коли ви отримуєте дані асинхронно, компонент буде відображатися хоча б раз перед завантаженням даних – незалежно від того, чи визначено в конструкторі componentWillMount або componentDidMount. Коли Quiz рендериться в перший раз, this.state.items має значення undefined. Це означає, що ItemList отримує елементи зі значенням undefined, і в консолі ви отримаєте помилку "Uncaught TypeError: Cannot read property 'map' of undefined".

Найпростіший шлях вирішення такої проблеми – ініціалізація стану з визначеними значеннями в конструкторі.

class Quiz extends Component {
  // Додано this:
  constructor(props) {
    super(props);

    // Призначено сам стан і значення за замовчуванням для елементів
    this.state = {
      items: []
    };
  }

  componentWillMount() {
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item =>
          <li key={item.id}>{item.name}</li>
        )}
      </ul>
    );
  }
}

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

2. TypeError: 'undefined' is not an object (evaluating

Ця помилка властива Safari при читанні властивостей або виклику методів невизначених об'єктів. Ви можете легко протестувати її у консолі для розробників Safari. Вона є точною копією помилки у Chrome, яка була приведена вище, проте з іншим повідомленням про помилку.

TypeError: '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 означає, що значення пусте. Щоб підтвердити, що вони не еквівалентні, спробуйте використати оператор еквівалентності:

TypeError: null is not an object

Одна з причин виникнення такої помилки на реальному проекті – спроба використання DOM елементу у JavaScript коді до того, як елемент завантажився. Тому, що DOM API повертає null у разі порожнього посилання на об'єкт.

Будь-який код JS, що виконується і має справу з DOM елементами, має виконуватися лише після створення DOM елементів, адже у JavaScript код інтерпретується зверху-вниз, оскільки він розташований у HTML. Тож, якщо існує тег <script> перед DOM елементом, то код, який в ньому міститься, буде виконуватися під час завантаження браузером HTML сторінки. Ви отримаєте цю помилку, якщо DOM елемент не буде створений до завантаження скрипта.

У цьому прикладі, ми можемо вирішити проблему, додаючи обробник події, що буде повідомляти нас, коли сторінка буде готова. Як тільки addEventListener виконується, метод init() зможе використовувати DOM елементи.

<script>
  function init() {
    var myButton = document.getElementById("myButton");
    var myTextfield = document.getElementById("myTextfield");
    myButton.onclick = function() {
      var userName = myTextfield.value;
    }
  }
  document.addEventListener('readystatechange', function() {
    if (document.readyState === "complete") {
      init();
    }
  });
</script>

<form>
  <input type="text" id="myTextfield" placeholder="Type your name" />
  <input type="button" id="myButton" value="Go" />
</form>

4. (unknown): Script error

З нею можна стикнутися, якщо необроблена помилка JavaScript перетинає межі домену, а це суперечить політиці веб. Наприклад, якщо розмістити код JavaScript на CDN, то будь-які необроблені помилки (помилки, що переходять до обробника window.onerror, замість того, щоб бути спійманими в try-catch), будуть визначені, як прості "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 до блоку розташування, який обробляє ваші JavaScript файли:

location ~ ^/assets/ {
    add_header Access-Control-Allow-Origin *;
}

HAProxy

Додайте наступний рядок до ваших налаштувань:

rspadd Access-Control-Allow-Origin:\\ *
  1. Додайте crossorigin="anonymous" до властивостей тегу <script>

У HTML файлі, для кожного тегу <script>, якому ви встановили заголовок Access-Control-Allow-Origin додайте властивість crossorigin="anonymous". Впевніться, що ви визначили чи посилається заголовок з файлу скрипта до додання властивості crossorigin. У Firefox скрипт не буде працювати, якщо атрибут crossorigin зазначено, проте заголовок Access-Control-Allow-Origin відсутній.

5. TypeError: Object doesn't support property

Така помилка виникає в IE при спробі викликати невизначений метод.

TypeError: Object doesn't support property

Вона еквівалентна до помилки "TypeError: 'undefined' is not a function" в Chrome. Варто пам'ятати, що різні браузери мають різні повідомлення про помилку.

Це звичайна проблема IE при роботі з веб-застосунками, що працюють з простором імен в JavaScript. Коли виникає подібна проблема, то у 99.9% випадків її причина – неможливість прив'язки методів з наявним простором імен, через ключове слово this. Наприклад, якщо ви маєте простір імен Codeguida і в ньому є метод isAwesome. Зазвичай, якщо керування знаходиться в просторі імен Codeguida, ви можете викликати метод isAwesome так:

this.isAwesome();

Такий синтаксис нормально сприймається у Chrome, Firefox and Opera, однак IE до них не відноситься. Тож вам необхідно буде використовувати префікс імені актуального простору імен.

Codeguida.isAwesome();

6. TypeError: 'undefined' is not a function

Це помилка, що з'являється в Chrome під час виклику невизначеної функції. Ви можете протестувати її у консолі розробника.

TypeError: 'undefined' is not a function

На протязі багатьох років техніки JavaScript та патерни проектування стають все більш витонченими, відповідне збільшення самоповторень, колбеків і замикань стає причиною поширення плутанини.

Розглянемо приклад:

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!
  this.timer = setTimeout(function(){
    self.clearBoard();  
  }, 0);
};

Альтернатива у нових браузерах – використання методу bind() для передачі прямого посилання:

function testFunction () {
  this.clearLocalStorage();
  this.timer = setTimeout(this.reset.bind(this), 0);  // bind to 'this'
};

function testFunction(){
    this.clearBoard();    //назад у контекст правильного 'this'!
};

7. Uncaught RangeError: Maximum call stack

Помилки такого роду виникають як наслідок декількох обставин. Одна з них – виклик рекурсивної функції, що не має умови завершення. Приклад:

Uncaught RangeError: Maximum call stack

Може трапитися, що ви передали функції значення за межами діапазону. Багато функцій приймають тільки специфічні типи вхідних даних. Як приклад, Number.toExponential(digits) і Number.toFixed(digits) приймають цифри від 0 до 20, а Number.toPrecision(digits) від 1 до 21.

var a = new Array(4294967295);  //OK
var b = new Array(-1); //range error

var num = 2.555555;
document.writeln(num.toExponential(4));  //OK
document.writeln(num.toExponential(-2)); //range error!

num = 2.9999;
document.writeln(num.toFixed(2));   //OK
document.writeln(num.toFixed(25));  //range error!

num = 2.3456;
document.writeln(num.toPrecision(1));   //OK
document.writeln(num.toPrecision(22));  //range error!

8. TypeError: Cannot read property 'length'

Стає наслідком спроби визначення довжини даних з невизначеної змінної. Ви можете протестувати її у Chrome.

TypeError: Cannot read property 'length'

Зазвичай ви знаходите довжину масиву, але ви можете отримати помилку якщо цей масив не ініціалізовано, або ім'я змінної знаходиться в іншому контексті. Розглянемо наступний приклад:

var testArray= ["Test"];

function testFunction(testArray) {
    for (var i = 0; i < testArray.length; i++) {
      console.log(testArray[i]);
    }
}

testFunction();

Коли ви оголошуєте функцію з параметрами, вони стають локальними. Це означає, що навіть якщо ви маєте змінну з іменем testArray, параметри з таким само ім'ям у функцій будуть локальними.

Існує два шляхи вирішення проблеми:

  1. Зовнішні параметри при оголошенні функції (це означає, що ви зможете отримати доступ до змінних поза функцією, тож вам не потрібні локальні параметри).
var testArray = ["Test"];

/* Precondition: defined testArray outside of a function */
function testFunction(/* No params */) {
    for (var i = 0; i < testArray.length; i++) {
      console.log(testArray[i]);
    }
}

testFunction();
  1. Виклик функції передачею масиву з визначеними значеннями:
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, і ми не можемо задати властивісті undefined. У такому випадку додаток буде повертати "Uncaught TypeError cannot set property of undefined."

Наприклад, у браузері Chrome:

Uncaught TypeError: Cannot set property

Якщо об'єкт test не існує, помилка буде повертати "Uncaught TypeError cannot set property of undefined."

10. ReferenceError: event is not defined

Така помилка виникає якщо ви намагаєтесь отримати доступ до невизначеної змінної, або вона поза межами простору імен. Можна протестувати у Chrome:

ReferenceError: event is not defined

Якщо ви отримаєте помилку при використанні системи обробки подій, то вам слід впевнитись, у тому, що ви передали об'єкт події як параметр. Старіші версії браузерів, такі як IE пропонують глобальні події для змінних, і Chrome автоматично прив'язує змінну події до обробника. Проте Firefox так робити не буде. Такі бібліотеки як jQuery спробують нормалізувати поведінку. Проте, найкращий спосіб – використання переданої в ваш обробник подій функції.

document.addEventListener("mousemove", function (event) {
 console.log(event);
})
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 869
Приєднався: 1 рік тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід