Огляд мови JavaScript

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

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

Типи даних

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

  • Number: використовується для всіх числових значень (цілих та дробових), окрім дуже великих цілих чисел.
  • BigInt: використовується для великих цілих чисел довільної довжини.
  • String: використовується для зберігання тексту
  • Boolean: true і false — зазвичай використовуються для умовної логіки.
  • Symbol: використовується для створення унікальних ідентифікаторів, котрі не конфліктують одне з одним.
  • Undefined: позначає, що змінній не було присвоєння значення.
  • Null: позначає свідоме незначення.

Все решта відомо як Object. Серед загальновживаних типів об'єктів:

Функції в JavaScript не є особливими структурами даних – вони лишень особливий тип об'єктів, котрий можна викликати.

Числа

JavaScript має два вбудовані числові типи: Number і BigInt.

Тип Number є 64-бітовим значенням подвійної точності з рухомою комою IEEE 754, а отже – цілі числа можуть бути безпечно представлені в діапазоні від -(253 − 1) до 253 − 1 – без утрати точності, а числа з рухомою комою можуть зберігатися аж до 1.79 × 10308. JavaScript не розрізняє серед чисел дробові й цілі.

console.log(3 / 2); // 1.5, а не 1

Тож нібито ціле число фактично є неявним дробом. У зв'язку з кодуванням IEEE 754 арифметика чисел з рухомою комою іноді може бути неточною.

console.log(0.1 + 0.2); // 0.30000000000000004

Для операцій, що очікують на цілі числа, як то побітові операції, число перетворюється на 32-бітове ціле.

Числові літерали також можуть мати префікси для індикації основи числення (двійкові, вісімкові, десяткові чи шістнадцяткові), а також суфікс експоненти.

console.log(0b111110111); // 503
console.log(0o767); // 503
console.log(0x1f7); // 503
console.log(5.03e2); // 503

Тип BigInt є цілим числом довільної довжини. Його поведінка подібна до поведінки цілих чисел мови C (наприклад, ділення обрізає результат в напрямку до нуля), окрім того, що таке число може зростати нескінченно. BigInt задається у вигляді числового літерала з суфіксом n.

console.log(-3n / 2n); // -1n

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

Об'єкт Math надає стандартні математичні функції та сталі.

Math.sin(3.5);
const circumference = 2 * Math.PI * r;

Є три способи перетворити рядок на число:

  • parseInt() тобто розбір рядка як цілого числа.
  • parseFloat(), тобто розбір рядка як дробового числа.
  • Функція Number(), котра розбирає рядок так, ніби він є числовим літералом, і підтримує чимало різних представлень чисел.

Також як скорочення Number() можна використовувати унарний плюс +.

Серед числових значень також є NaN (скорочення від "Not a Number" – "не число") та Infinity. Чимало "недійсних математичних" операцій поверне NaN — наприклад, якщо спробувати розібрати нечисловий рядок, або використання Math.log() на від'ємному числі. Ділення на нуль призведе до Infinity (зі знаком плюса чи мінуса).

NaN є заразним значенням: якщо воно є операндом будь-якої математичної операції, то результат такої операції також буде NaN. Крім цього, NaN є єдиним значенням у JavaScript, котре не дорівнює саме собі (згідно зі специфікацією IEEE 754).

Рядки

Рядки в JavaScript є послідовностями символів Unicode. Це повинно бути радісною новиною для всіх, кому доводилося мати справу з інтернаціоналізацією. Якщо точніше, то рядки закодовані UTF-16.

console.log("Hello, world");
console.log("你好,世界!"); // Майже всі символи Unicode можуть бути записані в рядкових літералах буквально

Рядки можуть бути записані як з одинарними, так і подвійними лапками: JavaScript не розрізняє окремі символи та рядки. Коли треба представити один-єдиний символ, це роблять просто у вигляді рядка з цим одним символом.

console.log("Hello"[1] === "e"); // true

Аби з'ясувати довжину рядка (в кодових одиницях), слід звернутись до його властивості length.

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

Оператор + для рядків перевантажений: коли один з операндів є рядком, то він виконує зчеплення рядків, а не числове додавання. Особливий синтаксис шаблонних літералів дає змогу стисліше записувати рядки зі вбудованими в них виразами. На відміну від f-рядків Python чи інтерпольованих рядків C#, шаблонні літерали використовують гравіси (а не одинарні чи подвійні лапки)

