Чому JavaScript відстійний?

21 хв. читання

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

Представлення мови

JavaScript це динамічно типізована мова програмування заснована на прототипах, яка слідує стандартуECMA, а отже підтримує імперативну, об'єктно–орієнтовану і функціональну парадигми програмування. Я не є великим фанатом мов без строгої типізації, але я розумію як доцільність використання таких мов в певних ситуаціях так і їхні сильні сторони в програмах певного типу. Звісно якщо вони все не псують.

Java в JavaScript

Читач може припустити що JavaScript - це якась дивна версія Java створена спеціально для виконання в середовищі браузера. Що ж, назви і справді схожі, але на цьому схожість закінчується. Це дві різні мови програмування які поділяють кілька ключових слів які, швидше за все, походять від їхнього спільного предка — C.

JavaScript пережив декілька змін назви. Спершу розробка велась під назвою Mocha, потім назва була змінена на LiveScript і нарешті мову було похрещено як JavaScript. Це доволі тупа і оманлива назва, яка спричинила деяку плутанину, але тоді це був винахідливий маркетинговий хід який, імовірно, дуже допоміг JavaScript стрімко набути популярності.

Система типів

Згідно специфікації ECMA мова має шість вбудованих типів даних:

  • Undefined
  • Null
  • Boolean
  • String
  • Number
  • Object.

IEEE 754 — поганий вибір

Що справді мене турбує так це те, що вони вирішили використовувати стандарт IEEE 754 з подвійною точністю представлення чисел. Це означає, що все, що ви отримаєте це один єдиний тип Number, який підтримує тільки 64 бітні числа з плаваючою комою з подвійною точністю. JavaScript не має рідних цілих чисел. Проблема з представленням цілих чисел за допомогою змінних з плаваючою комою полягає в тому, що вони втрачають точність як тільки значення стає занадто великим. Наприклад, результат виконання порівняння 9999999999999999 == 10000000000000000 виводить true. Це правильно в контексті стандарту IEEE 754 (і тому typeof NaN → 'number') хоча така поведінка може призвести до помилок, якщо програмістам не відомо про цей факт. Немає зміни типу або схожого механізму для того, щоб уникати подібної проблеми автоматично.

Особливості

JavaScript не завжди поводить себе так, як ви могли б очікувати. Моєму столу на роботі довелося витримувати удар за ударом поки я був в агонії намагаючись підлагодити JavaScript код який взаємодіяв з внутрішньою SAP системою компанії. Я помітив що більшість вад в програмі були спричинені крихітними помилками, яких накопичувалося все більше і більше аж до того моменту, коли програма зламалася. Я виявив, що доволі важко знайти помилки на ранній стадії, коли вони трапляються. Я віддаю перевагу такій поведінці при якій система завершує свою роботу при найменшій помилці (скажімо через створення винятку відразу після того як сталося щось неочікуване). Оскільки JavaScript динамічна і доволі гнучка мова програмування, вона просто любить ігнорувати такі речі і продовжувати роботу далі, намагаючись зрозуміти те, що залишилося. На даний момент я просто вдячний, що я не мушу проводити більшу частину мого часу на роботі програмуючи на JavaScript.

Агресивне зведення типів

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

11.9.3 Абстрактний алгоритм порівняння на рівність

Операція порівняння x == y, де x і y є певними значеннями, призводить в результаті до true або false. Таке порівняння виконується наступним чином:

  1. Якщо обидва значення мають однаковий тип, то
    1. Якщо Type(x) - це Undefined, повернути true.
    2. Якщо Type(x) - це Null, повернути true.
    3. Якщо Type(x) - це Number, то
      1. Якщо Type(x) є NaN, повернути false.
      2. Якщо Type(y) є NaN, повернути false.
      3. Якщо x має таке ж значення типу Number що і y, повернути true.
      4. Якщо x +0 і y −0, повернути true.
      5. Якщо x −0 і y +0, повернути true.
      6. Повернути false.
    4. Якщо Type(x) - це String, то повернути true якщо x і y є однаковою послідовністю символів (одна й та ж довжина і однакові символи на відповідних позиціях). Інакше, повернути false.
    5. Якщо Type(x) - це Boolean, повернути true якщо x і y обоє true або обоє false. Інакше, повернути false.
    6. Повернути true якщо x і y посилаються на один і то й же об'єкт. Інакше, повернути false.
  2. Якщо x це null і y це undefined, повернути true.
  3. Якщо x це undefined і y це null, повернути true.
  4. Якщо Type(x) - це Number і Type(y) це String, повернути результат порівняння x == ToNumber(y).
  5. Якщо Type(x) - це String і Type(y) це Number, повернути результат порівняння ToNumber(x) == y.
  6. Якщо Type(x) - це Boolean, повернути результат порівняння ToNumber(x) == y.
  7. Якщо Type(y) - це Boolean, повернути результат порівняння x == ToNumber(y).
  8. Якщо Type(x) - це або String або Number і Type(y) це Object, повернути результат порівняння x == ToPrimitive(y).
  9. Якщо Type(x) - це Object і Type(y) це або String або Number, повернути результат порівняння ToPrimitive(x) == y.
  10. Повернути false.

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

    true == 1             → true
    true == "1"           → true
    false == 0            → true
    false == "0"          → true

    false == undefined    → false
    false == null         → false

    null == undefined     → true

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

    "\	\
\
" == 0                 → true
    "\	\
\
 16 \	\
\
" == 16      → true
    "\	\
\
 16 \	\
\
" == "16"    → false

