Skip to content

Latest commit

 

History

History
475 lines (276 loc) · 75.5 KB

File metadata and controls

475 lines (276 loc) · 75.5 KB

Серія "Ти поки що не знаєш JS". Книга 1: "Перші кроки". Друге видання

Глава 1: Що таке JavaScript?

Ти поки що не знаєш JS. Я теж ще ні. Принаймні, я точно не знаю його досконально, і ніхто з нас не знає. Але всі ми можемо почати пізнавати JS краще.

У першій главі першої книги серії Ти поки що не знаєш JS(далі скорочено YDKJSY) ми витратимо трохи часу на те, щоб закласти підґрунтя для подальшого руху. Треба почати з різноманітних важливих допоміжних знань та з розвіяння деяких міфів та хибних уявлень про те, чим мова являється, а чим — ні.

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

Про цю книгу

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

Після першої довідкової глави в решті книги викладається огляд того, що на вас чекає, якщо ви вирішите вивчати JS із книгами YDKJSY.

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

Майте на увазі, що попри те, що ця книга називається "Перші кроки", вона не призначена для початківців або для використання у якості першого підручника з JS. Основна задача цієї книги - підготувати вас до поглибленого вивчення JS з книгами решти серії; текст книги припускає, що ви вже маєте принаймні кілька місяців досвіду написання коду на JS. Тож, щоб отримати максимальну віддачу від Перших кроків, переконайтеся, що витратили достатньо часу на написання коду на JS та набули досвіду.

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

А що з назвою?

Мабуть, "JavaScript" - це назва, що найчастіше серед мов програмування наводить на хибні думки чи уявлення.

Чи має ця мова відношення до Java? Це скриптова форма Java? Вона тільки для написання сценаріїв, тобто скриптів, а не справжніх програм?

Правда в тому, що назва JavaScript - це артефакт маркетингових хитрощів. Коли Брендан Ейх вперше задумав цю мову, він дав їй кодову назву Mocha("Мока"). Для внутрішніх потреб в Netscape використовували бренд LiveScript, але коли прийшов час публічно назвати мову, переміг "JavaScript".

Чому переміг? Бо мова від початку мала сподобатись аудиторії програмістів, що переважно писали на Java, а слово "скрипт"(тобто, "сценарій") було у той час популярним позначенням легких програм. Так, нехай ці полегшені "сценарії" будуть першими, що поставлять на сторінки нової штуки, яку називають "Всесвітнє павутиння"!

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

Між кодом на JavaScript і кодом на Java є деякі поверхневі подібності. Ці подібності походять не від спільного розвитку мов, а від того, що обидві мови орієнтовані на розробників, що звикли до мов з синтаксисом, подібним до C (а також певною мірою C++).

Наприклад, ми ставимо { на початку блоку коду та } наприкінці, як у C чи C++ та Java. Також ми використовуємо ; для закінчення інструкції.

Певним чином правовідносини між JS та Java сягають навіть глибше, ніж синтаксис. Компанія Oracle (через Sun), яка все ще володіє і керує Java, також (через Netscape) володіє офіційною торговою маркою "JavaScript". Ця торгова марка майже ніколи не застосовується і, напевно, не могла б на цю мить.

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

Щоб сильніше відмежувати мову від торгової марки, що належить Oracle, офіційною назвою мови, визначеної TC39 та формалізованою організацією стандартизації ECMA, є ECMAScript. І справді, з 2016 року офіційна назва мови також доповнюється роком перегляду; на момент написання цього розділу це версія ECMAScript 2019, або скорочено ES2019.

Іншими словами, JavaScript або JS, яка працює у вашому браузері або в Node.js, є однією з реалізацій стандарту ES2019.

ПРИМІТКА:
Не використовуйте такі терміни, як "JS6" або "ES8", для позначення мови. Дехто так робить, але ці терміни лише посилюють плутанину. Слід дотримуватися назви "ES20xx" або просто "JS".

Як би ви не називали цю мову - JavaScript, JS, ECMAScript або ES2019, це точно не варіант мови Java!