const age = 25;
console.log("Мені " + age + " років."); // Зчеплення рядків
console.log(`Мені ${age} років.`); // Шаблонний літерал

Інші типи

JavaScript розрізняє null, котре позначає свідоме незначення (і доступне лише за допомогою ключового слова null) і undefined, що позначає відсутність значення. Є чимало способів отримати undefined:

  • Інструкція return за відсутності значення (return;) неявно повертає undefined.
  • Звертання до відсутньої властивості об'єкта (obj.iDontExist) повертає undefined.
  • Оголошення змінної без ініціалізації (let x;) неявно ініціалізує її значенням undefined.

JavaScript має тип Boolean, чиї можливі значення – true і false — є ключовими словами. Будь-яке значення може бути перетворено на булеве згідно з наступними правилами:

  1. false, 0, порожні рядки (""), NaN, null і undefined – стають false.
  2. Всі інші значення стають true.

Таке перетворення можна виконати явно за допомогою функції Boolean():

Boolean(""); // false
Boolean(234); // true

Проте це необхідно нечасто, адже JavaScript непомітно виконує цю операцію, коли очікує на булеве значення, як то в інструкції if (дивіться Контрольні структури). У зв'язку з цим іноді кажуть про "істинні" та "хибні" значення, маючи на увазі значення, котрі стають true і false відповідно, бувши вжитими в булевому контексті.

Підтримуються булеві операції, як то && (логічне і), || (логічне або) і ! (логічне не); дивіться Оператори.

Тип Symbol нерідко використовується для створення унікальних ідентифікаторів. Кожний символ, створений за допомогою функції Symbol(), гарантовано є унікальним. На додачу до цього, є зареєстровані символи, котрі є спільними сталими, а також загальновідомі символи, котрі використовуються мовою як "протоколи" для певних операцій. Більше про них можна прочитати в довідці про символи.

Змінні

Змінні в JavaScript оголошуються за допомогою одного з трьох ключових слів: let, const і var.

let дає змогу оголошувати змінні блокового рівня. Оголошена змінна стає доступною для блоку навколо неї.

let a;
let name = "Семен";

// myLetVariable тут *недоступна*

for (let myLetVariable = 0; myLetVariable < 5; myLetVariable++) {
  // myLetVariable доступна лише тут
}

// myLetVariable тут *недоступна*

const дає змогу створювати змінні, чиї значення ніколи не повинні змінюватися. Така змінна доступна всередині блоку, в якому вона оголошена

const Pi = 3.14; // Оголошення змінної Pi
console.log(Pi); // 3.14

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

const Pi = 3.14;
Pi = 1; // викине помилку, адже не можна змінювати сталу змінну.

Оголошення const запобігають лише повторним присвоєнням – але не видозмінам значення змінної, коли воно є об'єктом.

const obj = {};
obj.a = 1; // немає помилки
console.log(obj); // { a: 1 }

Оголошення var можуть мати дивну поведінку (наприклад, вони не обмежені блоками), і ними краще не користуватись у сучасному коді на JavaScript.

Коли змінна оголошується без присвоєння їй жодного значення, то її значенням стає undefined. Не можна оголосити змінну const без ініціалізатора, тому що її все одно не вийде оновити пізніше.

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

function foo(x, condition) {
  if (condition) {
    console.log(x);
    const x = 2;
    console.log(x);
  }
}

foo(1, true);

В більшості інших мов це б вивело "1" і "2", тому що до рядка const x = 2 x все одно вказує на параметр x у зовнішній області видимості. У JavaScript, у зв'язку з тим, що кожне оголошення поширюється на всю область видимості, це викидає помилку на першому console.log: "Cannot access 'x' before initialization". Докладніше про – на довідковій сторінці let.

JavaScript є динамічно типізованою мовою. Типи (як це описано в попередньому розділі) зв'язані лише зі значеннями, але не зі змінними. Змінним, оголошеним з let, завжди можна змінити тип при повторному присвоєнні значення.

let a = 1;
a = "foo";

Оператори

Серед числових операторів JavaScript – +, -, *, /, % (остача) і ** (піднесення до степеня). Значення присвоюються за допомогою =. Кожний бінарний оператор має також складений аналог з присвоєнням, як то += і -=, котрий розгортається в x = x оператор y.

x += 5;
x = x + 5;

Можна використовувати ++ і -- – для інкременту й декременту, відповідно. Їх можна використовувати як префіксні чи постфіксні оператори.