Принаймні з JavaScript ви не маєте хвилюватися про неявне перетворення, якщо рядок містить шіснадцяткові числа або вісімкове представлення чисел і операція порівняння не буде просто припинена, якщо операнд є некоректно сформованим числом (так, я дивлюся на тебе PHP). PHP перед порівнянням пробує конвертувати рядок в число тож "0xFF" == "255" в PHP буде true. JavaScript же ж такого не робить, якщо обидва операнда - це рядки. PHP також припиняє подальший розбір операндів, але все одно проводить операцію порівняння, якщо трапляються недопустимі символи: результатом "0xFFX" == 255 буде true (в JavaScript операція порівняння таких значень видасть false). Але в JavaScript є функція parseInt яка зустрівши недопустимі символи припинить подальший розбір параметра і поверне те, що в неї вийшло (parseInt("1234XXX") == 1234) навіть якщо рядок явно не є числом. В таких випадках функція–конструктор Number коректно поверне NaN (Number("1234XXX")).

Так, що ж станеться коли ми введемо масиви в гру? Ну [] == [] (масив == масив) звісно ж повертає false. Гаразд. Можливо воно розглядає їх як індивідуальні логічні об'єкти. Це нормально. Втім, що станеться якщо ми спробуємо [] == ![] (масив == не масив)? Результатом буде true. Що? O.o

Якщо ви почнете порівнювати не порожні масиви, дивна поведінка не припиняється:

    16 == [16]        → true
    16 == [1,6]       → false
    "1,6" == [1,6]    → true

Що тут відбувається, так це те, що JavaScript спершу перетворює масив в рядок і потім в число. Друга інструкція видасть в результаті false бо \[1,6\] насправді перетворюється в "1,6" що і показує третя інструкція.

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

    var a = "1"
    var b = 2
    var c = a + b    → c = "12"

Змінна b буде приведена до рядка і потім додана до змінної a, що і дає нам рядок "12". Для того щоб запобігти цьому (або скоріше змусити робити те що ви хочете) ви можете явно анотувати тип:

    var a = "1"
    var b = 2
    var c = +a + b    → c = 3 // Зауважте унарний плюс (+a).

Ви маєте можливість здійснити числове (з плаваючою крапкою) порівняння примусово з +a == +b, цілочислове порівняння з a|0 == b|0, рядкове порівняння з "" + a == "" + b і булеве з !a == !b. Це доволі тупо, але, агов, принаймні це працює. :\\

Я дуже раджу використовувати оператор ідентичності (===) і його протилежність (!==) де це тільки можливо. Додаткова перевірка типу може допомогти і допоможе уникнути неприємних помилок, які потім може бути дуже важко знайти.

Фантастичний побічний ефект

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

alert((![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]);

Я навіть не серджуся. Це просто фантастика! :) Погляньте на сайт JSF, який присвячений генерації цих інструкцій.

Глобальні змінні

Так само як і багато інших мов програмування, JavaScript підтримує глобальні змінні. Проблема полягає в тому, що він дуже від них залежить роблячи так, що робота з ними стає дуже надокучливою. Ви не можете уникнути їх, ви справді змушені працювати з ними. Не існує обхідного шляху. Оскільки вони доступні в кожній області видимості вашої програми, ви ніколи не дізнається були вони змінені чи ні, можливо навіть сторонніми бібліотеками, які використовують ті ж самі назви змінних що й ви. Глобальні змінні можуть бути корисними для невеликих сценаріїв, але зазвичай вони серйозно збільшують складність ваших програм. В середовищі браузера всі глобальні змінні додаються до глобального об'єкту window.

Ще більше страждання додає те що не оголошені змінні будуть неявно додані в глобальну область видимості. Це легко може статися, якщо ви випадково зробите помилку при написанні назви змінної таким чином створивши глобальну змінну, яку ви не збиралися створювати.

    function foobar() {
        var bar = 10;
        //
        // [...Code...]
        //
        baz = 20; // Помилка в імені змінної. Тепер у вас є нова глобальна змінна baz
    }

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

    var calculateTopTenCosts = function() {
        var sum = 0;

        for(top = 0; top < 10; top++) {
            sum += getRankCosts(top);
        }

        return sum;
    }

На щастя цю помилку було доволі легко знайти оскільки функція завжди повертала 0. Отже, що з нею не так? Змінна–лічильник top не була правильно оголошена з використанням ключового слова var. Через JavaScript'товську неявну глобалізацію JavaScript створить змінну в глобальній області видимості. В даному випадку він цього зробити не може тому що top вже існує і є глобальною змінною — window.top. window.top — це посилання на найвище вікно в ієрархії фреймів. Цикл for просто буде пропущений оскільки умова top < 10 завжди буде дорівнювати false через той факт, що змінна top - це об'єкт, а не число.