"Java is to JavaScript as ham is to hamster." --Jeremy Keith, 2009" - Джеремі Кіт, 2009 Гра слів, яку приблизно можна перекласти так: "Java має таке ж відношення до JavaScript, як осел до оселедця".

Специфікація мови

Я згадував TC39 - технічний координаційний комітет, який керує JS. Його основним завданням є управління офіційною специфікацією мови. Члени комітету регулярно збираються для голосування щодо будь-яких узгоджених змін, які потім подають до ECMA, організації зі стандартизації.

Синтаксис та поведінка JS визначені у специфікації ES.

З моменту створення JS у 1995 році версія ES2019 є 10-ю версією специфікації з мажорним номером, тому в офіційній URL-адресі специфікації на сайті ECMA ви знайдете "10.0":

https://www.ecma-international.org/ecma-262/10.0/

До складу комітету TC39 входить від 50 до 100 осіб з широкого кола компаній, що зацікавлені у розвитку вебу, таких як розробники браузерів (Mozilla, Google, Apple) та виробники пристроїв (Samsung тощо). Всі члени комітету працюють на волонтерських засадах, хоча багато хто з них працює у згаданих вище компаніях, тому за виконання обов'язків в комітеті вони можуть отримувати часткову компенсацію.

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

Усі пропозиції TC39 проходять процес з п'яти етапів. Авжеж відлік, починається з 0, ми ж програмісти! Тож етапи мають номери від 0 до 4. Більше про процес можна прочитати тут: https://tc39.es/process-document/

Якщо пропозиція знаходиться на нульовому етапі розгляду (Stage 0), це приблизно означає, що хтось із TC39 вважає ідею непоганою і планує взяти участь в роботі над нею. Тобто, безліч ідей, які пропонують особи, які не є членами TC39, за допомогою неформальних засобів, таких як соціальні медіа чи публікації в блогах, насправді ще не досягли Етапу 0. Щоб вважати Етап 0 розпочатим, потрібно знайти члена TC39, який погодиться відстоювати вашу пропозицію.

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

Усі пропозиції обговорюються відкрито у репозиторії TC39 на Github: https://github.com/tc39/proposals

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

Попри деякі усталені і прикро увічнені міфи, жодних версій JavaScript не існує. Існує лише одна JS, офіційний стандарт, який підтримується TC39 та ECMA.

На початку 2000-х років, коли Microsoft шляхом зворотної розробки створив власну (і не зовсім сумісну зі специфікацією) версію JS під назвою "JScript", дійсно існували кілька версій JS одночасно. Ті часи давно минули, тому сьогодні заявляти таке про JS неактуально і неточно.

При розробці власних реалізацій JS усі основні браузери та виробники пристроїв зобов'язуються дотримуватися цієї єдиної центральної специфікації. Звичайно, рушії реалізують певні функції в різний час. Але ніколи не повинно бути так, щоб рушій v8 (JS-рушій у Chrome) реалізував певну функцію інакше або несумісно у порівнянні з рушієм SpiderMonkey (JS-рушій Mozilla).

Це означає, що ви можете вивчити один JS і покладатися на свої знання скрізь.

Веб керує усім (усім JS)

Список середовищ, у яких можна запустити JS, постійно розширюється: від браузерів до серверів (Node.js), роботів, лампочок і так далі. Проте єдиним середовищем, яке керує JS, є веб. Іншими словами, має значення лише те, як реалізується JS для веббраузерів.

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

Іноді специфікація JS диктує якусь нову або вдосконалену поведінку, і все ж це не буде точно відповідати тому, як вона працює в браузерних рушіях JS. Така невідповідність є історичною: рушії JS мають брати до уваги понад 20 років видимої поведінки у граничних випадках, на які розраховує веб. Таким чином, іноді рушії JS відмовляються відповідати вимогам специфікації, оскільки це порушить наявний контент вебу.

У цих випадках TC39 часто відступає і погоджується увідповіднити специфікацію до реальності вебу. Наприклад, TC39 планував додати метод contains(..) до масивів, але було виявлено, що це ім'я суперечить старим JS-фреймворкам, які все ще використовуються на деяких сайтах, тому назву змінили на сумісну includes(..). Те ж саме спричинило трагікомічну кризу JS-спільноти, яку назвали "smooshgate", коли запланований метод flatten(..) з часом був перейменований на flat(..).

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