Оператор + також виконує зчеплення рядків:

"привіт" + " світе"; // "привіт світе"

Якщо додати рядок до числа (чи іншого значення), то все буде спершу перетворено на рядки. Ось куди це може завести:

"3" + 4 + 5; // "345"
3 + 4 + "5"; // "75"

Додавання до чогось порожнього рядка – корисно для перетворення цього чогось на рядок.

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

123 == "123"; // true
1 == true; // true

123 === "123"; // false
1 === true; // false

Подвійна та потрійна рівність також мають відповідники для нерівності: != і !==.

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

const a = 0 && "Привіт"; // 0, тому що 0 – "хибне" значення
const b = "Привіт" || "світ"; // "Привіт", адже і "Привіт", і "світ" – "істинні" значення

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

const name = o && o.getName();

Або для кешування значень (коли хибні значення – недійсні):

const name = cachedName || (cachedName = getName());

Повний список операторів – на сторінці посібника та у відповідному розділі довідки. Особливо цікавим може бути пріоритет операторів.

Граматика

Граматика JavaScript – доволі подібна до граматики мов родини C. Є кілька речей, вартих згадки:

  • Ідентифікатори можуть містити символи Unicode, але не можуть збігатися з зарезервованими словами.
  • Коментарі – загальновживані // і /* */, коли чимало інших сценарних мов, як то Perl, Python і Bash – використовують #.
  • Крапки з комою в JavaScript – необов'язкові: мова автоматично їх додає, коли це потрібно. Проте трапляються проблемні місця, котрих слід остерігатися, адже на відміну від Python, для JavaScript крапки з комою все ж є частиною синтаксису.

Поглиблений розгляд граматики JavaScript – на сторінці довідки щодо лексичної граматики.

Контрольні структури

JavaScript має подібний до інших мов родини С набір контрольних структур. Умовні інструкції підтримуються у вигляді if та else; їх можна скласти в ланцюжок:

let name = "кошенята";
if (name === "щенята") {
  name += " гав";
} else if (name === "кошенята") {
  name += " няв";
} else {
  name += "!";
}
name === "кошенята няв";

JavaScript не має elif, а else if – це фактично просто гілка else, що складається лише з інструкції if.

JavaScript має цикли while і цикли do...while. Перший з них – добрий для простих циклічних операцій, а другий – для тих випадків, коли треба пересвідчитись, що тіло циклу виконається принаймні раз:

while (true) {
  // нескінченний цикл!
}

let input;
do {
  input = get_input();
} while (inputIsNotValid(input));

У JavaScript цикл for – такий же, як в C і Java: він дає змогу надати контрольну інформацію про цикл в одному рядку.

for (let i = 0; i < 5; i++) {
  // Виконається 5 разів
}

JavaScript містить іще два важливі цикли for: for...of, котрий ітерує ітеровані об'єкти, перш за все – масиви, і for...in, котрий обробляє всі перелічувані властивості об'єкта.

for (const value of array) {
  // певні дії над value
}

for (const property in object) {
  // певні дії над властивістю об'єкта
}

Інструкція switch може використовуватись для утворення багатьох гілок на основі перевірки рівності:

switch (action) {
  case "draw":
    drawIt();
    break;
  case "eat":
    eatIt();
    break;
  default:
    doNothing();
}

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

На відміну від частини мов, як то Rust, структури контролю виконання в JavaScript є інструкціями, а отже – їх не можна присвоїти змінній якось так: const a = if (x) { 1 } else { 2 }.

Помилки в JavaScript обробляються за допомогою інструкції try...catch.

try {
  buildMySite("./website");
} catch (e) {
  console.error("Збирання сайту не вдалося:", e);
}

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

function buildMySite(siteDirectory) {
  if (!pathExists(siteDirectory)) {
    throw new Error("Тека сайту не існує");
  }
}

Загалом, невідомо, якого типу помилка була перехоплена, адже інструкцією throw може бути викинуто будь-що. Проте зазвичай можна припускати, що викинуто примірник Error, як у прикладі вище. Є кілька вбудованих підкласів Error, як то TypeError і RangeError, котрі можна використовувати для передачі додаткової інформації про помилку. У JavaScript немає умовного перехоплення, – коли треба перехопити помилки лише одного типу, слід перехоплювати все, з'ясовувати тип помилки за допомогою instanceof і повторно викидати все зайве.