Якби змінна називалася якось по іншому це могло б працювати коректно доти, доки хтось не ввів би нову глобальну змінну ламаючи існуючий код без будь–яких змін алгоритму.

Автоматичне додавання крапки з комою

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

    function foo() {
        return // в цьому місці аналізатор додасть ;
        // Таким чином виконання функції буде завершено і вона поверне undefined
            {
                bar : "test"
            };
    }

Мета цієї функції повернути об'єкт, який має властивість bar зі значенням "test". Сумно, але написана таким чином функція видасть невірний результат, бо аналізатор помилково додасть позначку завершення інструкції не в те місце, таким чином не даючи функції зробити те, для чого вона була створена. Просто ще одна річ з багатьох інших, про яку потрібно пам'ятати під час розробки.

Відсутність належної області видимості

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

    function foobar(a) {
        if (a === 0) {
            var c = 20;
        }
        return c; // c все ще видима
    }

Відсутність хорошого способу структурувати великі програми це справді погано. Звісно ви можете (і повинні!) користуватися об'єктами і функціями для об'єднання функціональності і даних. Також можливо використовувати ці концепції для заміни просторів імен хоча це скоріше дратуючий і багатослівний спосіб програмування.

Інші джерела роздратування і неприємностей

Ось декілька додаткових речей за якими потрібно стежити і які я не хочу додавати в інші розділи статті.

Неочікувана поведінка

Деякі вбудовані функції або функції надані сторонніми бібліотеками можуть видавати несподівані результати. Наприклад, стандартна поведінка Array.sort() - це відсортовувати в алфавітному порядку навіть якщо масив містить тільки числа:

[5, 12, 9, 2, 18, 1, 25].sort(); → [1, 12, 18, 2, 25, 5, 9]

Для того щоб правильно це відсортувати ви маєте написати щось таке:

[5, 12, 9, 2, 18, 1, 25].sort(function(a, b){ return a - b; });

Звісно на такі випадки і існує документація, але не завадить згадати про це й тут.

Літерали/об'єкти

Пам'ятайте, що літерали (числа, рядки, булеві значення, null і undefined) насправді не є примірниками відповідних функцій–обгорток:

    var a = "foobar"
    var b = typeof a               → 'string'
    var c = a instanceof String    → false

Тільки об'єкти створені конструкторами (наприклад new String("foobar")) призведуть до очікуваних результатів. Втім ці обгортки не дуже корисні і, можливо, в найкращому випадку все, що вони роблять - це збивають з пантелику і є непотрібними.

Числові межі

Інша пастка про яку потрібно пам'ятати:

-1 < Number.MIN\\_VALUE → true

Якого біса?! Ну це дійсно тупо. Насправді мінімально можливе число міститься в -Number.MAX_VALUE. O\_o Number.MIN_VALUE — це найменше додатне число яке може бути представлено системою. Це просто невдалий вибір назви.

JSLint

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

Мрія

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

Браузери розмовляють тільки на JavaScript, тож ми вимушені використовувати цю жахливу мову. Але чи справді ми мусимо писати на JavaScript? Може ми просто не так на це дивимося? Може ми маємо думати про JavaScript не як про мову програмування, а скоріше як про середовище або як про кінцевий результат компіляції. JavaScript в своїй основі доволі простий тож він може бути ефективно оптимізований JIT компіляторами.

Існує незліченна кількість інструментів і компіляторів розроблених спільнотою web розробників.

Я вірю що JavaScript відійде на задній план в найближчому майбутньому. Можливо настільки що він навіть не буде нас турбувати.JavaScript буде працювати під капотом і буде прирівнюватися до JVM, роблячи Web справді незалежним від мови програмування.

Слідкуйте за asm.js, він може всіх нас врятувати! ;)

Висновки

JavaScript може бути жахливою мовою програмування, але він має великий потенціал як середовище. Douglas Crockford зробив коротке і точне зауваження в своїй книзі JavaScript: The Good Parts (яку я дуже раджу):

JavaScript побудовано на кількох дуже хороших ідеях і декількох дуже поганих.

Ця цитата доволі добре все підсумовує. JavaScript є те чим він є. Ще декілька років розробки і підходящі умови і я гадаю що гарні частини засяють. Такі визначні бібліотеки і фреймворки як jQuery роблять JavaScript набагато більш стерпним (ну справді, хто нині не використовує jQuery?). Прийшов час в який ми почали опановувати JavaScript як потужну платформу яка по суті доступна на будь–якому пристрої. Носима електроніка, смартфони, планшетні ПК, портативні комп'ютери, настільні комп'ютери, навіть кластери серверів — на всьому цьому буде JavaScript.

JavaScript це страхітлива мова з численними вадами яка попри це має світле майбутнє.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

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

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

Вхід / Реєстрація