JavaScript: синтаксис, типы, функции

В формах из прошлого урока уже мелькал JS - addEventListener, fetch, e.preventDefault(). Сейчас разберём, что это за язык, какие у него грабли и зачем он бэкендеру.

JS - это язык, который браузер исполняет на стороне клиента. На бэкенде ты пишешь API на Go/PHP. На фронте JS дёргает твой API, рисует интерфейс, валидирует формы. Знание JS на уровне «прочитал и понял» - обязательный минимум для backend-разработчика. Без него ты не дебагнешь интеграцию с фронтом, не напишешь скрипт автоматизации, не разберёшь чужой fetch.

Где запускать JS

Три способа на старте - выбирай удобный:

1. DevTools → Console - открой любую страницу, F12 → вкладка Console.
   Печатай команды, видишь результат. Идеально для экспериментов.

2. .html файл со скриптом:
   <script>console.log('Привет');</script>
   Открой файл в браузере, открой DevTools → Console.

3. Node.js (изучим в js-node) - JS без браузера.

Первые уроки будем работать в DevTools → Console. Бэкендер всё равно проводит там половину времени дебага.

Переменные: let и const

let count = 0;          // можно переприсваивать
count = 5;              // OK

const PI = 3.14;        // нельзя переприсваивать
// PI = 3.15;           // TypeError: Assignment to constant variable

let - изменяемая переменная. const - неизменяемая. По умолчанию пиши const, переключайся на let только когда точно нужно переприсваивать (счётчик, флаг, аккумулятор).

В legacy-коде встретится `var` - старый способ объявления, с неудобными правилами видимости (поднимается на верх функции, игнорирует блоки). Читать научишься без специального обучения, писать новое - никогда.

const для объекта - не «всё запрещено»

Главная грабля для бэкендера, пришедшего из языков со строгой иммутабельностью:

const user = { name: 'Иван' };
user.name = 'Пётр';      // OK! Объект тот же, поменялось поле
user.age = 30;           // OK! Добавили новое поле

// user = {};            // TypeError: переприсваивание самой ссылки запрещено

const запрещает менять саму ссылку на объект, но не запрещает менять содержимое. Если нужна реальная иммутабельность - Object.freeze(user).

Типы

JS - динамически типизированный язык. Тип определяется значением, а не объявлением:

let x = 42;        // number
x = 'hello';       // string - JS не возражает
x = true;          // boolean
x = null;          // null
x = undefined;     // undefined
x = { a: 1 };      // object
x = [1, 2, 3];     // тоже object (массив - это объект)

console.log(typeof x);  // "object"

Семь типов: number, string, boolean, null, undefined, object (включая массивы и функции), symbol (редко). typeof null === 'object' - исторический баг JS, который никто уже не исправит.

number - без int и float

В JS нет отдельного целого и дробного - только number:

1 + 1            // 2
1.5 * 2          // 3
1 / 0            // Infinity (не ошибка!)
0 / 0            // NaN (Not a Number)
0.1 + 0.2        // 0.30000000000000004 - float, IEEE 754

Если нужна точность для денег - отдельная библиотека (decimal.js) или хранить копейки целыми числами.

string - двойные, одинарные, обратные

const a = 'одинарные';
const b = "двойные";
const c = `обратные - поддерживают подстановку: ${a}`;
// → "обратные - поддерживают подстановку: одинарные"

Обратные кавычки (template literals) - для строк с подстановкой ${...} и для многострочных строк. По умолчанию пиши 'одинарные', если не нужна подстановка.

null vs undefined

let a;              // объявлена, не присвоена → undefined
let b = null;       // явно «значения нет»

console.log(typeof a);   // "undefined"
console.log(typeof b);   // "object" (опять исторический баг)

Конвенция: undefined - «значения не присвоили», null - «специально поставили пусто». На практике многие API возвращают то одно, то другое. Проверяй обе ситуации одной идиомой: if (value == null) сравнит и с null, и с undefined.

Главная грабля: == vs ===

'1' == 1            // true - приведение типов
'1' === 1           // false - строгое сравнение

null == undefined   // true
null === undefined  // false

[] == false         // true - wtf?
[] === false        // false

== сначала приводит типы, потом сравнивает. Это источник тонн багов. Всегда пиши === и !==. Единственное полезное исключение - == null для проверки «null или undefined» одним выражением.

В реальном проекте включают ESLint с правилом `eqeqeq` - он подсветит каждое `==` и потребует `===`. Дисциплина инструмента, не личная.

Truthy и falsy

Многие операторы (if, &&, ||) принуждают значение к boolean. Шесть значений падают в false, всё остальное - в true:

// Falsy:
false, 0, -0, '', null, undefined, NaN

// Всё остальное truthy, включая:
'0'         // строка из символа '0' - truthy!
[]          // пустой массив - truthy
{}          // пустой объект - truthy
'false'     // строка - truthy

Это даёт компактный код:

const name = userInput || 'Аноним';
// если userInput - пустая строка, undefined, null - возьмём 'Аноним'

Но и грабли:

const count = req.query.count || 10;
// если count === 0 (а это валидное число!), возьмём 10 → баг

С 2020-х есть ?? (nullish coalescing) - fallback только для null/undefined:

const count = req.query.count ?? 10;
// 0 останется 0, fallback только при null/undefined

Массивы и объекты

// Массивы
const arr = [1, 2, 3];
arr.length;            // 3
arr[0];                // 1
arr.push(4);           // [1, 2, 3, 4]
arr.includes(2);       // true
arr.map(x => x * 2);   // [2, 4, 6, 8]
arr.filter(x => x > 2);// [3, 4]

// Объекты
const user = {
  name: 'Иван',
  age: 30,
  greet() { return 'Привет, ' + this.name; }   // метод
};

user.name;             // 'Иван'
user['age'];           // 30 - синтаксис скобок, для динамических ключей
user.greet();          // 'Привет, Иван'
delete user.age;       // удалить поле

Деструктуризация

Достать поля одним выражением:

const user = { name: 'Иван', age: 30, email: 'i@m.ru' };

const { name, age } = user;
console.log(name, age);  // "Иван" 30

// С переименованием:
const { name: userName } = user;
// userName === 'Иван'

// Массивы:
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first=1, second=2, rest=[3, 4, 5]

Spread (распыление)

// Скопировать массив:
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];   // [1, 2, 3, 4, 5]

// Скопировать объект:
const user = { name: 'Иван', age: 30 };
const updated = { ...user, age: 31 };  // { name: 'Иван', age: 31 }

Spread - иммутабельный путь обновлять объекты. В React пригодится, но и в обычном фронте удобно.

Функции

Два синтаксиса:

// Обычная функция
function add(a, b) {
  return a + b;
}

// Стрелочная (arrow)
const add2 = (a, b) => a + b;

// Стрелочная с телом
const add3 = (a, b) => {
  const result = a + b;
  return result;
};

Стрелочные функции короче и решают проблему this (про это - на отдельном уроке, если зайдёт). Для бэкендера правило простое: пиши стрелочные везде, кроме методов внутри объекта.

Параметры по умолчанию

function greet(name = 'друг', greeting = 'Привет') {
  return `${greeting}, ${name}!`;
}

greet();                    // "Привет, друг!"
greet('Иван');              // "Привет, Иван!"
greet('Иван', 'Здарова');   // "Здарова, Иван!"

Rest-параметры

Сколько угодно аргументов в один массив:

function sum(...nums) {
  return nums.reduce((acc, n) => acc + n, 0);
}

sum(1, 2, 3, 4);  // 10

Замыкания на минимуме

Функция, объявленная внутри другой функции, помнит переменные внешней:

function makeCounter() {
  let count = 0;
  return () => ++count;
}

const counter = makeCounter();
counter();   // 1
counter();   // 2
counter();   // 3

counter - стрелочная функция, которая «помнит» count из makeCounter. Это и есть замыкание. На практике встречается каждый день: setTimeout, обработчики событий, фабрики функций - везде замыкания.

Грабли, которые экономят часы дебага

1. Скобки при возврате объекта из стрелки

const wrong = () => { name: 'Иван' };   // вернёт undefined!
const right = () => ({ name: 'Иван' }); // обернули в круглые скобки

Без круглых скобок JS думает, что { ... } - это тело функции, а name: - метка.

2. === для NaN не работает

NaN === NaN     // false! Единственное значение, не равное самому себе
Number.isNaN(NaN)  // true - правильная проверка

3. Объекты сравниваются по ссылке

{ a: 1 } === { a: 1 }     // false - это два разных объекта в памяти
const x = { a: 1 };
x === x                     // true - та же ссылка

Для сравнения по содержимому нужна функция или JSON.stringify(a) === JSON.stringify(b) (хрупкий способ, но иногда сходит).

4. for...in vs for...of

const arr = ['a', 'b', 'c'];

for (const i in arr) console.log(i);     // '0', '1', '2' - индексы (строки!)
for (const v of arr) console.log(v);     // 'a', 'b', 'c' - значения

Для массивов всегда for...of. for...in - для объектов, и то редко.

Что пропустили намеренно

В реальном JS-коде встретишь много того, что в этом уроке нет: классы, prototypes, this в его странных формах, async-итераторы, генераторы, символы, прокси. На уровне «прочитал и понял» это не критично - достаточно знать, что они есть, и гуглить по необходимости. Гнать тебя через всю спецификацию ES2020 на старте - пустая трата времени.

Мини-задание

  • Открой DevTools → Console на любой странице
  • Напиши const user = { name: 'Иван', age: 30 }; и достань name через деструктуризацию
  • Создай стрелочную функцию, которая принимает массив чисел и возвращает их сумму через reduce
  • Объясни себе, почему 0.1 + 0.2 !== 0.3
  • Найди в чужом фронт-коде (например, в твоём собственном фронте) одно использование === и одно - === после ??. Понял ли что они делают?

На следующем уроке начнём работать с DOM - изменять страницу из JS, ловить клики и сабмиты форм без перезагрузки.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.