try {
  buildMySite("./website");
} catch (e) {
  if (e instanceof RangeError) {
    console.error(
      "Схоже, що параметр лежить поза множиною допустимих значень:",
      e,
    );
    console.log("Повторна спроба...");
    buildMySite("./website");
  } else {
    // Невідомо, як обробляти помилки інших типів; їх слід викинути, аби
    // десь вище в стеку викликів їх могли перехопити й обробити
    throw e;
  }
}

Якщо помилка не перехоплена try...catch у стеку викликів, то відбудеться вихід з програми.

Повний список інструкцій контролю виконання – у розділі довідника.

Об'єкти

Об'єкти JavaScript можна уявляти як колекції пар ключ-значення. У цьому вони подібні до:

  • Словників у Python.
  • Гешів у Perl і Ruby.
  • Гештаблиць у C та C++.
  • HashMap у Java.
  • Асоціативних масивів у PHP.

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

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

const obj = {
  name: "Carrot",
  for: "Max",
  details: {
    color: "orange",
    size: 12,
  },
};

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

// Запис крапки
obj.name = "Simon";
const name = obj.name;

// Запис квадратних дужок
obj["name"] = "Simon";
const name = obj["name"];

// Для визначення ключа можна використовувати змінну
const userName = prompt("what is your key?");
obj[userName] = prompt("what is its value?");

Звертання до властивостей можна об'єднувати в ланцюжок:

obj.details.color; // orange
obj["details"]["size"]; // 12

Об'єкти завжди є посиланнями, тож якщо не скопіювати об'єкт явно, то видозміни об'єкта будуть помітні зовні.

const obj = {};
function doSomething(o) {
  o.x = 1;
}
doSomething(obj);
console.log(obj.x); // 1

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

const me = {};
const stillMe = me;
me.x = 1;
console.log(stillMe.x); // 1

Більше про об'єкти та прототипи – на сторінці довідника Object. Більше про синтаксис об'єктного ініціалізатора – на його сторінці довідника.

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

Масиви

Масиви у JavaScript фактично є особливим типом об'єктів. Вони працюють вельми подібно до звичайних об'єктів (до числових властивостей, природно, можна звернутися лише за допомогою синтаксису []), але на додачу мають магічну властивість length. Її значення завжди на одиницю більше за найбільший індекс у масиві.

Масиви зазвичай створюються за допомогою літералів масивів:

const a = ["пес", "кіт", "курка"];
a.length; // 3

Масиви JavaScript усе ж є об'єктами: їм можна присвоювати будь-які властивості, включно з довільними числовими індексами. Єдина "магія" полягає в тому, що властивість length автоматично буде оновлена при присвоєнні певного індексу.

const a = ["пес", "кіт", "курка"];
a[100] = "лисиця";
console.log(a.length); // 101
console.log(a); // ['пес', 'кіт', 'курка', порожнє × 97, 'лисиця']

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

Звертання до індексу поза length помилки не викидає. Якщо звернутися до відсутнього індексу масиву, буде отримано значення undefined:

const a = ["пес", "кіт", "курка"];
console.log(typeof a[90]); // undefined

Масиви можуть містити будь-які елементи, а також довільно збільшуватись і зменшуватись.

const arr = [1, "foo", true];
arr.push({});
// arr = [1, "foo", true, {}]

Масиви можна ітерувати в циклі for, як це можливо в інших C-подібних мовах:

for (let i = 0; i < a.length; i++) {
  // Певні дії з a[i]
}

Або, оскільки масиви є ітерованими об'єктами, можна використати for...of, що є синонімом синтаксису for (int x : arr) у C++ і Java:

for (const currentValue of a) {
  // Певні дії з currentValue
}

Масиви несуть чимало методів масивів. Чимало з них ітерує масив – наприклад, map() застосовує функцію зворотного виклику до кожного елемента масиву й повертає новий масив

const babies = ["пес", "кіт", "курка"].map((name) => `малятко ${name}`);
// babies = ['малятко пес', 'малятко кіт', 'малятко курка']

Функції

Поруч з об'єктами, ключовими компонентами для розуміння JavaScript є функції. Найбазовіше оголошення функції має наступний вигляд:

function add(x, y) {
  const total = x + y;
  return total;
}

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

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

add(); // NaN
// Рівносильно add(undefined, undefined)

add(2, 3, 4); // 5
// додано перші два; 4 – проігноровано