Що робити? Для цього існує Додаток B, "Додаткові функції ECMAScript для веббраузерів".1 Специфікація JS містить цей додаток, щоб деталізувати всі відомі невідповідності між офіційною специфікацією JS та реальною JS у вебі. Іншими словами, це винятки, дозволені лише для веб-JS; інші середовища JS повинні дотримуватися букви закону.

Розділи B.1 та B.2 охоплюють доповнення до JS (синтаксис та програмні інтерфейси), які знову ж таки, з історичних причин, містить вебверсія JS, але які TC39 не планує формально описувати як частину ядра JS. Приклади включають вісімкові літерали з префіксом 0, глобальні утиліти escape(..)/unescape(..), рядкові "помічники", такі як anchor(..) та blink(), і метод RegExp compile(..).

Розділ B.3 містить деякі конфлікти, коли код може працювати як у вебрушіях, так і в рушіях інших середовищ, але поведінка може помітно відрізнятися, що призводить до різних результатів. Більшість перелічених змін стосуються ситуацій, які позначені як ранні помилки у строгому режимі виконання ("strict mode").

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

Не все те JS...

Чи є цей код програмою на JS?

alert("Hello, JS!");

Залежить від вашої точки зору. Функція alert(..) не включена до специфікації JS, але реалізована у всіх вебсередовищах, що підтримують JS. Проте в Додатку B ви її теж не знайдете, то як це розуміти?

Різні JS-середовища (наприклад, JS-рушії браузера, Node.js тощо) додають до глобальної області видимості ваших JS-програм програмні інтерфейси(API), що надають доступ до специфічних для цього середовища можливостей, як-от можливість виводити спливаюче вікно з попередженням в браузері користувача.

Насправді чимало програмних інтерфейсів дуже схожі на JS, наприклад, fetch(..), getCurrentLocation(..) та getUserMedia(..), насправді є веб-API, лише схожими на JS. У Node.js, з іншого боку, ми можемо отримати доступ до сотень методів API з різних вбудованих модулів, наприклад fs.write(..).

Іншим поширеним прикладом є console.log(..) і всі інші методи console.*. Вони не вказані в специфікації JS, але через універсальну корисність визначені майже в кожному JS-середовищі, згідно з приблизно узгодженим консенсусом.

Отже, JS не визначає alert(..) і console.log(..). Але вони схожі на JS. Вони є функціями та об'єктними методами і вони відповідають правилам синтаксису JS. Їхню поведінку контролює середовище, що запустило JS-рушій, але поверхнево вони, безумовно, повинні зважати на JS, щоб мати можливість грати на його полі.

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

Отже, виклик alert(..) — це JS, але сам alert є лише гостем, а не частиною офіційної специфікації JS.

Не завжди JS

Консоль або REPL (Read-Evaluate-Print-Loop, цикл "читання-обчислення-друк") в інструментах розробника (dev tools) вашого браузера або Node на перший погляд здається звичайним JS-середовищем, але насправді це не так.

Інструменти розробника це, власне, інструменти для розробників. Їх головне призначення - полегшити життя розробникам. Вони надають пріоритет приємному розробницькому досвіду (DX, developer experience). Точно і чисто відбивати всі нюанси поведінки JS за специфікацією не є метою таких інструментів. Таким чином, існує безліч особливостей поведінки, які можуть видатися неочікуваними, якщо ви розглядаєте консоль як чисте JS-середовище.

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

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

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

  • Чи створює оголошення з var або function на верхньому рівні так званої глобальної області видимості консолі реальну глобальну змінну, а також віддзеркалену одноіменну властивість на window, і навпаки.

  • Що відбувається з безліччю оголошень let і const у глобальній області видимості верхнього рівня.

  • Коли ви вводите "use strict"; в одному рядку (натискаючи <enter> після), чи вмикається строгий режим для решти сеансу консолі, як це робиться в першому рядку файлу .js, а також чи можна використовувати "use strict"; після "першого рядка" і все одно переходити до строгого режиму для цього сеансу.

  • Як працює у нестрогому режимі прив'язка this за замовчуванням для викликів функцій та чи буде глобальний об'єкт містити очікувані глобальні змінні.

  • Принцип роботи підняття (див. Книгу 2, Області видимості та замикання) у коді на кілька рядків.

  • ...та декілька інших

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

