10 найпоширеніших проблем з JavaScript, з якими стикаються розробники

  • 11 ноября, 15:01
  • 3159
  • 0

Сьогодні JavaScript лежить в основі практично всіх сучасних веб-додатків. Ось чому проблеми з JavaScript і пошук помилок, які їх спричиняють, знаходяться на першому місці для веб-розробників.

Потужні бібліотеки та фреймворки на основі JavaScript для розробки односторінкових додатків (SPA) , графіки й анімації, а також серверні платформи JavaScript — це не щось нове. JavaScript справді став лідером у світі розробки веб-додатків, і тому він стає все більш важливим навиком для опанування.

Спочатку JavaScript може здатися досить простим. І справді, створити базову функціональність JavaScript у веб-сторінці є досить простим завданням для будь-якого досвідченого розробника програмного забезпечення, навіть якщо він новачок у JavaScript. І все ж ця мова значно складніша та потужніша, ніж можна було б спочатку подумати. Дійсно, багато тонкощів JavaScript призводять до ряду поширених проблем, які заважають йому працювати (10 з яких ми обговорюємо тут), про які важливо знати та уникати.

Проблема JavaScript №1: неправильні посилання наthis

Серед розробників JavaScript не вистачає одностайності щодо ключового слова JavaScriptthis .

Оскільки методи кодування JavaScript і шаблони проектування з роками стають дедалі складнішими, відбулося відповідне збільшення кількості самопосилальних областей у зворотних викликах і закриттях, які є досить поширеним джерелом « this/that», що спричиняє проблеми з JavaScript.

Розглянемо цей приклад фрагмента коду:

Game.prototype.restart = function () {    this.clearLocalStorage();    this.timer = setTimeout(function() {    this.clearBoard();    // What is "this"?    }, 0);
};

Виконання наведеного вище коду призводить до такої помилки:

Uncaught TypeError: undefined is not a function

 Вся справа в контексті. Причина, по якій ви отримуєте наведену вище помилку, полягає в тому, що, коли ви викликаєте setTimeout(), ви насправді викликаєте window.setTimeout(). У результаті анонімна функція, яка передається setTimeout(), визначається в контексті windowоб’єкта, який не має clearBoard()методу.

Традиційним рішенням, сумісним зі старим браузером, є просто зберегти ваше посилання thisу змінній, яка потім може бути успадкована закриттям; наприклад:

Game.prototype.restart = function () {    this.clearLocalStorage();    var self = this;   // Save reference to 'this', while it's still this!    this.timer = setTimeout(function(){    self.clearBoard();    // Oh OK, I do know who 'self' is!    }, 0);
};

Крім того, у новіших браузерах ви можете використовувати bind()метод для передачі належного посилання:

Game.prototype.restart = function () {    this.clearLocalStorage();    this.timer = setTimeout(this.reset.bind(this), 0);  // Bind to 'this'
};

Game.prototype.reset = function(){    this.clearBoard();    // Ahhh, back in the context of the right 'this'!
};

Проблема JavaScript №2: Вважається, що існує область дії на рівні блоку

Поширеним джерелом плутанини серед розробників JavaScript (і, отже, поширеним джерелом помилок) є припущення, що JavaScript створює нову область для кожного блоку коду. Хоча це вірно для багатьох інших мов, це не так для JavaScript. Розглянемо, наприклад, такий код:

for (var i = 0; i < 10; i++) {    /* ... */
}
console.log(i);  // What will this output?

Якщо ви здогадуєтеся, що console.log()виклик виведе undefinedабо видасть помилку, ви вгадали неправильно. Вірте чи ні, але воно виведе 10.

У більшості інших мов наведений вище код призведе до помилки, оскільки «життя» (тобто область) змінної iбуде обмежено forблоком. Однак у JavaScript це не так, і змінна iзалишається в області навіть після завершення forциклу, зберігаючи своє останнє значення після виходу з циклу. (Ця поведінка відома, до речі, як змінний підйом )

Підтримка областей на рівні блоків у JavaScript доступна через ключове letслово . Ключове letслово вже багато років широко підтримується браузерами та внутрішніми механізмами JavaScript, такими як Node.js.

Проблема JavaScript №3: Створення витоків пам’яті

Витоки пам’яті є майже неминучою проблемою JavaScript, якщо ви свідомо не кодуєте, щоб уникнути їх. Існує багато способів їх появи, тому ми лише висвітлимо кілька найбільш поширених випадків.

Приклад витоку пам’яті 1: Висячі посилання на неіснуючі об’єкти

Розглянемо наступний код:

var theThing = null;
var replaceThing = function () {  var priorThing = theThing;  // Hold on to the prior thing  var unused = function () {    // 'unused' is the only place where 'priorThing' is referenced,    // but 'unused' never gets invoked    if (priorThing) {      console.log("hi");    }  };  theThing = {    longStr: new Array(1000000).join('*'),  // Create a 1MB object    someMethod: function () {      console.log(someMessage);    }  };
};
setInterval(replaceThing, 1000);    // Invoke `replaceThing' once every second

Якщо ви запустите наведений вище код і відстежите використання пам’яті, ви побачите, що у вас значний витік пам’яті, витік цілого мегабайта на секунду! І навіть ручний  гарбідж колектор (GC) не допомагає. Але чому?

Давайте розглянемо це більш детально:

Кожен theThingоб’єкт містить власний longStrоб’єкт розміром 1 МБ. Щосекунди, коли ми викликаємо replaceThing, він утримує посилання на попередній theThingоб’єкт у priorThing. Але ми все одно не думаємо, що це буде проблемою, оскільки кожного разу, на яке раніше priorThingпосилалися, буде розіменовано (коли priorThingскидається через priorThing = theThing;). Крім того, на нього посилаються лише в основному тексті replaceThingта у функції unused, яка фактично ніколи не використовується.

Отже, нам знову залишається дивуватися, чому тут стається витік пам’яті.

Щоб зрозуміти, що відбувається, нам потрібно краще зрозуміти внутрішню роботу JavaScript. Типовий спосіб реалізації замикань полягає в тому, що кожен функціональний об’єкт має посилання на об’єкт у стилі словника, що представляє його лексичний обсяг. Якщо обидві функції, визначені всередині , replaceThingфактично використовуються priorThing, було б важливо, щоб вони обидві отримували той самий об’єкт, навіть якщо priorThingйого призначають знову і знову, щоб обидві функції використовували те саме лексичне середовище. Але як тільки змінна використовується будь-яким замиканням, вона потрапляє в лексичне середовище, яке спільно використовують усі закриття в цій області. І саме цей маленький нюанс призводить до витоку пам’яті .

Приклад витоку пам’яті 2: циклічні посилання

Розглянемо цей фрагмент коду:

function addClickHandler(element) {    element.click = function onClick(e) {        alert("Clicked the " + element.nodeName)    }
}

Тут onClickмає закриття, яке зберігає посилання на element(через element.nodeName). Крім того, присвоєння onClick, element.clickстворює циклічне посилання; тобто: elementonClickelementonClickelement

Цікаво, що навіть якщо elementвидалити з DOM, наведене вище циклове самопосилання запобігатиме збору elementта onClick, отже, витоку пам’яті.

Уникнення витоку пам'яті: Основи

Управління пам’яттю JavaScript (і, зокрема, збирання сміття ) значною мірою базується на понятті доступності об’єкта.

Наступні об’єкти вважаються доступними та відомі як «корені»:

  1. Об’єкти, на які посилаються з будь-якого місця в поточному стеку викликів (тобто всі локальні змінні та параметри у функціях, які зараз викликаються, і всі змінні в області закриття)
  2. Усі глобальні змінні

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

У браузері є збірник сміття, який очищає пам'ять, зайняту недоступними об'єктами; іншими словами, об'єкти будуть видалені з пам'яті тоді і тільки якщо GC вважає, що вони недоступні. На жаль, досить легко отримати неіснуючі «зомбі» об’єкти, які більше не використовуються, але які GC все ще вважає «досяжними».

Проблема JavaScript №4: Плутанина щодо рівності

Однією зі зручностей JavaScript є те, що він автоматично перетворює будь-яке значення, на яке посилаються в логічному контексті, до логічного значення. Але є випадки, коли це може бути настільки ж заплутаним, наскільки зручним. Деякі з наведених нижче, наприклад, відомі як проблеми для багатьох розробників JavaScript:

// All of these evaluate to 'true'!
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);

// And these do too!
if ({}) // ...
if ([]) // ...

Стосовно двох останніх, незважаючи на те, що вони порожні (що може змусити когось повірити, що вони оцінюватимуться як false), обидва {}та []є фактично об’єктами, і будь -якому об’єкту буде приведено логічне значення trueв JavaScript, відповідно до ECMA-262 специфікації .

 Відповідно, якщо явно не потрібне приведення типу, зазвичай найкраще використовувати ===  !==(а не == !=), щоб уникнути будь-яких ненавмисних побічних ефектів приведення типу. ( ==і !=автоматично виконує перетворення типу під час порівняння двох речей, тоді як ===і !==виконує те саме порівняння без перетворення типу.)

І зовсім як додатковий момент, але оскільки ми говоримо про приведення до типу та порівняння, варто згадати, що порівняння NaNз чим завгодно (навіть NaN!) завжди повертатиме false. Тому ви не можете використовувати оператори рівності ( ==, ===, !=, !==), щоб визначити, чи є значення NaNчи ні. Замість цього використовуйте вбудовану глобальну isNaN()функцію:

console.log(NaN == NaN);    // False
console.log(NaN === NaN);   // False
console.log(isNaN(NaN));    // True

Проблема JavaScript №5: неефективна маніпуляція DOM

JavaScript дозволяє відносно легко маніпулювати DOM (тобто додавати, змінювати та видаляти елементи), але не робить нічого для підвищення ефективності цього.

Типовим прикладом є код, який додає серію елементів DOM по одному. Код, який послідовно додає кілька елементів DOM, є неефективним і, швидше за все, не працюватиме належним чином.

Однією з ефективних альтернатив, коли потрібно додати декілька елементів DOM, є використання замість них фрагментів документів , що покращує ефективність і продуктивність.

Наприклад:

var div = document.getElementsByTagName("my_div");
    
var fragment = document.createDocumentFragment();

for (var e = 0; e < elems.length; e++) {  // elems previously set to list of elements
    fragment.appendChild(elems[e]);
}
div.appendChild(fragment.cloneNode(true));

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

Проблема JavaScript №6: неправильне використання визначень функцій усередині forциклів

Розглянемо цей код:

var elements = document.getElementsByTagName('input');
var n = elements.length;    // Assume we have 10 elements for this example
for (var i = 0; i < n; i++) {    elements[i].onclick = function() {        console.log("This is element #" + i);
    };
}

Виходячи з наведеного вище коду, якби було 10 елементів введення, клацання будь -якого з них відображало б «Це елемент №10»! Це пояснюється тим, що до моменту onclickвиклику для будь -якого з елементів вищезгаданий forцикл буде завершено, а значення iвже буде 10 (для всіх ).

Ось як ми можемо виправити вищезазначені проблеми з JavaScript, щоб досягти бажаної поведінки:

var elements = document.getElementsByTagName('input');
var n = elements.length;    // Assume we have 10 elements for this example
var makeHandler = function(num) {  // Outer function     return function() {   // Inner function         console.log("This is element #" + num);     };
};
for (var i = 0; i < n; i++) {    elements[i].onclick = makeHandler(i+1);
}

У цій переглянутій версії коду makeHandlerнегайно виконується щоразу, коли ми проходимо через цикл, щоразу отримуючи поточне значення i+1та прив’язуючи його до numзмінної області видимості. Зовнішня функція повертає внутрішню функцію (яка також використовує цю numзмінну області видимості), а для елемента onclickвстановлюється ця внутрішня функція. Це гарантує, що кожен onclickотримує та використовує належне iзначення (через numзмінну області видимості).

Проблема JavaScript №7: Нездатність належним чином використовувати успадкування прототипів

Напрочуд високий відсоток розробників JavaScript не в змозі повністю зрозуміти, а отже, повністю використати особливості успадкування прототипів.

Ось простий приклад. Розглянемо цей код:

BaseObject = function(name) {    if (typeof name !== "undefined") {        this.name = name;    } else {        this.name = 'default'    }
};

Здається досить простим. Якщо ви вказали ім’я, використовуйте його, інакше встановіть ім’я «за замовчуванням». Наприклад:

var firstObj = new BaseObject();
var secondObj = new BaseObject('unique');

console.log(firstObj.name);  // -> Results in 'default'
console.log(secondObj.name); // -> Results in 'unique'

Але що, якби ми зробили це:

delete secondObj.name;

Тоді ми отримаємо:

console.log(secondObj.name); // -> Results in 'undefined'

Але хіба не краще було б повернутися до «за замовчуванням»? Це можна легко зробити, якщо ми модифікуємо вихідний код для використання прототипного успадкування наступним чином:

BaseObject = function (name) {    if(typeof name !== "undefined") {        this.name = name;    }
};

BaseObject.prototype.name = 'default';

У цій версії BaseObjectуспадковує nameвластивість від свого prototypeоб’єкта, де встановлено (за замовчуванням) значення 'default'. Таким чином, якщо конструктор викликається без імені, ім’я за умовчанням буде default. І аналогічно, якщо nameвластивість видаляється з екземпляра BaseObject, тоді буде здійснено пошук у ланцюжку прототипів, і nameвластивість буде отримано з prototype об’єкта, де його значення все ще є 'default'. Отже, тепер ми отримуємо:

var thirdObj = new BaseObject('unique');
console.log(thirdObj.name);  // -> Results in 'unique'

delete thirdObj.name;
console.log(thirdObj.name);  // -> Results in 'default'

Проблема JavaScript №8: Створення неправильних посилань на методи екземплярів

Давайте визначимо простий об’єкт і створимо його екземпляр наступним чином:

var MyObject = function() {}
    
MyObject.prototype.whoAmI = function() {    console.log(this === window ? "window" : "MyObj");
};

var obj = new MyObject();

Тепер, для зручності, давайте створимо посилання на whoAmIметод, імовірно, щоб ми могли отримати до нього доступ просто за допомогою, whoAmI()а не за допомогою long obj.whoAmI():

var whoAmI = obj.whoAmI;

І щоб переконатися, що все виглядає сумісно, давайте роздрукуємо значення нашої нової whoAmIзмінної:

console.log(whoAmI);

Виходи:

function () {    console.log(this === window ? "window" : "MyObj");
}

Гаразд, круто. Виглядає добре.

Але тепер подивіться на різницю, коли ми викликаємо obj.whoAmI()та нашу зручну довідку whoAmI():

obj.whoAmI();  // Outputs "MyObj" (as expected)
whoAmI();      // Outputs "window" (uh-oh!)

Що пішло не так? Коли ми робили призначення var whoAmI = obj.whoAmI;, нова змінна whoAmIвизначалася в глобальному просторі імен. У результаті його значення thisдорівнює window, а неobj екземпляр MyObject!

Таким чином, якщо нам справді потрібно створити посилання на існуючий метод об’єкта, ми маємо переконатися, що це зробимо в просторі імен цього об’єкта, щоб зберегти значення this. Один із способів зробити це:

var MyObject = function() {}
    
MyObject.prototype.whoAmI = function() {    console.log(this === window ? "window" : "MyObj");
};

var obj = new MyObject();
obj.w = obj.whoAmI;   // Still in the obj namespace

obj.whoAmI();  // Outputs "MyObj" (as expected)
obj.w();       // Outputs "MyObj" (as expected)

Проблема JavaScript №9: надання рядка як першого аргументу setTimeoutабоsetInterval

Для початку, давайте прояснимо дещо: надання рядка як першого аргументу setTimeoutабо setIntervalсамо по собі не є помилкою . Це абсолютно законний код JavaScript. Проблема тут більше в продуктивності та ефективності. Рідко пояснюється, що якщо ви передаєте рядок як перший аргумент або , він буде переданий конструктору функції для перетворення в нову функцію. Цей процес може бути повільним і неефективним, і рідко буває необхідним.

Альтернативою передачі рядка як першого аргументу цим методам є передача замість цього функції . Давайте розглянемо приклад.

Отже, тут буде досить типове використання setIntervaland setTimeout, передаючи рядок як перший параметр:

setInterval("logTime()", 1000);
setTimeout("logMessage('" + msgValue + "')", 1000);

Кращим вибором було б передати функцію як початковий аргумент; наприклад:

setInterval(logTime, 1000);   // Passing the logTime function to setInterval
    
setTimeout(function() {       // Passing an anonymous function to setTimeout    logMessage(msgValue);     // (msgValue is still accessible in this scope)
}, 1000);

Проблема JavaScript №10: невикористання «строгого режиму»

Як пояснюється в Посібнику з найму JavaScript , «суворий режим» (тобто, у тому числі 'use strict';на початку вихідних файлів JavaScript) — це спосіб добровільно застосувати більш суворий аналіз і обробку помилок у вашому коді JavaScript під час виконання, а також зробити його більш безпечним .

Хоча, за загальним визнанням, невикористання суворого режиму не є «помилкою» як такою, його використання все більше заохочується, а його пропуск все частіше вважається поганим тоном.

Ось деякі основні переваги суворого режиму:

  1. Полегшує налагодження. Помилки в коді, які в іншому випадку були б проігноровані або виникли б мовчки, тепер створюватимуть помилки або створюватимуть винятки, повідомляючи вас про проблеми з JavaScript у вашій кодовій базі та швидше спрямовуючи вас до їх джерела.
  2. Запобігає випадковим глобалам. Без строгого режиму присвоєння значення неоголошеній змінній автоматично створює глобальну змінну з таким іменем. Це одна з найпоширеніших помилок JavaScript. У строгому режимі спроба зробити це видає помилку.
  3. Усуває thisпримус. Без суворого режиму посилання на thisзначення null або undefined автоматично приводиться до глобального. Це може спричинити багато неприємних помилок. У строгому режимі посилання на thisзначення null або undefined викликає помилку.
  4. Забороняє дублювання імен властивостей або значень параметрів. Строгий режим видає помилку, коли виявляє повторювану іменовану властивість в об’єкті (наприклад, var object = {foo: "bar", foo: "baz"};) або дублікат іменованого аргументу для функції (наприклад, function foo(val1, val2, val1){}), тим самим виявляючи те, що майже напевно є помилкою у вашому коді, яку ви могли б витратити багато відстеження часу.
  5. Робить eval() безпечнішим. Існують деякі відмінності в eval()поведінці в суворому режимі та в нестрогому режимі. Найголовніше те, що в строгому режимі змінні та функції, оголошені в eval()операторі, не створюються в області видимості. (Вони створюються в області вмісту в несуворому режимі, що також може бути поширеним джерелом проблем із JavaScript.)
  6. Видає помилку при неправильному використанні deleteОператор delete(використовується для видалення властивостей з об’єктів) не можна використовувати для неконфігурованих властивостей об’єкта. Нестрогий код миттєво завершиться помилкою, коли буде зроблена спроба видалити властивість, яка не підлягає конфігурації, тоді як строгий режим у такому випадку викличе помилку.

Усунення проблем з JavaScript за допомогою розумнішого підходу

Як і з будь-якою технологією, чим краще ви розумієте, чому і як JavaScript працює, а як ні, тим надійнішим буде ваш код і тим більше ви зможете ефективно використовувати справжню потужність мови. І навпаки, відсутність належного розуміння парадигм і концепцій JavaScript справді є причиною багатьох проблем JavaScript.

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


0 комментариев
Сортировка:
Добавить комментарий

IT Новости

Смотреть все