Доступна низка інших синтаксисів параметрів. Наприклад, синтаксис решти параметрів дає змогу зібрати всі надлишкові параметри, передані викликачем, у масив, подібно до *args у Python. (Оскільки JS не має іменованих параметрів на рівні мови, то **kwargs немає.)

function avg(...args) {
  let sum = 0;
  for (const item of args) {
    sum += item;
  }
  return sum / args.length;
}

avg(2, 3, 4, 5); // 3.5

У коді вище змінна args зберігає всі значення, котрі були передані до функції.

Параметр решти збереже всі аргументи після місця, де оголошений, але не до нього. Інакше кажучи, function avg(firstValue, ...args) збереже перше значення, передане до функції, у змінній firstValue, а решту аргументів – у args.

Якщо функція приймає список аргументів, і його значення уже лежать у масиві, можна використати у виклику функції синтаксис розгортання – для розгортання масиву як списку аргументів. Наприклад: avg(...numbers).

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

// Зверніть увагу на дужки { }: це деструктурування об'єкта
function area({ width, height }) {
  return width * height;
}

// Тут дужки { } утворюють новий об'єкт
console.log(area({ width: 2, height: 3 }));

Також є синтаксис усталеного параметра, котрий дає упущеним параметрам (або таким параметрам, що передані як undefined) змогу мати усталене значення.

function avg(firstValue, secondValue, thirdValue = 0) {
  return (firstValue + secondValue + thirdValue) / 3;
}

avg(1, 2); // 1, а не NaN

Анонімні функції

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

// Зверніть увагу, що перед дужками немає імені функції
const avg = function (...args) {
  let sum = 0;
  for (const item of args) {
    sum += item;
  }
  return sum / args.length;
};

Такий код робить анонімну функцію доступною для заклику шляхом виклику avg() з якимись аргументами – що семантично рівносильно оголошенню функції за допомогою синтаксису оголошення function avg() {}.

Є іще один спосіб визначити анонімну функцію – використання виразу стрілкової функції.

// Зверніть увагу, що перед дужеками немає імені функції
const avg = (...args) => {
  let sum = 0;
  for (const item of args) {
    sum += item;
  }
  return sum / args.length;
};

// `return` можна опустити, просто повернувши вираз
const sum = (a, b, c) => a + b + c;

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

Анонімні функції можуть бути корисними в іще один спосіб: їх можна як оголосити, так і зразу закликати – в межах одного виразу, що зветься Негайно закликаним функційним виразом (IIFE):

(function () {
  // …
})();

На тему ситуацій для застосування IIFE варто прочитати імітацію приватних методів за допомогою замикань.

Рекурсивні функції

JavaScript дозволяє викликати функції рекурсивно. Це особливо корисно при обробці деревоподібних структур, як от структури DOM у браузері.

function countChars(elm) {
  if (elm.nodeType === 3) {
    // TEXT_NODE
    return elm.nodeValue.length;
  }
  let count = 0;
  for (let i = 0, child; (child = elm.childNodes[i]); i++) {
    count += countChars(child);
  }
  return count;
}

Функційні вирази також можуть бути іменовані, що дає їм змогу бути рекурсивними.

const charsInBody = (function counter(elm) {
  if (elm.nodeType === 3) {
    // TEXT_NODE
    return elm.nodeValue.length;
  }
  let count = 0;
  for (let i = 0, child; (child = elm.childNodes[i]); i++) {
    count += counter(child);
  }
  return count;
})(document.body);

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

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

Функції є об'єктами першого класу

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

// Функція повертає функцію
const add = (x) => (y) => x + y;
// Функція приймає функцію
const babies = ["пес", "кіт", "курка"].map((name) => `малятко ${name}`);

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

Внутрішні функції

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

function parentFunc() {
  const a = 1;

  function nestedFunc() {
    const b = 4; // parentFunc не може цього використовувати
    return a + b;
  }
  return nestedFunc(); // 5
}

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

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

Класи

JavaScript пропонує синтаксис класу, доволі подібний до такого синтаксису в мовах штибу Java.

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    return `Привіт, я ${this.name}!`;
  }
}

const p = new Person("Марія");
console.log(p.sayHello());

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

const withAuthentication = (cls) =>
  class extends cls {
    authenticate() {
      // …
    }
  };

class Admin extends withAuthentication(Person) {
  // …
}