Не думайте, що поведінка, яку ви бачите в консолі розробника, є відображенням точної буквальної семантики JS; для цього прочитайте специфікацію. Натомість думайте про консоль як про середовище, що просто вміє виконувати JS. Це саме по собі корисно.

Мова з багатьма обличчями

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

Але незалежно від індивідуального стилю програми, парадигму майже завжди можна визначити з першого погляду.

Зазвичай на рівні парадигми код розподіляють на процедурний, об'єктно-орієнтований (або класово-орієнтований) та функційний:

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

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

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

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

Деякі мови сильно схильні до певної парадигми. C є процедурною мовою, Java / C ++ майже повністю орієнтована на класи, а Haskell - на функційне програмування.

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

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

Пряма та зворотна сумісність

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

Давайте розберемося.

Зворотна сумісність означає, що як тільки щось визнається валідним JS-кодом, в мову більше не може бути внесено таких змін, що зроблять цей код невалідним. Код, написаний у 1995 році, яким би примітивним чи обмеженим він не був, має працювати і сьогодні. Як часто заявляють члени TC39, "не можна ламати веб!".

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

Така "гарантія" - зовсім не дрібниця. Підтримка зворотної сумісності, розтягнута на майже 25 років історії мови, створює величезний тягар та ставить цілу низку унікальних задач. У світі обчислювальних технологій буде важко знайти багато інших прикладів такої відданості ідеї зворотної сумісності.

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

З цього правила є кілька невеликих винятків. У JS відбулися деякі зміни, що порушили зворотну сумісність, але TC39 робить такі зміни дуже обережно. Комітет оцінює негативний вплив такої зміни за допомогою даних, зібраних браузерами, а браузери в кінцевому підсумку приймають рішення і голосують, чи готові взяти на себе негатив від користувачів через дуже дрібну поломку, зважений на користь виправлення або вдосконалення деяких аспектів мови для багатьох інших сайтів і користувачів.

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

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

HTML і CSS, навпаки, дотримуються прямої сумісності, але не зворотної. Якщо ви натрапили на HTML або CSS, написані ще в 1995 році, цілком можливо, що сьогодні цей код геть не працюватиме або працюватиме не належним чином. Але якщо ви використовуєте нову функцію, додану у 2019 році, в браузері 2010 року, сторінка не зламається - нерозпізнаний CSS / HTML пропускається, тоді як решта CSS / HTML буде оброблена коректно.

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

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

Попри те, що JS не є і не може бути прямо-сумісною мовою, важливо визнати зворотну сумісність JS, включаючи стійкі переваги для Всесвітньої мережі та обмеження та труднощі, які вона спричиняє для JS.

Стрибки через прогалини

Оскільки JS не дотримується прямої сумісності, завжди існує можливість розриву між валідним JS-кодом, і найстарішим рушієм, який ваш вебсайт або програма має підтримувати. Якщо ви запускаєте програму, яка використовує можливості ES2019, на рушії 2016 року, швидше за все ваша програма зламається.

Якщо йдеться про новий синтаксис, програма, як правило, геть не зможе скомпілюватися та запуститися, через синтаксичну помилку. Якщо функція є програмним інтерфейсом (наприклад, Object.is(..) з ES6), програма може запуститись до певної точки, але потім викинути виняток виконання та зупинитися, як тільки зустріне посилання на невідомий інтерфейс.

Чи означає це, що JS-розробники приречені завжди відставати від темпів прогресу, використовуючи лише той код, який зрозумілий найстарішим JS-рушіям тих середовищ, які вони повинні підтримувати? Авжеж ні!

Натомість це означає, що JS-розробники повинні докладати особливих зусиль, щоб заповнити цю прогалину.