Статичні властивості створюються за допомогою префіксу static. Приватні властивості – префіксу решітки # (не private). Решітка є невіднятною частиною імені властивості. (Уявляйте # як _ у Python.) На відміну від більшості інших мов, способу отримати значення приватної властивості поза тілом класу немає зовсім – навіть у похідних класах.

Детальні настанови щодо різних можливостей класів – на сторінці посібника.

Асинхронне програмування

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

Є три сформовані способи писати на JavaScript асинхронний код:

  • На основі функцій зворотного виклику (як то setTimeout())
  • На основі Promise
  • async і await, що є синтаксичним цукром для Promise

Наприклад, ось який вигляд на JavaScript може мати вигляд операція зчитування з файлу:

// На основі функцій зворотного виклику
fs.readFile(filename, (err, content) => {
  // Ця функція зворотного виклику закликається, коли файл прочитано, що може трапитись після певного часу
  if (err) {
    throw err;
  }
  console.log(content);
});
// Код тут буде виконаний, поки файл іще очікує на зчитування

// На основі Promise
fs.readFile(filename)
  .then((content) => {
    // Що робити, коли файл зчитано
    console.log(content);
  })
  .catch((err) => {
    throw err;
  });
// Код тут буде виконаний, поки файл іще очікує на зчитування

// Async/await
async function readFile(filename) {
  const content = await fs.readFile(filename);
  console.log(content);
}

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

Коли є асинхронне значення, то неможливо отримати його синхронно. Наприклад, коли є проміс, то до підсумкового результату можна звернутися лише через метод then(). Подібно до цього, await можна використовувати лише в асинхронному контексті, котрий зазвичай є асинхронною функцією або модулем. Проміси ніколи не блокують виконання – відкладається лише логіка, котра залежить від їх результатів; все решта тим часом виконується. Якщо ви функційний програміст, то можете впізнати в промісах монади, котрі можуть бути відображені за допомогою then() (проте проміси не є правильними монадами, бо автоматично сплощуються; наприклад, не можна отримати Promise<Promise<T>>).

Фактично однопотокова модель зробила Node.js популярною платформою для серверного програмування, завдяки своєму неблокувальному введенню-виведенню, через що обробка великої кількості запитів до бази даних чи файлової системи є дуже швидкою. Проте зав'язані на ЦП (інтенсивні в обчисленні) задачі, котрі є чистим JavaScript, усе ж блокують головний потік. Для досягнення справжнього паралелізму може знадобитися використання воркерів.

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

Модулі

Також JavaScript описує модульну систему, котра підтримується більшістю середовищ виконання. Модуль – це зазвичай файл, котрий ідентифікується шляхом до себе, або URL. Інструкції import і export можна використовувати для обміну даними між модулями:

import { foo } from "./foo.js";

// Неекспортовані змінні є внутрішніми відносно свого модуля
const b = 2;

export const a = 1;

На відміну від Haskell, Python, Java тощо, вирішення модулів JavaScript цілком визначається середовищем: зазвичай воно засноване на URL або шляхах до файлів, тож відносні шляхи до файлів "просто працюють" і є відносними до шляху поточного модуля, а не якогось кореневого шляху проєкту.

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

Різні середовища можуть використовувати різні модульні системи. Наприклад, Node.js використовує менеджер пакетів npm і здебільшого засноване на файловій системі, натомість Deno і браузери – цілком покладаються на URL, і там модулі можуть бути вирішені через HTTP URL.

Більше інформації про це – на сторінці посібника про модулі.

Мова та середовище виконання

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

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

Середовище виконання, або ж хост, – це щось, що передає дані рушієві JavaScript (інтерпретатору), надає додаткові глобальні властивості та надає зачепи, через які рушій може взаємодіяти з навколишнім світом. Вирішення модулів, зчитування даних, друк повідомлень, надсилання мережевих запитів тощо – все це операції на рівні середовища виконання. Від часу своєї появи JavaScript була пристосована до різних середовищ, як то браузерів (котрі надають API штибу DOM), Node.js (котра надає API штибу доступу до файлової системи) тощо. JavaScript була успішно інтегрована у веб (що було її головним призначенням), мобільні застосунки, стільникові застосунки, серверні застосунки, безсерверні, вбудовані системи, й не тільки. При вивченні можливостей ядра JavaScript важливо також розуміти можливості, надані хостом, аби використовувати отримані знання. Наприклад, можна прочитати про всі API вебплатформи, реалізовані браузерами, й іноді не лише ними.

Подальше вивчення

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

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