Розв'язанням проблеми нового та несумісного синтаксису є транспіляція. Транспіляція - це винайдений спільнотою термін для опису використання інструменту для перетворення вихідного коду програми з однієї форми на іншу (але все ще на текстовий вихідний код). Як правило, проблеми прямої сумісності, пов'язані з синтаксисом, вирішуються за допомогою транспілятора (найпоширенішим є Babel (https://babeljs.io)), що перетворює синтаксис цієї нової версії на еквівалентний старий синтаксис.

Наприклад, розробник може написати такий фрагмент коду:

if (something) {
    let x = 3;
    console.log(x);
}
else {
    let x = 4;
    console.log(x);
}

Ось як виглядатиме код у дереві вихідного коду для цієї програми. Але під час створення файлу чи файлів для запуску на загальнодоступному вебсайті транспілятор Babel може перетворити код таким чином:

var x$0, x$1;
if (something) {
    x$0 = 3;
    console.log(x$0);
}
else {
    x$1 = 4;
    console.log(x$1);
}

Оригінальний фрагмент використовував let для створення блокової змінної x в обох гілках конструкції if, тобто як у if, так і у else, які не заважали одна одній. Еквівалентна програма (з мінімальним перероблюванням), яку Babel може створити, просто вирішує назвати дві різні змінні унікальними іменами, створюючи однаковий результат, коли імена не співпадають.

ПРИМІТКА:
Ключове слово let було додано до мови разом з ES6 (у 2015 році). Попередній приклад транспіляції мав би застосовуватися лише в тому випадку, коли застосунку потрібно працювати в середовищі JS, що підтримує версію, що передує ES6. Приклад наведено лише для простоти ілюстрації. Коли стандарт ES6 тільки вийшов, потреба в такій транспіляції була досить поширеною, але в 2020 році набагато рідше виникає необхідність підтримувати середовища, що не підтримують ES6. Таким чином, визначення "цільової мови", яка використовується для транспіляції, є розсувним вікном, яке зміщується вгору лише тоді, коли приймаються рішення припинити підтримку якогось старого браузера або рушія.

Ви можете поставити собі питання: "Навіщо взагалі ускладнювати собі життя використанням інструменту для перетворення нової версії синтаксису на стару? Чи не могли б ми просто написати дві змінні, використовуючи ключове слово let?" Причина полягає в тому, що розробникам наполегливо рекомендується використовувати найновішу версію JS, щоб їх код був чистим та найбільш ефективно передавав свою ідею.

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

Заповнення прогалин

Якщо проблема прямої сумісності пов'язана не з новим синтаксисом, а радше з відсутнім методом програмного інтерфейсу (API), який був доданий нещодавно, найпоширенішим розв'язанням є надання визначення для цього відсутнього методу, що діє так, ніби старе середовище його визначає. Цей шаблон називається "поліфіл" (polyfil), він же "шім" (shim).

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

// getSomeRecords() повертає проміс
// даних, по які звертається
var pr = getSomeRecords();

// показати візуальний індикатор(спінер) на час завантаження даних
startSpinner();

pr
.then(renderRecords)   // відрендерити, якщо дані отримано успішно
.catch(showError)      // інакше показати помилку
.finally(hideSpinner)  // у будь-якому разі сховати спінер

Цей код використовує метод finally(..) на прототипі промісу, що був доданий у ES2019. Якби цей код запустили у середовищі до ES2019, метод finally(..) не існував би, і сталася б помилка.

Поліфіл для finally(..) в середовищах до ES2019 може виглядати так:

if (!Promise.prototype.finally) {
    Promise.prototype.finally = function f(fn){
        return this.then(
            function t(v){
                return Promise.resolve( fn() )
                    .then(function t(){
                        return v;
                    });
            },
            function c(e){
                return Promise.resolve( fn() )
                    .then(function t(){
                        throw e;
                    });
            }
        );
    };
}
ПОПЕРЕДЖЕННЯ:
Код поліфіла finally(..) наведений як ілюстрація. Це спрощена та не повністю сумісна зі специфікацією версія. Не використовуйте цей поліфіл у своєму коді; за можливістю завжди використовуйте надійний, офіційний поліфіл, наприклад, з колекції поліфілів та шимів ES-Shim.

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

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

Завжди пишіть код з використанням тих можливостей мови, що найкраще передають ваші думки та наміри. В загальному випадку це означає використання останньої стабільної версії JS. Уникайте негативного впливу на читабельність коду через спроби вручну компенсувати відсутній синтаксис або програмні інтерфейси, яких бракує. Інструменти існують саме для цього!

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

А що там з інтерпретацією?

Довго обговорюване питання щодо коду, написаного на JS: це інтерпретований скрипт чи скомпільована програма? Здається, думка більшості полягає в тому, що JS - це інтерпретована (скриптова) мова. Але правда складніша.

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

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

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

Раніше код на скриптових або інтерпретованих мовах виконувався найчастіше зверху вниз рядок за рядком; як правило, не було початкового проходу через програму для її обробки до початку виконання (див. рисунок 1).

Інтерпретація скрипта для подальшого виконання

Рис. 1: Виконання коду шляхом інтерпретації

У скриптових або інтерпретованих мовах помилка у п'ятому рядку програми не буде виявлена, поки не будуть виконані рядки з 1 по 4. Примітно, що помилка в рядку 5 може бути пов’язана з умовою часу виконання, наприклад, якоюсь змінною чи значенням, що має невідповідне значення для операції, або через неправильний вираз чи команду на цьому рядку. Залежно від контексту перенесення обробки помилок на рядок, на якому виникає помилка, може бути бажаним або небажаним ефектом.

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

Парсинг, компіляція та виконання програми

Рис. 2: Парсинг + компіляція + виконання

У цій моделі обробки помилок помилкова команда (наприклад, команда з синтаксичною помилкою) у рядку 5 буде перехоплена на етапі парсингу, перш ніж розпочнеться будь-яке виконання, і програма не запуститься. Для пошуку синтаксичних (так званих "статичних") помилок, як правило, бажано знати про них перед будь-яким частковим виконанням.

Тож що спільного у мов, що парсяться, та «компільованих» мов? По-перше, всі компільовані мови парсяться. Отже, парсинг - це вже половина роботи з компіляції. У класичній теорії компіляції останнім кроком, що залишився після парсингу, є генерація коду, тобто створення виконуваної форми.

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

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

Вихідний код JS парситься перед виконанням. Специфікація вимагає цього, оскільки вона вимагає повідомляти про "ранні помилки" - статично визначені помилки в коді, такі як подвоєння імені параметра, перед початком виконання коду. Ці помилки неможливо розпізнати без парсингу коду.

Отже JS підлягає парсингу, але чи він також підлягає компіляції?

Скоріше так, ніж ні. Розпарсений JS перетворюється на оптимізовану (двійкову) форму, і цей "код" згодом виконується (рис. 2); зазвичай після виконання усієї важкої роботи з синтаксичного аналізу рушій не переходить назад у режим послідовного виконання (як на малюнку 1). Більшість мов / рушіїв цього не робить, оскільки це було б дуже неефективно.

Точніше кажучи, ця "компіляція" створює свого роду двійковий байт-код, який потім передається на виконання віртуальній машині JS. Деякі люблять стверджувати, що ця ВМ "інтерпретує" байт-код. Але тоді це означає, що Java, а також десяток інших мов на базі JVM інтерпретуються, а не компілюються. Звичайно, це суперечить розповсюдженому твердженню про те, що Java та подібні їй - це компільовані мови.

Цікаво, що хоча Java та JavaScript - це дуже різні мови, питання інтерпретації чи компіляції між ними досить тісно пов’язане!

Інша складність полягає в тому, що рушії JS можуть виконувати декілька проходів з обробки / оптимізації JIT (Just-In-Time) згенерованого коду (після синтаксичного аналізу), які залежно від точки зору також можна обґрунтовано позначити або "компіляцією", або "інтерпретацією". Насправді те, що відбувається під капотом рушія JS, фантастично складна річ.

То до чого зводяться ці дрібні деталі? Зробіть крок назад і розгляньте процес, який проходить вихідний код програми на JS:

  1. Після того, як програма покидає редактор розробника, її транспілює Babel, потім збирає Webpack (і, можливо, ще пів дюжини інших процесів збірки), а потім в зовсім іншій формі вона потрапляє до рушія JS.

  2. Рушій JS парсить код у абстрактне синтаксичне дерево(AST).

  3. Потім рушій перетворює AST на своєрідний байт-код, двійкове проміжне представлення (IR), яке потім уточнюється / трансформується ще більше оптимізуючим компілятором JIT.

  4. Нарешті, віртуальна машина JS виконує програму.

Візуалізуємо ці кроки:

Кроки з компіляції та виконання JS

Fig. 3: Парсинг, компіляція та виконання JS

Чи обробляється JS більше як інтерпретований скрипт рядок за рядком, як на малюнку 1, або він обробляється більше як компільована мова, яка обробляється повністю в один чи кілька проходів перед виконанням (як на малюнках 2 та 3) ?

Я думаю, зрозуміло, що принаймні за духом, JS - це компільована мова.

І знову ж таки, важливо згадати, що, оскільки JS компілюється, ми отримуємо інформацію про статичні помилки (наприклад, неправильний синтаксис) перед тим, як виконати наш код. Це істотно інша модель взаємодії, ніж та, що ми спостерігаємо в традиційних скриптових програмах, і, можливо, така модель корисніша.

Web Assembly (WASM)

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

У 2013 році інженери з Mozilla Firefox продемонстрували порт ігрового рушія Unreal 3 з C на JS. Запуск цього коду на рушії JS у браузері з повною швидкодією 60 кадрів за секунду став можливим завдяки набору оптимізацій, які JS-рушій міг виконати, оскільки JS-версія рушія Unreal використовувала такий стиль коду, який надавав перевагу підмножині JS, названа "ASM.js".

Ця підмножина є валідним JS, написаним дещо незвичним чином, який передає певну важливу інформацію про типи рушієві, що дозволяє йому робити суттєві оптимізації. ASM.js було введено як один із способів подолання проблеми швидкодії JS.

Важливо зауважити, що ASM.js ніколи не замислювався як код, що мають писати розробники, а скоріше формою подання програми, яка була перекладена з іншої мови (наприклад, C), де необхідні "анотації" вставлялися автоматично інструментами.

Через кілька років після того, як ASM.js продемонстрував валідність програм, створених інструментами, які можуть бути оброблені JS-рушієм більш ефективно, інша група інженерів (також з Mozilla) випустила Web Assembly (WASM).

WASM схожий на ASM.js тим, що його початковий намір було надати можливість програмам, написаним на інших мовах, ніж JS (C тощо), бути перетвореними у форму, яка може працювати в рушії JS. Але на відміну від ASM.js, WASM вирішив додатково обійти деякі відомі затримки при синтаксичному аналізі / компіляції JS через представлення програми у формі, що абсолютно не схожа на JS.

WASM - це формат представлення, більш схожий на Assembly (звідти його назва), який може оброблятися рушієм JS, пропускаючи етап синтаксичного аналізу / компіляції, який зазвичай робить рушій JS. Синтаксичний аналіз / компіляція програми, орієнтованої на WASM, відбувається заздалегідь (AOT, ahead-of-time); розповсюджується програма в двійковому пакеті, готова для роботи рушія JS з мінімальною обробкою.

Первинною мотивацією для WASM було явно потенційне покращення швидкодії. Попри те, що швидкодія залишається в центрі уваги, WASM додатково мотивоване бажанням привнести більше паритету для мов, інших ніж JS, на вебплатформу. Наприклад, якщо така мова, як Go, підтримує багатопотокове програмування, а JS як мова - ні, то WASM пропонує можливість перетворення програми на Go у форму, яку може зрозуміти рушій JS, без необхідності реалізації потоків у JS.

Іншими словами, WASM знімає тиск на додавання до JS функцій, які здебільшого / виключно призначені для використання у програмах з інших мов. Це означає, що про розвиток функцій JS можна судити (за допомогою TC39), не збиваючись на інтереси / вимоги в інших мовних екосистемах, при цьому дозволяючи цим мовам мати життєздатний шлях до Інтернету.

Інший погляд на WASM навіть не пов’язаний безпосередньо з Інтернетом (тобто, літерою W у назві WASM). WASM перетворюється на своєрідну міжплатформенну віртуальну машину (VM), де програми можуть бути скомпільовані один раз і запущені в безлічі різних системних середовищ.

Отже, WASM не тільки для Інтернету, також WASM не є JS. За іронією долі, навіть попри на те, що WASM працює на рушії JS, мова JS є однією з найменш придатних мов для джерел програм WASM, оскільки WASM значною мірою покладається на статичну типізацію. Навіть TypeScript (TS), тобто, статично типізований JS, не зовсім придатний (як зараз) для транспіляції до WASM, хоча мовні варіанти, такі як AssemblyScript, намагаються подолати розрив між JS / TS і WASM.

Ця книга не про WASM, тому я не буду витрачати більше часу на її обговорення, окрім однієї останньої думки. Деякі люди припускають, що WASM вказує на майбутнє, де JS вирізається або зводиться до використання тільки у вебі. Ці люди часто ставляться до JS негативно і хочуть замінити його якоюсь іншою мовою - будь-якою іншою! Оскільки WASM дозволяє іншими мовами працювати на рушії JS, на перший погляд, це не така вже і фантастика.

Але дозвольте просто сказати: WASM не замінить JS. WASM значно збільшує те, що може зробити у вебі (включаючи розширення можливостей JS). Це чудова річ, цілком ортогональна тому, чи будуть деякі люди використовувати це як можливість не писати на JS.

Строго кажучи

Ще в 2009 році, коли вийшов ES5, JS додав опцію строгий режим для заохочення покращення коду програм на JS.

Переваги строгого режиму значно перевищують недоліки, але старі звички повільно вмирають, і інерцію існуючих (інакше кажучи, "застарілих") кодових баз насправді важко змінити. На жаль, більш ніж через 10 років опціональність строгого режиму означає, що він все ще не обов'язковим для програмістів JS.

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

Найбільш строгі режими керування мають форму ранніх помилок, тобто помилки, які не є суто синтаксичними помилками, але все одно видаються під час компіляції (до запуску коду). Наприклад, строгий режим забороняє однакові імена двох параметрів функції і призводить до ранньої помилки. Деякі інші відмінності строгого режиму можна спостерігати лише під час виконання, наприклад, як this за замовчуванням має значення undefined замість глобального об'єкта.

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

Строгий режим вмикається для кожного окремого файлу спеціальною прагмою (перед нею не допускається нічого, крім коментарів / пробілів):

// тут можуть бути лише коментарі чи пробіли
"use strict";
// решта файлу виконується у строгому режимі
ЗАСТЕРЕЖЕННЯ:
Слід пам’ятати, що навіть випадкові крапка з комою перед прагмою "use strict" зроблять прагму недійсною; помилок не виникає, оскільки JS дозволяє строковий літеральний вираз у позиції оператора, але строгий режим не буде увімкнено без жодних повідомлень!

Строгий режим також можна ввімкнути для кожної функції з тими самими правилами щодо оточення:

function someOperations() {
    // тут лише пробіли та коментарі
    "use strict";

    // увесь код тут виконується у строгому режимі
}

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

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

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

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

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

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

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

Визначення

JS - це реалізація стандарту ECMAScript (у версії ES2019 на момент написання книги), якою керує комітет TC39 та приймається ECMA. Вона працює у браузерах та інших середовищах JS, таких як Node.js.

JS - мультипарадигмальна мова, тобто синтаксис та можливості дозволяють розробнику поєднувати концепції з різних базових парадигм, таких як процедурна, об’єктно-орієнтована (OO/класи) та функційна (FP), а також відхилятися від них та модифікувати за потребою та переробляти.

JS - це компільована мова, тобто інструменти (включаючи рушій JS) обробляють і перевіряють програму та повідомляють про будь-які помилки до її запуску.

Нарешті ми визначилися з тим, що таке мова JS. Тож давайте почнемо знайомитися з її тонкощами.

Footnotes

  1. ECMAScript 2019 Language Specification, Appendix B: Additional ECMAScript Features for Web Browsers, https://www.ecma-international.org/ecma-262/10.0/#sec-additional-ecmascript-features-for-web-browsers (latest as of time of this writing in January 2020)