Обзор

Раздел покрывает базовые конструкции JavaScript и jQuery которые нужны для работы с фронтом Laravel-проектов. Advanced-темы (React, TypeScript, генераторы, worker'ы, service worker) намеренно вынесены за рамки.

Ключевые темы раздела

ТемаРаздел
var vs let vs constТипы
== vs ===Операторы
Function declaration vs arrow functionФункции
this в arrow vs обычнойФункции
Array methods: map/filter/reduce/forEachМассивы
Destructuring + spreadОбъекты
querySelector + addEventListenerDOM/Events
Promise / async-awaitAsync
fetch API / AJAXFetch
jQuery $(...) selectorsjQuery
jQuery events + delegatedjQuery
$.ajax / $.postjQuery AJAX
CSRF token в Laravel AJAXИнтеграция
JavaScript: типы, var / let / const

Примитивы (7 типов) + Object

ТипПримерtypeof
number42, 3.14, NaN, Infinity'number'
bigint9007199254740992n'bigint'
string'hello', "a", `template ${x}`'string'
booleantrue / false'boolean'
nullnull'object' ← классический баг
undefinedundefined'undefined'
symbolSymbol('id')'symbol'
object{}, [], new Date()'object' / 'function' для fn
typeof null === 'object' — известный баг JS с 1995 года. Не исправят из соображений совместимости.

var / let / const — главное отличие

varletconst
Область видимостиФункция (function scope)Блок {} (block scope)Блок {}
HoistingДа, с undefinedДа, но TDZ (temporal dead zone) → ReferenceErrorДа, но TDZ
Переобъявление (redeclaration)МожноНельзя в том же scopeНельзя
Переприсваивание (reassignment)МожноМожноНельзя (но объекты можно мутировать!)
Когда использоватьНикогда (legacy)Если значение меняетсяПо умолчанию, всегда
// Правило современного JS: const по умолчанию, let если реассигн, var никогда
const name = 'Alice';       // не меняется
let counter = 0;
counter++;                     // ok — let разрешает

// const массив/объект — ссылка неизменяема, содержимое — можно менять!
const arr = [1, 2];
arr.push(3);                // OK — мутация массива
arr = [4, 5];              // TypeError: Assignment to constant

Четыре разные операции, которые часто путают

В таблице выше «переобъявление» и «переприсваивание» стоят соседними строками, и их легко смешать. Это разные операции, и запрещены они по разным причинам.

ТерминПо-английскиЧто происходитПример
ПереприсваиваниеreassignmentСуществующей переменной дают новое значениеlet x = 1; x = 2;
ПереобъявлениеredeclarationЗаново объявляют переменную с тем же именем в том же scopelet x = 1; let x = 2;
МутацияmutationМеняют содержимое объекта или массива, не трогая саму переменнуюconst a = [1]; a.push(2);
ЗатенениеshadowingОбъявление с тем же именем во вложенном scopelet x = 1; { let x = 2; }

Переприсваивание — это x = новоеЗначение. Работает у var и let, запрещено у const:

// ── ПЕРЕПРИСВАИВАНИЕ (x = ...) ──
let counter = 0;
counter = counter + 1;   // let разрешает
counter++;               // то же самое

var old = 1;
old = 2;                 // var разрешает

const fixed = 1;
fixed = 2;               // TypeError: Assignment to constant variable
Главное про const. Он не делает значение неизменяемым — он запрещает переприсваивать саму переменную. Ссылка на объект фиксируется навсегда, а содержимое по этой ссылке менять можно.
// ── МУТАЦИЯ: содержимое под const менять можно ──
const user = { name: 'Alice' };
user.name = 'Bob';       // мутация объекта
user.age = 30;           // добавление поля
user = { name: 'Eve' };  // переприсваивание ссылки — нельзя

const arr = [1, 2];
arr.push(3);             // мутация
arr[0] = 99;             // мутация
arr = [4, 5];            // переприсваивание

Если нужна настоящая неизменяемость — Object.freeze (действует поверхностно, только на верхний уровень), глубокое копирование с заморозкой или библиотека вроде Immer.

Переобъявление — это второе let x = … для той же переменной в том же scope. Здесь правила жёстче:

// ── ПЕРЕОБЪЯВЛЕНИЕ в ТОМ ЖЕ scope ──
var a = 1;
var a = 2;    // var разрешает — и молча затирает прежнее

let b = 1;
let b = 2;    // SyntaxError: Identifier 'b' has already been declared

const c = 1;
const c = 2;  // SyntaxError

// var и let конфликтуют между собой в одном scope
var x = 1;
let x = 2;    // SyntaxError

// но во вложенном блоке это уже ДРУГАЯ переменная — затенение
let y = 1;
{
  let y = 2;  // внешняя y не тронута
}
Зачем запрет. let и const не дают переобъявить переменную, чтобы вы случайно не затёрли уже существующую. var разрешает и делает это молча — отсюда значительная часть багов в старом коде.

Truthy / Falsy — 6 значений false

// Все эти → false в bool-контексте:
false, 0, -0, 0n, "", null, undefined, NaN

// Все остальные → true, включая ловушки:
Boolean('0');         // true — строка "0" непустая!
Boolean('false');     // true — строка "false" непустая
Boolean([]);            // true — пустой массив truthy (!)
Boolean({});            // true — пустой объект truthy (!)
Внимание PHP vs JS: в PHP [] falsy. В JS [] truthy. Часто путают.
Scope — область видимости

Что это и зачем

Scope (область видимости) — часть программы, внутри которой переменная доступна по имени. За пределами своего scope она не видна: обращение даёт ReferenceError. Проще говоря, scope отвечает на вопрос «откуда эта переменная видна».

Именно от scope зависит, какие переменные конфликтуют между собой, какие затеняют друг друга и почему var и let ведут себя по-разному.

Зачем нуженЧто это даёт на практике
ИзоляцияПеременные не протекают наружу и не затирают чужие
ИнкапсуляцияДетали реализации можно скрыть — на этом стоят замыкания
Управление памятьюВышедшее из scope может быть собрано сборщиком мусора
ПредсказуемостьПонятно, где переменная существует, а где уже нет

Четыре вида scope

ВидЧто попадаетГраницы
ГлобальныйВсё объявленное на верхнем уровне скриптаВесь скрипт
МодульныйВерхний уровень файла с import/exportФайл модуля
Функциональныйvar и параметры функцииВся функция, включая вложенные блоки
Блочныйlet, constБлок { }
// Функциональный scope: var не замечает блоков
function f() {
  var x = 1;
  if (true) {
    var x = 2;   // это ТА ЖЕ переменная, не новая
  }
  console.log(x);  // 2
}

// Блочный scope: let и const живут только внутри { }
{
  let a = 1;
  const b = 2;
  var c = 3;
}
console.log(typeof a);  // 'undefined' — let не видна снаружи
console.log(typeof b);  // 'undefined' — const не видна
console.log(c);          // 3 — var «протекла» наружу
Классическая ловушка с var в цикле. for (var i = 0; …) создаёт одну переменную на всю функцию, поэтому все отложенные колбэки увидят её финальное значение. С let на каждой итерации создаётся своя переменная, и колбэки получают разные значения.
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
// 3, 3, 3 — одна общая i, к моменту вызова она уже равна 3

for (let i = 0; i < 3; i++) setTimeout(() => console.log(i));
// 0, 1, 2 — на каждой итерации своя i

Лексический scope и цепочка поиска

JavaScript использует лексический (статический) scope: видимость определяется тем, где функция написана в коде, а не тем, откуда её вызвали.

const x = 'global';

function outer() {
  const x = 'outer';

  function inner() {
    console.log(x);   // 'outer' — по месту объявления, не вызова
  }
  return inner;
}

const fn = outer();
fn();   // 'outer', хотя вызвана из глобального контекста

Когда движок встречает имя переменной, он ищет её снизу вверх по цепочке scope: текущий блок → внешняя функция → ещё внешняя → … → модульный или глобальный scope. Нашёл — использует, не нашёл нигде — ReferenceError.

const a = 1;                 ← глобальный scope
function outer() {
  const b = 2;               ← scope функции outer
  function inner() {
    const c = 3;             ← scope функции inner
    console.log(a, b, c);
    │        │  │  └─ найдена здесь же
    │        │  └──── не нашли здесь → ищем в outer → нашли
    │        └─────── не нашли ни здесь, ни в outer → глобальный → нашли
    └──────────────── поиск всегда идёт изнутри наружу, никогда наоборот
  }
  inner();
}
Отсюда растут замыкания. Функция inner запоминает своё лексическое окружение и сохраняет доступ к переменным внешней функции даже после того, как та завершилась. Это и есть замыкание — см. раздел про функции.

Затенение: другая переменная, а не переобъявление

Объявление с тем же именем во вложенном scope создаёт новую переменную, которая закрывает собой внешнюю. Внешняя при этом не меняется — это ключевое отличие затенения от переобъявления.

let x = 'outer';
{
  let x = 'inner';    // затеняет внешнюю
  console.log(x);      // 'inner'
}
console.log(x);        // 'outer' — внешняя не тронута

С var затенение работает только на границе функций — блоки он не замечает, поэтому внутри if или for это будет та же самая переменная.

Hoisting и TDZ

Hoisting (поднятие) — объявления обрабатываются при входе в scope, до выполнения кода. Разница между var и let не в том, поднимается ли объявление, а в том, инициализировано ли оно к этому моменту.

varlet / const
Объявление поднимаетсяДаДа
Значение до строки объявленияundefinedПеременная в TDZ
Обращение раньше объявленияТихо вернёт undefinedReferenceError
Куда поднимается в блоке внутри функцииДо начала функцииДо начала блока
console.log(v);   // undefined — var поднят и инициализирован
var v = 1;

console.log(l);   // ReferenceError: Cannot access 'l' before initialization
let l = 1;

console.log(c);   // ReferenceError — то же для const
const c = 1;

TDZ (temporal dead zone, временная мёртвая зона) — промежуток от начала блока до строки объявления. Это защита: ошибка ловится сразу, вместо того чтобы код тихо продолжил работу с undefined, как было с var.

Короткая формула. Scope — это «где переменная видна». JavaScript использует лексический scope: видимость определяется местом объявления, а поиск идёт по цепочке снизу вверх. var живёт в функции, let и const — в блоке. Одинаковое имя во вложенном scope — затенение, а не переобъявление.
JavaScript: операторы, == vs ===

Ключевое отличие: == vs ===

== — loose (нестрогое), делает type coercion. === — strict, сравнение типа И значения. В production всегда ===.

console.log(1 == '1');       // true — строка приведена к числу
console.log(1 === '1');      // false — разные типы
console.log(null == undefined);  // true — специальный случай (только между собой!)
console.log(null === undefined); // false — разные типы
console.log(NaN === NaN);        // false — NaN не равен ничему, даже себе
console.log(Number.isNaN(NaN));  // true — правильная проверка NaN
Правило: всегда ===. Единственное исключение — value == null для проверки одновременно на null И undefined (короткая запись).

Логические операторы (short-circuit)

// && возвращает первое falsy или последнее (не всегда bool!)
'a' && 'b';        // 'b' — оба truthy → последнее
0 && 'b';          // 0 — первое falsy

// || возвращает первое truthy или последнее
null || 'default'; // 'default'
'a' || 'b';        // 'a'

// ?? — Nullish Coalescing (ES2020) — только null/undefined, не falsy
0 ?? 10;             // 0 — 0 не null/undefined
0 || 10;             // 10 — 0 falsy
null ?? 'default'; // 'default'

// ?. — Optional Chaining (ES2020) — короткая проверка null
const avatar = user?.profile?.avatar;  // undefined если хоть где-то null
В PHP есть аналогичные ?? и ?-> — эти концепции одинаково работают.

Тернарный + spread + rest

// Тернарный
const status = age >= 18 ? 'adult' : 'minor';

// Spread ... для массивов
const a = [1, 2];
const b = [...a, 3, 4];      // [1, 2, 3, 4]

// Spread ... для объектов
const user = { name: 'Alice' };
const extended = { ...user, age: 30 };  // { name:'Alice', age:30 }

// Rest ... в параметрах
function sum(...nums) {         // nums = [1,2,3,4]
    return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4);              // 10
Все операторы (таблица)

Арифметические

ОператорЧто делаетПримерРезультат
+Сложение (или конкатенация строк)5 + 38
-Вычитание5 - 32
*Умножение5 * 315
/Деление10 / 33.333...
%Остаток от деления10 % 31
**Возведение в степень2 ** 8256
++Инкремент (+1)x++x = x + 1
--Декремент (-1)x--x = x - 1
+xПриведение к числу+"42"42
-xСмена знака-5-5
+ со строкой = конкатенация: 1 + "2" = "12" (не 3). А 1 - "2" = -1 — минус приводит к числу.

Сравнения

ОператорЗначениеПример
==Равно (loose, приводит типы)1 == "1" → true
===Строго равно (тип + значение)1 === "1" → false
!=Не равно (loose)1 != "2" → true
!==Строго не равно1 !== "1" → true
>, <Больше / меньше5 > 3 → true
>=, <=Больше/меньше или равно5 >= 5 → true

Логические

ОператорЗначениеПример
&&И — все truea && b
||ИЛИ — хотя бы один truea || b
!НЕ — инверсия!true → false
??Nullish — дефолт только для null/undefinedval ?? "def"
?.Optional chaining — безопасный доступuser?.profile?.name
? :Тернарный — короткий if/elseage > 18 ? "adult" : "minor"

Присваивания

ОператорЭквивалент
x = 5Простое присваивание
x += 3x = x + 3
x -= 3x = x - 3
x *= 3x = x * 3
x /= 3x = x / 3
x %= 3x = x % 3
x **= 3x = x ** 3
x ||= yx = x || y (если x falsy)
x &&= yx = x && y (если x truthy)
x ??= yx = x ?? y (если x null/undefined)

Специальные

ОператорЗначениеПример
...Spread / Rest[...arr, 4] / function f(...args)
,Comma — вычисляет несколько, возвращает последнееlet x = (1, 2, 3); → x=3
typeofТип значения (строкой)typeof 42 → "number"
instanceofЯвляется ли экземпляром класса[] instanceof Array → true
inЕсть ли ключ в объекте"name" in user → bool
deleteУдалить свойство объектаdelete obj.name
newСоздать экземпляр классаnew Date()
voidВсегда возвращает undefinedvoid 0 → undefined

Битовые (редко)

ОператорЗначение
&Битовое И
|Битовое ИЛИ
^Битовое XOR
~Битовое НЕ
<<Сдвиг влево
>>Сдвиг вправо
>>>Сдвиг вправо без знака
Используются редко: работа с бинарными флагами (permissions), низкоуровневые оптимизации, битмаски. В повседневной разработке почти не встречаются.
Ключевые слова / команды (statements)

Зарезервированные слова языка. Строят структуру программы.

Объявление

СловоЧто
varПеременная (function scope, legacy)
letПеременная (block scope, можно менять)
constКонстанта (block scope, нельзя переприсвоить)
functionОбъявить функцию
classОбъявить класс
asyncАсинхронная функция

Управление потоком

СловоЧто
if / elseУсловие
switch / case / defaultМножественный выбор
forЦикл со счётчиком
while / doЦикл по условию
for...ofЦикл по значениям iterable
for...inЦикл по ключам объекта
breakПрервать цикл / switch
continueСледующая итерация цикла
returnВернуть значение из функции
yieldОтдать значение из генератора
awaitЖдать Promise (внутри async)

Ошибки

СловоЧто
tryБлок с потенциальной ошибкой
catch (err)Обработчик ошибки
finallyВсегда выполняется (успех или ошибка)
throwБросить ошибку
try {
    const data = JSON.parse(badStr);
} catch (err) {
    console.error(err.message);
    throw new Error('Не смогли распарсить');
} finally {
    console.log('Всегда сработает');
}

Модули (ES6)

СловоЧто
importИмпортировать из другого файла
exportЭкспортировать переменную/функцию/класс
export defaultЭкспорт по умолчанию (один на файл)
fromОткуда импортировать
asПереименовать при импорте
// utils.js
export const PI = 3.14;
export function sum(a, b) { return a + b; }
export default class User {}

// main.js
import User, { PI, sum } from './utils.js';
import { sum as add } from './utils.js';   // переименовать

Named vs Default import — фигурные скобки vs без

Правило простое: фигурные скобки = именованный экспорт. Без скобок — default. Именно поэтому import { useI18n } from 'vue-i18n' — в скобках: useI18n объявлен как export (не export default).

В модуле экспортИмпорт
export default Ximport X from 'mod' — без скобок, имя произвольное
export const X = ...import { X } from 'mod' — в скобках, имя должно совпадать
оба одновременноimport Def, { X, Y } from 'mod'
переименовать при импортеimport { X as MyX } from 'mod'
всё сразу как namespaceimport * as M from 'mod' → M.X, M.Y

Ключевое отличие: у default-экспорта имя при импорте произвольное — модуль не знает как ты его называешь. У named — имя обязательно совпадает с экспортом (иначе — as).

// Реальный пример: vue-i18n
// Внутри библиотеки: export function useI18n() { ... }
//                    export function createI18n() { ... }
// Оба — named-экспорты, поэтому:

import { useI18n } from 'vue-i18n';              // в скобках
import { useI18n, createI18n } from 'vue-i18n';  // несколько сразу
import { useI18n as useLocale } from 'vue-i18n'; // переименовали

// Если бы vue-i18n делал `export default useI18n` — было бы:
import useI18n from 'vue-i18n';                // без скобок

Как узнать, что именно экспортирует модуль? Три способа:

  • Официальная документация — там пишут import { X } from 'lib'.
  • node_modules/имя-пакета/dist/ — открыть .d.ts файл, там видны все export.
  • В консоли (для установленного пакета): import * as M from 'lib'; console.log(M) — все именованные экспорты как поля объекта.
Классическая ошибка: import useI18n from 'vue-i18n' (без скобок) → в переменной useI18n окажется undefined либо весь модуль-объект (зависит от bundler'а). Вылезет ошибка типа «useI18n is not a function». Проверяй скобки первым делом.

Откуда вообще берётся имя useI18n

Имя задаёт автор библиотеки, когда пишет export. У vue-i18n в исходниках буквально лежит:

// где-то в node_modules/vue-i18n/dist/vue-i18n.mjs
export function useI18n(options) { ... }

Слово useI18n они сами придумали и записали в коде. Могли назвать useLang, translate — но выбрали именно так. Ты как потребитель обязан импортить с точно таким именем, потому что оно жёстко зашито в их экспортах.

Как посмотреть список экспортов реальной библиотеки:

# Ищем строки export в скомпилированном dist
grep -rn '^export' node_modules/vue-i18n/dist/*.mjs | head -3

В конце файла будет строка вида:

export { DatetimeFormat, I18nD, I18nInjectionKey, I18nN, I18nT,
         NumberFormat, Translation, VERSION,
         createI18n, useI18n, vTDirective };

Это список всего, что библиотека отдаёт наружу. Каждое имя можно импортить в скобках: import { createI18n }, import { useI18n }, import { Translation }. Чего в списке нет — импортить нельзя, ошибка будет.

Мнемоника: «имя = договор двух сторон». Автор написал export const X — назначил имя. Ты пишешь import { X } — цитируешь его буквально. Хочешь по-своему? Только через as: import { X as MyX }.

import / export — это не функции, а ключевые слова

Частая путаница: «это функции же?». Нет. Это ключевые слова синтаксиса ES-модулей — как if, for, const, class. Часть самого языка.

Функцияimport / export
Скобки при вызовеfn(x)Нет — import X from 'y' без (...)
Где можно писатьГде угодноТолько на верхнем уровне файла (не в if, не в функции)
Когда работаетВ момент вызова, в runtimeВ фазе загрузки, до запуска кода — движок парсит и строит граф зависимостей
Динамические именаfn(variable)Нельзя — путь к модулю должен быть литералом: from './x.js', не from variable

Что это значит на практике: нельзя написать if (условие) import X from 'y'. Импорты всегда наверху файла, до любого кода. Bundler (Vite / webpack) читает их до запуска, чтобы понять «какой файл от какого зависит», склеить всё в бандл, вырезать неиспользуемое (tree-shaking).

Динамический import() — единственный случай, когда import похож на функцию

Отдельная штука: import('./x.js') со скобками — это функция, возвращает Promise. Используется для code splitting: подгрузить модуль не сразу, а по требованию (например, только когда пользователь открыл окно чата).

// Обычный (статический) — сразу при загрузке страницы
import { useI18n } from 'vue-i18n';

// Динамический — грузим только когда нужно
button.addEventListener('click', async () => {
    const { openChat } = await import('./chat.js');
    openChat();
});

Здесь import(...) — это функция (со скобками), возвращающая Promise с объектом-модулем. Bundler на этой точке разрежет бандл на два файла: главный и «chat-chunk». Пользователь скачает второй только при клике.

ESM vs CommonJS — короткая история

import / export появились в ES6 (2015) — это ESM (ECMAScript Modules). До этого 10+ лет Node.js жил на CommonJS — свой формат с require() и module.exports. Обе системы до сих пор встречаются.

CommonJS (старое, Node.js)ESM (современное, стандарт)
Импорт const X = require('mod') import X from 'mod'
Named const { X } = require('mod') (деструктуризация из объекта) import { X } from 'mod'
Экспорт module.exports = X / exports.foo = 1 export default X / export const foo = 1
Динамика Обычная функция — можно везде Статический наверху, динамический через import()
Файловое расширение .js (по умолчанию в старых Node) .mjs или .js в "type": "module" проекте
Смесь двух форматов. В Node-мире бывает: пакет опубликован в CommonJS, а ты в проекте пишешь ESM (или наоборот). Отсюда ошибки вида Cannot use import statement outside a module или ERR_REQUIRE_ESM. Лечится: в package.json поставить "type": "module" (весь проект ESM), или использовать динамический import() для загрузки ESM-пакета из CJS-кода.
Итог по модулям:
  • import { X } from 'mod' — named, имя должно совпадать с export автора
  • import X from 'mod' — default, имя произвольное
  • Имена задаёт автор библиотеки — проверить можно grep '^export' node_modules/pkg/dist/*.mjs
  • import/export — ключевые слова, работают только на верхнем уровне, парсятся до запуска кода
  • Динамический import('...') — единственная «функциональная» форма, для code splitting
  • CommonJS (require) — старый Node-формат, до 2015. Сейчас — постепенно вытесняется ESM

import.meta — метаданные ES-модуля

import.meta — специальный объект, доступный внутри любого ES-модуля. Содержит метаданные о текущем файле. Часть стандарта JS. Не путать со ключевым словом import — здесь import используется как именной префикс, а .meta — свойство.

// В любом ESM-файле
console.log(import.meta);
// { url: 'file:///project/src/api.js', env: {...} }

console.log(import.meta.url);
// путь к самому файлу — в браузере URL, в Node — file://...

Стандарт определяет только import.meta.url. Всё остальное — расширения от рантайма / бандлера:

СвойствоКто добавляетЗачем
import.meta.urlСтандарт JSПуть к модулю (аналог __filename в CommonJS)
import.meta.envVite (не стандарт)Переменные из .env-файлов
import.meta.hotVite / HMRAPI для hot module replacement
import.meta.globViteМассовый импорт файлов по glob-паттерну
import.meta.resolveNode.js 20+Резолвить URL модуля

import.meta.env.VITE_* — переменные окружения в Vite

Vite при сборке собирает все переменные из .env-файлов проекта, у которых имя начинается с префикса VITE_, и делает их доступными через import.meta.env. Это способ хранить настройки (URL API, публичные ключи, флаги фичей) вне кода — легко менять между dev / staging / prod без правки исходников.

# .env.development
VITE_API_URL=https://kazchess-develop.silentsoft.kz
VITE_APP_NAME=KazChess Dev

# .env.production
VITE_API_URL=https://api.kazchess.kz
VITE_APP_NAME=KazChess
// api_public.js
import axios from 'axios';

const api = axios.create({
    baseURL: import.meta.env.VITE_API_URL,
});

export default api;

Как это работает: Vite при сборке подставляет строку в код напрямую. Итоговый JS после билда выглядит так:

const api = axios.create({
    baseURL: 'https://api.kazchess.kz',   // ← подставилось при сборке
});

Дефолтные поля import.meta.env:

ПолеЧто
import.meta.env.MODE'development' / 'production' / 'test' — режим сборки
import.meta.env.DEVtrue в dev, false в prod
import.meta.env.PRODtrue в prod-сборке
import.meta.env.BASE_URLBase path приложения (из vite.config.js)
import.meta.env.VITE_*Всё, что ты определил сам в .env
Только префикс VITE_. Переменные без префикса — не попадут в клиентский бандл (это защита: DB_PASSWORD не должен утечь в браузер). Хочешь чтобы клиент видел — префикс VITE_. Хочешь скрыть — оставляй без префикса, использовать только на сервере.
import.meta.env — фича Vite, не стандарт JS. В обычном Node.js без Vite её нет. Аналог в webpack — process.env.REACT_APP_* (Create React App), в Next.js — process.env.NEXT_PUBLIC_*. Идея та же: префикс = «можно в клиент».
Значения подставляются при сборке, не в рантайме. Изменил .env — надо пересобрать (npm run build) или перезапустить dev-сервер. Хочешь менять URL API без пересборки — читай его через отдельный runtime-конфиг (/config.json, загружаемый через fetch при старте).

Специальные значения / слова

СловоЧто
true / falseБулевые
nullЯвное «пусто»
undefinedНе присвоено
NaNNot a Number
thisКонтекст вызова
superРодительский класс
new.targetБыл ли вызов через new
debuggerТочка останова в DevTools
JavaScript: if / switch / циклы

Условия — стандартно

if (age >= 18) {
    console.log('adult');
} else if (age >= 13) {
    console.log('teen');
} else {
    console.log('child');
}

// switch — использует ===
switch (status) {
    case 'active':
    case 'pending':              // fall-through — оба обрабатываются одинаково
        handleActive();
        break;                    // обязателен break, иначе идёт дальше
    case 'blocked':
        handleBlocked();
        break;
    default:
        throw new Error('unknown status');
}

Циклы — 5 видов, когда что

ЦиклИспользовать когда
for (let i=0; i<n; i++)Классика — контроль индекса, можно break/continue
for (const item of arr)Массивы / iterable — по значениям
for (const key in obj)Только по объектам! Для массивов НЕ используй (даст индексы как строки + prototype-цепочку)
arr.forEach(fn)Массивы — короткий синтаксис, но НЕТ break/continue
while / do-whileПока условие true, число итераций неизвестно
// for...of — идиоматично для массивов
for (const user of users) {
    console.log(user.name);
}

// for...of + entries для индексов
for (const [i, user] of users.entries()) {
    console.log(i, user.name);
}

// Object.entries / keys / values
for (const [key, value] of Object.entries(user)) {
    console.log(key, value);
}
forEach нельзя прервать — break/continue/return НЕ работают как ожидаешь. Если нужен break — используй for...of или some()/find().
JavaScript: функции + arrow functions

4 способа объявить функцию

// 1. Function Declaration — hoisted (можно вызвать до объявления)
function sum(a, b) { return a + b; }

// 2. Function Expression — не hoisted
const sum = function(a, b) { return a + b; };

// 3. Arrow Function (ES6) — короткая + свой this
const sum = (a, b) => a + b;           // одно выражение — return неявный
const double = x => x * 2;                  // 1 параметр — скобки не нужны
const log = x => { console.log(x); };      // с {} return нужен явно

// 4. Метод класса (краткий синтаксис)
class Calc {
    sum(a, b) { return a + b; }
}

Главное отличие arrow: this

Arrow не имеет своего this — берёт из окружающего scope (lexical this). Обычная function имеет свой this зависящий от того как её вызвали.

class User {
    constructor(name) { this.name = name; }

    // Обычная function теряет this в setTimeout
    greetBad() {
        setTimeout(function() {
            console.log(this.name);      // undefined! this = window/undefined
        }, 100);
    }

    // Arrow function захватывает this из класса
    greetGood() {
        setTimeout(() => {
            console.log(this.name);      // 'Alice'
        }, 100);
    }
}
Когда что использовать:
  • Arrow — callbacks, короткие функции, когда нужен this из окружения
  • Обычная — методы класса, объекты, конструкторы (нельзя new с arrow)

Дефолтные параметры + destructuring

function greet(name = 'Гость', greeting = 'Привет') {
    return `${greeting}, ${name}!`;
}
greet();                     // "Привет, Гость!"
greet('Alice');              // "Привет, Alice!"

// Destructuring в параметрах — как named args в PHP 8
function createUser({ name, email, age = 18 }) {
    return { name, email, age };
}
createUser({ name: 'Alice', email: 'a@x.kz' });
JavaScript: массивы + методы

Топ-8 методов массивов (аналоги PHP)

JS методЧто делаетPHP аналог
arr.map(fn)Трансформация — новый массивarray_map
arr.filter(fn)Фильтр — новый массивarray_filter
arr.reduce(fn, init)Свёртка в 1 значениеarray_reduce
arr.forEach(fn)Обход, ничего не возвращаетforeach без результата
arr.find(fn)Первый совпавший элемент или undefinedarray_filter + [0]
arr.some(fn)Есть ли ХОТЯ БЫ ОДИН по условию → boolцикл
arr.every(fn)ВСЕ ли по условию → boolцикл
arr.includes(val)Есть ли значение → boolin_array
const users = [
    { id: 1, name: 'Alice', age: 30, active: true },
    { id: 2, name: 'Bob',   age: 15, active: false },
    { id: 3, name: 'Cena',  age: 45, active: true },
];

// map — только имена
const names = users.map(u => u.name);         // ['Alice', 'Bob', 'Cena']

// filter — только active
const active = users.filter(u => u.active);      // [Alice, Cena]

// reduce — сумма возрастов
const totalAge = users.reduce((sum, u) => sum + u.age, 0);  // 90

// find — первый совпавший
const bob = users.find(u => u.name === 'Bob');

// some / every
users.some(u => u.age < 18);     // true — Bob 15
users.every(u => u.age > 10);    // true — все старше 10

// Цепочка методов — очень идиоматично
const adultNames = users
    .filter(u => u.age >= 18)
    .map(u => u.name)
    .sort();                                            // ['Alice', 'Cena']

Мутирующие vs немутирующие

Мутирует исходныйВозвращает новый
push, pop, shift, unshiftmap, filter, slice
splice, sort, reverse, fillconcat, flat, flatMap
// slice — не мутирует, возвращает копию
const copy = arr.slice();               // или [...arr]

// splice — мутирует, извлекает/вставляет
arr.splice(1, 2);                    // удалить 2 элемента начиная с индекса 1
arr.splice(1, 0, 'new');             // вставить 'new' на индекс 1

// sort — мутирует! Всегда делай копию:
const sorted = [...arr].sort();       // исходный arr не тронут
JavaScript: объекты + destructuring

Object literal + доступ

const user = {
    name: 'Alice',
    age: 30,
    greet() { return `Hi, ${this.name}`; },        // short-hand method
    ['dynamic_' + key]: true,                       // computed property
};

// Доступ: dot и bracket notation
user.name;                       // dot — для валидных имён
user['name'];                    // bracket — для динамических ключей / пробелов
user[key];                       // bracket — из переменной

// Проверка наличия ключа
'name' in user;                  // true — есть key (даже если undefined)
user.hasOwnProperty('name');      // true — только own props, не prototype
Object.hasOwn(user, 'name');     // современный вариант (ES2022)

// Удаление ключа
delete user.age;

Точка и скобки: три записи, которые путают

Две из трёх записей — это скобочная нотация, и отличаются они только тем, что стоит внутри скобок: строка или переменная. Третья — точечная.

user.name;        // точка — имя свойства записано буквально в коде
user['name'];     // скобки + строка — то же самое, но через строковый литерал
user[key];        // скобки + переменная — имя вычисляется во время выполнения
Главное отличие. Точка подставляет имя свойства буквально. Скобки вычисляют выражение внутри и приводят результат к строке — это и становится именем свойства.

user[key] — ради этого скобки и существуют

Здесь key — переменная, её значение подставляется в момент выполнения. Точка так не умеет: user.key будет искать свойство с именем "key", а не значение переменной.

const user = { name: 'Alice', age: 30 };
const key = 'name';

user.key;   // undefined — ищет свойство "key", которого нет
user[key];  // 'Alice' — подставляет значение key, то есть user['name']

Отсюда все случаи, где без скобок не обойтись:

// 1. Имя поля приходит снаружи
function getField(obj, fieldName) {
  return obj[fieldName];
}
getField(user, 'age');   // 30

// 2. Перебор ключей
Object.keys(user).forEach(k => console.log(k, user[k]));

// 3. Колонка таблицы задана переменной
const column = 'price';
const row = { id: 1, price: 100, qty: 2 };
row[column];   // 100

// 4. Вычисляемый ключ при создании объекта
const field = 'email';
const data = { [field]: 'a@b.com' };   // { email: 'a@b.com' }

// 5. Обновление поля формы по атрибуту name — типовой приём в React
function handleChange(e) {
  const { name, value } = e.target;
  setForm(prev => ({ ...prev, [name]: value }));
}
Частая ошибка. Написать obj.fieldName там, где fieldName — переменная. Ошибки не будет, код просто молча вернёт undefined, и разбираться придётся уже по симптомам где-то дальше.

Когда скобки обязательны, а точка не работает

Точка требует, чтобы имя свойства было корректным идентификатором JavaScript: буквы, цифры, _, $, и не начинается с цифры. Всё остальное — только через скобки.

const obj = {
  'first name': 'Alice',   // пробел
  'user-id': 42,            // дефис
  '123key': 'weird',        // начинается с цифры
};

obj.first name;      // SyntaxError
obj['first name'];   // 'Alice'
obj['user-id'];      // 42
obj['123key'];       // 'weird'

Такие ключи чаще всего приходят снаружи: заголовки HTTP ('content-type'), поля из чужого JSON, колонки выгрузки. В своём коде их лучше не создавать.

Внутри скобок может стоять любое выражение — его результат приводится к строке:

user['na' + 'me'];        // 'Alice' — выражение
user[`name`];               // 'Alice' — шаблонная строка
user[0];                  // то же, что user['0'] — число станет строкой
user[null];               // user['null']
user[Symbol.iterator];    // символы — только через скобки
Символы доступны исключительно через скобки. Для Symbol точечная запись не работает в принципе — у символа нет имени, пригодного для идентификатора.

Сводка

ЗаписьНотацияИмя свойстваКогда применять
user.nameточечнаялитеральное "name"По умолчанию, когда ключ известен и корректен
user['name']скобочнаялитеральное "name"Ключ некорректен как идентификатор, либо нужно единообразие
user[key]скобочнаязначение переменной keyИмя поля известно только во время выполнения
user[expr]скобочнаярезультат выраженияВычисляемый ключ: user['a'+'b'], user[obj.id], user[Symbol.x]
Правило. Точка по умолчанию — она короче и читается лучше. Скобки — когда ключ динамический или не проходит как идентификатор. Запись user['name'] с фиксированной строкой встречается редко: при известном ключе почти всегда пишут user.name. А user[key] — единственный способ обратиться к полю, имя которого на момент написания кода неизвестно.

Destructuring — извлечение полей

// Из объекта
const { name, age } = user;
const { name: userName } = user;                // переименование
const { name, age = 18 } = user;                // дефолт если undefined
const { profile: { avatar } } = user;               // вложенное

// Из массива — по позиции
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first=1, second=2, rest=[3,4,5]

// Обмен переменных
[a, b] = [b, a];

Практический паттерн: деструктуризация возвращаемого объекта функции

Библиотеки часто возвращают из функции объект с кучей полей, и ты вытаскиваешь только нужные. Классический пример — Vue-композаблы (useI18n, useRoute, useStore), React-хуки (useState), fetch(), чтение .env.

// vue-i18n возвращает { t, locale, tm, te, d, n, ... }
const { t, locale } = useI18n();

// эквивалентно (без деструктуризации):
const i18n = useI18n();
const t = i18n.t;
const locale = i18n.locale;

// Только одно поле — так же нормально:
const { t } = useI18n();

// Переименовать:
const { t: translate } = useI18n();
translate('hello');   // теперь через новое имя

Что там за поля в примере с vue-i18n:

ПолеЧто это
tФункция перевода — t('interface.hello') → «Привет»
localeРеактивная ссылка на текущий язык ('ru' / 'en' / 'kz'). В шаблоне видна как locale, менять как locale.value = 'en'
tmПеревод массива-сообщений
d, nФорматирование даты / числа под текущий locale
Vue-специфика: реактивные значения (ref / reactive) при обычной деструктуризации теряют реактивность. Решения — toRefs() для reactive-объектов, storeToRefs() для Pinia. Подробно — в разделе Vue → Реактивность.

Копирование / слияние объектов

// Shallow copy — spread
const copy = { ...user };
const merged = { ...defaults, ...overrides };      // последний побеждает

// Или Object.assign
const copy2 = Object.assign({}, user);
Object.assign(target, src1, src2);           // мутирует target

// Deep copy — structuredClone (современно)
const deep = structuredClone(user);          // работает с nested объектами

// Старый способ deep copy (потеряет функции, Date):
const deep2 = JSON.parse(JSON.stringify(user));
spread делает shallow copy. Вложенные объекты остаются по ссылке — мутация вложенного затронет оригинал!
DOM manipulation — vanilla JS

Поиск элементов

// По ID — один элемент
const el = document.getElementById('header');

// querySelector — CSS-селектор, первый совпавший
const btn = document.querySelector('.btn-primary');
const input = document.querySelector('input[name="email"]');
const first = document.querySelector('ul li:first-child');

// querySelectorAll — все совпавшие (NodeList)
const items = document.querySelectorAll('.item');
items.forEach(item => console.log(item));

// Устаревшие (медленнее, HTMLCollection живой):
document.getElementsByClassName('btn');
document.getElementsByTagName('div');

Изменение элемента

// Текст vs HTML
el.textContent = 'Привет';             // безопасно (экранирует HTML)
el.innerHTML = '<strong>Привет</strong>';  // XSS если данные от юзера!

// Атрибуты
el.setAttribute('data-id', '42');
el.getAttribute('data-id');
el.dataset.id;                        // удобный доступ к data-* атрибутам
el.removeAttribute('disabled');

// Классы
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('open');
el.classList.contains('active');           // bool

// Стили inline
el.style.color = 'red';
el.style.backgroundColor = '#f0f0f0';      // camelCase!

// Формы
input.value;                            // значение input
checkbox.checked;                       // bool для checkbox
select.value;                           // выбранная option
innerHTML + user input = XSS. Всегда используй textContent для user-generated текста.

Создание / вставка / удаление

// Создать
const div = document.createElement('div');
div.textContent = 'Hello';
div.className = 'msg';

// Вставить
document.body.append(div);                // в конец (можно несколько)
document.body.prepend(div);               // в начало
parent.insertBefore(newEl, referenceEl);   // перед конкретным
el.insertAdjacentHTML('beforeend', '<p>x</p>');  // быстрая вставка HTML

// Удалить
el.remove();                             // современно (IE не поддерживает)
el.parentNode.removeChild(el);            // старый способ

// Клонировать
const clone = el.cloneNode(true);            // true = deep (с потомками)
Events + обработка форм

addEventListener — основной способ

const btn = document.querySelector('#submit');

btn.addEventListener('click', (event) => {
    console.log('Clicked!');
    console.log(event.target);            // на чём кликнули
    console.log(event.currentTarget);     // к чему привязан listener
});

// Удалить listener — нужна ссылка на ту же функцию!
const handler = () => console.log('once');
btn.addEventListener('click', handler);
btn.removeEventListener('click', handler);

// Опции: once, capture, passive
btn.addEventListener('click', handler, { once: true });     // сработает 1 раз
scrollEl.addEventListener('scroll', h, { passive: true });   // оптимизация scroll

console.log или return — зачем в обработчике логи

Это две разные задачи, и их легко спутать. console.log показывает значение человеку в консоли. return отдаёт значение тому месту, откуда функцию вызвали.

console.log(x)return x
Кому предназначеноРазработчику, в консольКоду, который вызвал функцию
Влияет на программуНетДа — это результат вызова
Выполнение функцииПродолжаетсяПрерывается на этой строке
Где уместноОтладка, проверка данныхФункция вызвана ради результата
// Функцию вызвали, чтобы получить результат — нужен return
function sum(a, b) {
  return a + b;        // без return вернётся undefined
}

const result = sum(2, 3);  // 5 — значение попало в переменную
console.log(result);      // а здесь лог нужен, чтобы увидеть это число

Почему return в обработчике бесполезен

Обработчик вызывает браузер, а не ваш код. Браузеру некуда девать возвращённое значение — он его просто отбрасывает.

btn.addEventListener('click', (event) => {
  return event.target;   // значение никуда не попадёт: его некому принять
});

Поэтому в обработчике либо смотрят данные логом, либо передают их дальше — вызовом другой функции или изменением состояния:

function showUserProfile(element) {
  // открыть модальное окно, подгрузить данные и так далее
}

btn.addEventListener('click', (event) => {
  showUserProfile(event.target);   // не возвращаем, а передаём
});
Правило. Возвращают значение туда, откуда функцию позвали. Обработчик зовёт браузер — значит возвращать некому. Результат работы обработчика выражается в действиях: вызвать функцию, записать состояние, изменить страницу.

Когда return в обработчике всё-таки нужен

В одном случае — как досрочный выход, чтобы не выполнять остальной код. Значение при этом не важно, важна сама остановка.

btn.addEventListener('click', (event) => {
  if (!event.target.matches('.active')) {
    return;                // кнопка неактивна — дальше не идём
  }
  doWork(event.target);     // выполнится только для активной кнопки
});

Такой приём называют ранним выходом: он избавляет от вложенных if и держит основной сценарий слева, без лишних отступов.

Важное отличие от jQuery. В jQuery return false внутри обработчика отменяет действие по умолчанию и останавливает всплытие — то есть работает как preventDefault() плюс stopPropagation(). В обычном addEventListener такого эффекта нет: return false там равносилен простому выходу, и форма всё равно отправится. Отменять нужно явно.
// jQuery: одно значение отменяет и отправку, и всплытие
$('form').on('submit', function () {
  return false;
});

// addEventListener: return false ничего не отменит
form.addEventListener('submit', (e) => {
  e.preventDefault();      // только так
});

Ключевые события

СобытиеКогда
click, dblclickКлик / двойной
submitФорма submit
changeЗначение input изменилось (после blur)
inputКаждое нажатие клавиши в input
focus, blurФокус в/из элемента
keydown, keyupКлавиатура
mouseenter, mouseleaveНаведение мыши (без bubbling)
DOMContentLoadedDOM готов (аналог $(document).ready())
loadСтраница полностью загружена + assets

Обработка формы + preventDefault

const form = document.querySelector('#login-form');

form.addEventListener('submit', async (event) => {
    event.preventDefault();                              // не перезагружать страницу

    // Извлечь данные формы
    const formData = new FormData(form);
    const data = Object.fromEntries(formData);          // {email:'a@x', password:'123'}

    // Валидация на клиенте
    if (!data.email.includes('@')) {
        return alert('Некорректный email');
    }

    // Отправка
    const res = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
    });
    const result = await res.json();
    console.log(result);
});

Event bubbling + delegation

Bubbling — событие идёт от target вверх по DOM. Delegation — вешаем listener на родителя, а не на каждый child.

// Плохо: listener на каждую кнопку (100 listeners для 100 кнопок)
document.querySelectorAll('.delete-btn').forEach(btn => {
    btn.addEventListener('click', handleDelete);
});

// Хорошо: 1 listener на родителя (delegation)
document.querySelector('#user-list').addEventListener('click', (e) => {
    if (e.target.matches('.delete-btn')) {
        handleDelete(e);
    }
});
// Плюс: работает для НОВЫХ элементов добавленных после — не нужно перепривязывать
Async: Promise + async/await

Promise — 3 состояния

СостояниеЧто значит
pendingОжидание — операция ещё идёт
fulfilledУспех — есть результат
rejectedОшибка — есть причина
// Создание Promise
const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) resolve('OK');
        else reject(new Error('Failed'));
    }, 1000);
});

// Использование через then / catch / finally
promise
    .then(result => console.log('Success:', result))
    .catch(err => console.error('Error:', err.message))
    .finally(() => console.log('Done'));                // всегда выполнится

async / await — синтаксический сахар над Promise

// async функция ВСЕГДА возвращает Promise
async function loadUser(id) {
    try {
        const res = await fetch(`/api/users/${id}`);   // ждёт Promise
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const user = await res.json();
        return user;
    } catch (err) {
        console.error('Load failed:', err);
        throw err;
    }
}

// Вызов — либо .then, либо await в другой async
loadUser(1).then(user => console.log(user));

// Или
(async () => {
    const user = await loadUser(1);
    console.log(user);
})();

Promise.all / race / allSettled

// Promise.all — параллельно + ждёт ВСЕ. Если один упал — весь rejected.
const [user, posts, comments] = await Promise.all([
    fetch('/api/user'),
    fetch('/api/posts'),
    fetch('/api/comments'),
]);

// Promise.allSettled — не падает, возвращает все результаты (успехи + ошибки)
const results = await Promise.allSettled([p1, p2, p3]);
// [{status:'fulfilled', value:...}, {status:'rejected', reason:...}]

// Promise.race — кто первый — того и результат
const fastest = await Promise.race([p1, p2, timeout(5000)]);
Fetch API / AJAX / JSON

GET запрос

// Простой GET
const res = await fetch('/api/users');
const users = await res.json();

// С параметрами через URLSearchParams
const params = new URLSearchParams({ page: 2, limit: 10 });
const res = await fetch(`/api/users?${params}`);

POST / PUT / DELETE — JSON body

const res = await fetch('/api/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
    },
    body: JSON.stringify({ name: 'Alice', email: 'a@x.kz' }),
});

// Обработка
if (!res.ok) {
    throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
fetch НЕ бросает error на 4xx/5xx! Только на network fail. Всегда проверяй res.ok вручную.

FormData — файлы + multipart/form-data

// Из существующей формы
const formData = new FormData(form);

// Или собрать вручную
const fd = new FormData();
fd.append('name', 'Alice');
fd.append('avatar', fileInput.files[0]);     // файл

await fetch('/api/upload', {
    method: 'POST',
    body: fd,        // НЕ ставь Content-Type — браузер сам поставит multipart/form-data с boundary
});

JSON — parse / stringify

const json = '{"name":"Alice","age":30}';

// Строка → объект
const obj = JSON.parse(json);          // { name: 'Alice', age: 30 }

// Объект → строка
const str = JSON.stringify(obj);
const pretty = JSON.stringify(obj, null, 2);  // с отступами 2 пробела

// Ошибки — try/catch
try {
    const obj = JSON.parse(badJson);
} catch (e) {
    console.error('Invalid JSON:', e.message);
}
JSON.parse / JSON.stringify

JSON — текстовый формат обмена данными. Два глобальных метода в JS: JSON.parse и JSON.stringify.

Формат JSON — что можно и нельзя

МожноНельзя
string, number, boolean, nullundefined, function, Symbol, BigInt
Объект { } и массив [ ]Date (превращается в строку)
Вложенность любой глубиныЦиклические ссылки (TypeError)
Ключи только в двойных кавычкахОдинарные кавычки — невалидно

JSON.stringify — объект → строка

const user = { name: 'Alice', age: 30, active: true };

JSON.stringify(user);
// '{"name":"Alice","age":30,"active":true}'

// С форматированием — 2 пробела отступ
JSON.stringify(user, null, 2);
// '{
//   "name": "Alice",
//   "age": 30,
//   "active": true
// }'

// Фильтр — только эти поля
JSON.stringify(user, ['name', 'age']);
// '{"name":"Alice","age":30}'  — active пропущено

// Функция-replacer — трансформация каждого значения
JSON.stringify(user, (key, value) => {
    if (key === 'age') return undefined;   // undefined = убрать поле
    return value;
});
// '{"name":"Alice","active":true}'

JSON.parse — строка → объект

const json = '{"name":"Alice","age":30}';

const obj = JSON.parse(json);
// { name: 'Alice', age: 30 }

// Функция-reviver — трансформация значений при парсинге
const obj2 = JSON.parse(json, (key, value) => {
    if (key === 'age') return value + 1;
    return value;
});
// { name: 'Alice', age: 31 }

Ошибки — обязательно try/catch

// JSON.parse БРОСАЕТ SyntaxError если строка невалидна
try {
    const obj = JSON.parse(userInput);
} catch (e) {
    console.error('Invalid JSON:', e.message);
    // SyntaxError: Unexpected token ... in JSON at position ...
}

// stringify тоже может бросить — циклическая ссылка или BigInt
const obj = {};
obj.self = obj;                     // объект ссылается на себя
JSON.stringify(obj);              // TypeError: Converting circular structure to JSON

JSON.stringify({ n: 10n });     // TypeError: Do not know how to serialize a BigInt

Практические приёмы

// 1. Клонировать объект (deep clone)
const clone = JSON.parse(JSON.stringify(obj));
// Теряет функции, Date превращается в строку, Symbol теряется
// Для полного клона — structuredClone(obj) (современно)

// 2. Сохранить в localStorage
localStorage.setItem('user', JSON.stringify(user));
const saved = JSON.parse(localStorage.getItem('user'));

// 3. Body для fetch POST
fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(user),
});

// 4. Разбор ответа сервера
const res = await fetch('/api/user');
const data = await res.json();       // fetch сам делает JSON.parse внутри

// 5. Сравнить два объекта поверхностно
JSON.stringify(a) === JSON.stringify(b);
// Порядок ключей важен, работает не всегда идеально

toJSON — кастомизация сериализации класса

class User {
    constructor(name, password) {
        this.name = name;
        this.password = password;
    }

    toJSON() {                             // вызывается автоматически при JSON.stringify
        return { name: this.name };  // не сериализуем password
    }
}

JSON.stringify(new User('Alice', 'secret'));
// '{"name":"Alice"}' — password не попал
jQuery — что это, актуальность

Что такое jQuery

Библиотека JS созданная в 2006 (John Resig). Основная цель — устранить кросс-браузерные несовместимости (IE6-8) и упростить DOM-операции. Один глобальный объект $ (или jQuery) даёт удобное API.

Актуальность в 2026

АспектСтатус
Новые проектыНе используют — vanilla JS + fetch API покрывают всё
Legacy проектыАктивно поддерживаются, замена стоит миллионы
WordPress и другие CMSВстроены — при работе с плагинами jQuery неизбежен
Итог: на новых проектах — vanilla JS + fetch API. На legacy — jQuery, потому что переписывать существующий код часто нецелесообразно. jQuery закрыл проблему кроссбраузерности эпохи IE 6-8; современные браузеры это решили в стандарте.

Как подключить

<!-- CDN — стандартный способ подключить jQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

<!-- Проверка версии -->
<script>
    console.log($.fn.jquery);   // "3.7.1"
</script>
jQuery Selectors

$() = document.querySelectorAll

Основа jQuery — функция $(). Принимает CSS-селектор, возвращает jQuery-объект (обёртку над NodeList).

$('#header');                        // по ID
$('.btn');                           // по классу (все)
$('div');                            // по тегу
$('input[type="checkbox"]');          // по атрибуту
$('ul li:first-child');              // любой CSS3 селектор
$('a[href^="https"]');                // href начинается с "https"

// Специфичные jQuery-селекторы (не в CSS):
$(':checked');                       // отмеченные checkbox/radio
$(':selected');                      // выбранные option
$(':visible');                       // видимые элементы
$(':hidden');
$(':contains("Hello")');              // с текстом "Hello"

// Контекст поиска — 2-й параметр
$('.item', '#container');            // .item ТОЛЬКО внутри #container

Traversal — навигация по дереву

$('.item').parent();               // прямой родитель
$('.item').parents('.wrapper');      // все предки совпадающие с селектором
$('.item').closest('form');          // ближайший предок (или сам)
$('.item').children();             // прямые дети
$('.item').find('.btn');            // потомки любого уровня
$('.item').siblings();             // соседи
$('.item').next();                 // следующий сосед
$('.item').prev();                 // предыдущий сосед

// Первый / последний / по индексу
$('.item').first();
$('.item').last();
$('.item').eq(2);                  // 3-й элемент (индекс с 0)
jQuery: DOM manipulation

Текст / HTML / value

// Getter (без аргументов) / Setter (с аргументом)
$('.msg').text();                       // прочитать текст
$('.msg').text('Привет');              // установить (безопасно, экранирует HTML)

$('.msg').html();                       // прочитать HTML
$('.msg').html('<b>Bold</b>');          // XSS если данные от юзера

$('input').val();                      // значение input
$('input').val('new value');           // установить

Атрибуты + data

$('.link').attr('href');                  // прочитать
$('.link').attr('href', '/x');            // установить
$('.link').removeAttr('disabled');

// data-* атрибуты
$('.item').data('id');                   // читает data-id
$('.item').data('id', 42);               // ставит в кеш jQuery, не в DOM! attr() лучше для DOM

Вставка / удаление

$('#list').append('<li>New</li>');          // в конец родителя
$('#list').prepend('<li>First</li>');      // в начало
$('.item').after('<p>After</p>');         // после элемента
$('.item').before('<p>Before</p>');       // перед элементом

$('.item').remove();                     // удалить полностью
$('.item').empty();                      // очистить содержимое (сам остаётся)
$('.item').detach();                     // удалить но сохранить в памяти (можно вернуть)

// Замена
$('.old').replaceWith('<div>New</div>');

Chaining — визитка jQuery

// Каждый метод возвращает jQuery-объект → можно цепочку
$('#user')
    .addClass('active')
    .removeClass('hidden')
    .attr('title', 'Selected')
    .text('Alice')
    .css('color', 'red')
    .fadeIn();
jQuery: CSS / классы
// Классы
$('.el').addClass('active');
$('.el').removeClass('hidden');
$('.el').toggleClass('open');
$('.el').hasClass('active');          // bool

// Несколько классов
$('.el').addClass('a b c');            // добавить несколько
$('.el').removeClass();                // удалить ВСЕ классы

// CSS inline
$('.el').css('color');                 // читать
$('.el').css('color', 'red');
$('.el').css({                          // несколько сразу — объектом
    'color': 'red',
    'background-color': '#f0f0f0',           // с дефисами — в кавычках
    fontSize: '16px',                     // или camelCase без кавычек
});

// Показать / скрыть
$('.el').show();
$('.el').hide();
$('.el').toggle();

// С анимацией — базовые
$('.el').fadeIn(300);
$('.el').fadeOut(300);
$('.el').slideDown();
$('.el').slideUp();
$('.el').slideToggle();
jQuery Events

$(document).ready() — аналог DOMContentLoaded

// Полный синтаксис
$(document).ready(function() {
    // DOM готов, можно работать
});

// Короткая форма (используется чаще)
$(function() {
    console.log('ready!');
});

Прямая привязка событий

// Короткие методы для популярных событий
$('#btn').click(function(e) {
    console.log('clicked');
    console.log($(this));                // this = HTML element, $(this) = jQuery объект
});

$('#form').submit(function(e) {
    e.preventDefault();
    // ...
});

$('input').change(handler);
$('input').focus(handler);
$('input').blur(handler);
$('input').keyup(handler);
$('.el').hover(enterFn, leaveFn);         // mouseenter + mouseleave

.on() — универсальный + delegated events

// Стандартная привязка
$('#btn').on('click', handler);

// Несколько событий сразу
$('input').on('focus blur', handler);

// С namespace — удобно для удаления только своих listener
$('.el').on('click.myapp', handler);
$('.el').off('.myapp');                    // удалить только с namespace myapp

//  DELEGATED events — критично для динамических элементов
// Событие вешается на РОДИТЕЛЯ но срабатывает только на потомках с селектором
$('#user-list').on('click', '.delete-btn', function(e) {
    const id = $(this).data('id');
    deleteUser(id);
});
// Плюс: работает для НОВЫХ .delete-btn добавленных после привязки — критично при AJAX-подгрузке

// Удалить
$('#btn').off('click');
$('#btn').off();                            // все события
Ключевое отличие .click() и .on('click', ...):
.click() — сокращённая версия, работает ТОЛЬКО на существующих элементах. .on('click', '.selector', ...) с делегацией — работает на элементах добавленных ПОСЛЕ (важно при AJAX-подгрузке).
jQuery: AJAX

$.get / $.post — короткие формы

// GET
$.get('/api/users', function(data) {
    console.log(data);   // уже распарсенный JSON (если сервер вернул application/json)
});

// POST
$.post('/api/users', {
    name: 'Alice',
    email: 'a@x.kz',
}, function(response) {
    console.log('Created:', response);
});

// getJSON — синоним $.get с dataType:'json'
$.getJSON('/api/users', handleUsers);

$.ajax() — полный контроль

$.ajax({
    url: '/api/users',
    method: 'POST',                                    // или type в старых версиях
    data: JSON.stringify({ name: 'Alice' }),
    contentType: 'application/json',                    // что ОТПРАВЛЯЕМ
    dataType: 'json',                                  // что ОЖИДАЕМ
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
    },
    success: function(data, textStatus, jqXHR) {
        console.log('OK', data);
    },
    error: function(jqXHR, textStatus, errorThrown) {
        console.error('Failed:', jqXHR.status, errorThrown);
    },
    complete: function() {
        // Всегда — успех или ошибка
    },
});

// Или через Promise-стиль (jQuery 1.5+)
$.ajax({ url: '/api/users' })
    .done(data => console.log(data))
    .fail(err => console.error(err))
    .always(() => console.log('done'));

Отправка формы через AJAX + serialize

$('#login-form').on('submit', function(e) {
    e.preventDefault();

    $.ajax({
        url: $(this).attr('action'),
        method: $(this).attr('method'),
        data: $(this).serialize(),                    // "name=Alice&email=a@x.kz"
        success: function(response) {
            $('#status').text('Успех!');
        },
    });
});

// Или serializeArray → массив объектов
$('#form').serializeArray();
// [{name:'email', value:'a@x.kz'}, {name:'password', value:'123'}]
jQuery vs vanilla JS — сравнение

Ключевые операции — что уходит если убрать jQuery. Полезно знать оба, чтобы переводить legacy код в современный.

ЗадачаjQueryVanilla JS
Найти по ID $('#header') document.getElementById('header')
Найти по классу $('.btn') document.querySelectorAll('.btn')
Клик listener $('.b').click(fn) el.addEventListener('click', fn)
Delegated event $('#p').on('click', '.b', fn) p.addEventListener('click', e => e.target.matches('.b') && fn(e))
Добавить класс $('.el').addClass('a') el.classList.add('a')
Скрыть $('.el').hide() el.style.display = 'none'
Установить HTML $('.el').html('...') el.innerHTML = '...'
Установить текст $('.el').text('...') el.textContent = '...'
Атрибут $('.el').attr('href', '/x') el.setAttribute('href', '/x')
AJAX GET $.get('/api', fn) fetch('/api').then(r => r.json()).then(fn)
AJAX POST $.post('/api', data, fn) fetch('/api', {method:'POST', body:JSON.stringify(data)})
DOM ready $(function(){...}) document.addEventListener('DOMContentLoaded', fn)
Итерация $('.item').each((i, el) => ...) document.querySelectorAll('.item').forEach(el => ...)
Что даёт jQuery уникального:
  • Delegated events в одну строку (в vanilla нужно писать вручную)
  • Кросс-браузерность для древних IE (сейчас не актуально)
  • Chaining — .addClass().attr().fadeIn()
  • Анимации из коробки — fadeIn, slideDown
  • Плагины — тысячи готовых компонентов (slider, datepicker и т.п.)
Форма → PHP (классика, без AJAX)

Самый базовый способ: HTML-форма отправляет данные, страница перезагружается, PHP обрабатывает.

HTML

<form action="/save.php" method="POST">
    <input name="name" type="text">
    <input name="email" type="email">
    <button type="submit">Save</button>
</form>

save.php

<?php
// Данные приходят в $_POST (для method="POST")
$name  = $_POST['name']  ?? '';
$email = $_POST['email'] ?? '';

// Валидация
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    http_response_code(400);
    exit('Invalid email');
}

// Сохранение (пример через PDO)
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute([$name, $email]);

// Redirect назад
header('Location: /users.php');
exit;

Суперглобальные массивы PHP — куда попадают данные

МассивИсточник
$_GETQuery string (?name=x) — для method="GET"
$_POSTТело POST-запроса (form-urlencoded или multipart)
$_FILESЗагруженные файлы через <input type="file">
$_REQUESTGET + POST + COOKIE (не надёжно, лучше явно)
$_COOKIECookie от браузера
$_SESSIONДанные сессии (после session_start())
$_SERVERЗаголовки, метод, путь и т.п.
file_get_contents('php://input') Сырое тело — для JSON POST (см. следующий раздел)
$_POST заполняется только для Content-Type: application/x-www-form-urlencoded или multipart/form-data. Если фронт шлёт JSON (application/json) — $_POST будет ПУСТ, нужно читать через php://input.
Fetch (vanilla JS) → PHP → JSON

Frontend — vanilla JS

const res = await fetch('/api/user.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice', email: 'a@x.kz' }),
});

if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
}

const data = await res.json();
console.log(data);   // { success: true, id: 42 }

Backend — user.php

<?php
header('Content-Type: application/json; charset=utf-8');

//  JSON приходит в raw body — читаем через php://input
$input = json_decode(file_get_contents('php://input'), true);

if (!$input) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON']);
    exit;
}

$name  = $input['name']  ?? '';
$email = $input['email'] ?? '';

// Валидация
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    http_response_code(422);
    echo json_encode(['error' => 'Invalid email']);
    exit;
}

// Сохранение
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute([$name, $email]);

// Ответ
echo json_encode([
    'success' => true,
    'id' => $pdo->lastInsertId(),
], JSON_UNESCAPED_UNICODE);

Ключевые правила

  • PHP всегда шлёт Content-Type: application/json первой строкой — иначе браузер посчитает ответ HTML
  • JSON приходит через php://input, не через $_POST
  • Статусы: 200 OK, 400 плохой запрос, 401 нет авторизации, 403 доступ запрещён, 404 не найдено, 422 ошибка валидации, 500 ошибка сервера
  • Всегда exit после echo json_encode — чтобы не примешать случайный текст ниже
  • JSON_UNESCAPED_UNICODE — чтобы русские буквы не превращались в А
jQuery AJAX → PHP

Вариант 1 — form-urlencoded (проще, PHP читает $_POST)

$.ajax({
    url: '/api/save.php',
    method: 'POST',
    data: { name: 'Alice', email: 'a@x.kz' },
    // contentType не указан → jQuery шлёт application/x-www-form-urlencoded
    dataType: 'json',                    // ждём JSON в ответ
    success: function(res) {
        console.log(res);
    },
    error: function(jqXHR) {
        console.error(jqXHR.responseJSON);
    },
});
<?php
// save.php — данные УЖЕ в $_POST
header('Content-Type: application/json');
$name  = $_POST['name']  ?? '';
$email = $_POST['email'] ?? '';
echo json_encode(['ok' => true, 'received' => compact('name', 'email')]);

Вариант 2 — JSON body (нужно php://input в PHP)

$.ajax({
    url: '/api/save.php',
    method: 'POST',
    contentType: 'application/json',          // ← ЯВНО указываем что шлём JSON
    data: JSON.stringify({ name: 'Alice' }),  // ← ОБЯЗАТЕЛЬНО stringify
    dataType: 'json',
});
Частая ошибка: указали contentType: 'application/json', но забыли JSON.stringify — jQuery отправит [object Object]. Оба параметра идут в паре.

Отправка формы через jQuery — самое короткое

$('#user-form').on('submit', function(e) {
    e.preventDefault();

    $.ajax({
        url: $(this).attr('action'),
        method: $(this).attr('method'),
        data: $(this).serialize(),   // "name=Alice&email=a@x.kz"
        dataType: 'json',
    })
    .done(res => console.log('OK', res))
    .fail(err => console.error('FAIL', err));
});
Загрузка файлов: JS/jQuery → PHP

Vanilla JS — FormData

<input type="file" id="avatar">
<button id="upload">Upload</button>

<script>
document.querySelector('#upload').addEventListener('click', async () => {
    const file = document.querySelector('#avatar').files[0];
    if (!file) return;

    const fd = new FormData();
    fd.append('avatar', file);
    fd.append('user_id', 42);

    const res = await fetch('/upload.php', {
        method: 'POST',
        body: fd,     // Content-Type НЕ указываем — браузер сам поставит multipart/form-data с boundary
    });
    console.log(await res.json());
});
</script>

jQuery — тоже FormData

const fd = new FormData();
fd.append('avatar', $('#avatar')[0].files[0]);

$.ajax({
    url: '/upload.php',
    method: 'POST',
    data: fd,
    processData: false,    // иначе jQuery попробует преобразовать FormData в query string
    contentType: false,    // иначе jQuery поставит свой, а нужен multipart с boundary
    success: function(res) { console.log(res); },
});

upload.php

<?php
header('Content-Type: application/json');

// $_FILES — заполняется автоматически при multipart/form-data
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    echo json_encode(['error' => 'Upload failed']);
    exit;
}

$file = $_FILES['avatar'];
// Структура $_FILES['avatar']:
// name        — оригинальное имя
// type        — MIME (не доверять, определять серверно)
// size        — размер в байтах
// tmp_name    — временный путь
// error       — код ошибки (UPLOAD_ERR_OK = 0)

// Валидация размера
if ($file['size'] > 5 * 1024 * 1024) {
    http_response_code(413);
    echo json_encode(['error' => 'File too large']);
    exit;
}

// Валидация типа — по СОДЕРЖИМОМУ, не по $file['type']
$mime = mime_content_type($file['tmp_name']);
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
    http_response_code(415);
    echo json_encode(['error' => 'Only JPG/PNG/WebP']);
    exit;
}

// Безопасное имя (нельзя доверять оригинальному имени!)
$ext  = pathinfo($file['name'], PATHINFO_EXTENSION);
$name = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = __DIR__ . '/uploads/' . $name;

// Переместить из tmp в постоянное место
move_uploaded_file($file['tmp_name'], $dest);

echo json_encode(['url' => '/uploads/' . $name]);

Лимиты в php.ini

ДирективаЧтоДефолт
upload_max_filesizeМакс размер одного файла2M
post_max_sizeОбщий размер POST-запроса (должен быть > upload_max_filesize)8M
max_file_uploadsСколько файлов за один запрос20
file_uploadsРазрешена ли загрузка вообщеOn
PHP → JS (передача данных из бэка во фронт)

Способ 1 — inline через <?= json_encode() ?>

Самый частый способ — сгенерировать JS-переменную прямо в HTML.

<?php
$user = ['id' => 42, 'name' => 'Alice', 'is_admin' => true];
?>

<script>
    // json_encode превращает PHP-массив в JS-объект
    const CURRENT_USER = <?= json_encode($user, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
    console.log(CURRENT_USER.name);   // 'Alice'
</script>
XSS-риск: если $user содержит строку типа </script><script>alert(1)</script> — она разорвёт твой скрипт.
Флаги JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT экранируют опасные символы.

Способ 2 — data-атрибуты HTML

Данные привязаны к конкретному DOM-элементу — чище, безопаснее.

<div
    id="user-card"
    data-user-id="<?= (int)$user['id'] ?>"
    data-user-name="<?= htmlspecialchars($user['name']) ?>"
>
    ...
</div>

<script>
    const card = document.querySelector('#user-card');
    const userId   = card.dataset.userId;
    const userName = card.dataset.userName;

    // jQuery
    const id = $('#user-card').data('user-id');
</script>

Способ 3 — отдельный JSON endpoint (лучший для SPA)

// current-user.php
<?php
header('Content-Type: application/json');
echo json_encode($_SESSION['user'] ?? null);
// index.html
<script>
    const user = await fetch('/current-user.php').then(r => r.json());
</script>
CSRF без Laravel — своими руками

Laravel даёт CSRF из коробки. На чистом PHP делаем сами через session.

1. Генерация токена + вставка в HTML

<?php
session_start();

// Токен на всю сессию (или регенерить на каждый запрос — параноидальнее)
if (empty($_SESSION['csrf'])) {
    $_SESSION['csrf'] = bin2hex(random_bytes(32));
}
?>

<meta name="csrf-token" content="<?= $_SESSION['csrf'] ?>">

<form action="/save.php" method="POST">
    <input type="hidden" name="_csrf" value="<?= $_SESSION['csrf'] ?>">
    ...
</form>

2. Проверка в PHP-обработчике

<?php
session_start();

// Токен может прийти либо из формы, либо из header (для AJAX)
$token = $_POST['_csrf']
    ?? $_SERVER['HTTP_X_CSRF_TOKEN']
    ?? '';

// hash_equals — защита от timing attack, НЕЛЬЗЯ использовать ===
if (!hash_equals($_SESSION['csrf'] ?? '', $token)) {
    http_response_code(419);
    exit('CSRF token mismatch');
}

// ... обработка

3. Frontend — отправка токена

// jQuery — глобально для всех AJAX
$.ajaxSetup({
    headers: {
        'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
    },
});

// Vanilla JS — руками для каждого fetch
const csrf = document.querySelector('meta[name="csrf-token"]').content;
fetch('/save.php', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrf,
    },
    body: JSON.stringify(data),
});
Ключевое:
  • Токен хранится в $_SESSION — недоступен для JS с другого домена
  • При каждом запросе фронт шлёт токен → PHP сверяет с сессией
  • Атакующий с другого сайта не может узнать токен → не сможет подделать запрос
  • Сравнение через hash_equals(), не через === — защита от timing attack
  • Cookie сессии — обязательно с httponly=true, samesite=Lax/Strict
Интеграция: jQuery AJAX ↔ Laravel

Полный поток — форма логина

1. Frontend (blade + jQuery)

<meta name="csrf-token" content="{{ csrf_token() }}">

<form id="login-form">
    <input name="email" type="email">
    <input name="password" type="password">
    <button type="submit">Login</button>
    <div id="errors"></div>
</form>

<script>
$(function() {
    $.ajaxSetup({                                        // Один раз глобально
        headers: {
            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
        },
    });

    $('#login-form').on('submit', function(e) {
        e.preventDefault();
        $('#errors').empty();

        $.ajax({
            url: '/api/login',
            method: 'POST',
            data: $(this).serialize(),
            dataType: 'json',
            success: function(res) {
                window.location.href = '/dashboard';
            },
            error: function(jqXHR) {
                if (jqXHR.status === 422) {                    // Laravel validation errors
                    const errors = jqXHR.responseJSON.errors;
                    $.each(errors, function(field, msgs) {
                        $('#errors').append(`<p>${msgs[0]}</p>`);
                    });
                } else {
                    alert('Ошибка: ' + jqXHR.status);
                }
            },
        });
    });
});
</script>

2. Backend (Laravel Controller)

public function login(LoginRequest $request): JsonResponse
{
    if (! Auth::attempt($request->validated())) {
        return response()->json([
            'message' => 'Invalid credentials',
        ], 401);
    }

    $request->session()->regenerate();

    return response()->json([
        'user' => Auth::user(),
        'redirect' => '/dashboard',
    ]);
}

public function rules(): array
{
    return [
        'email' => 'required|email',
        'password' => 'required|min:8',
    ];
}
CSRF token в AJAX (Laravel)

Laravel по умолчанию требует CSRF-токен для всех POST/PUT/DELETE. Если не отправишь — 419 Page Expired.

3 способа передать CSRF

Способ 1 — meta тег + $.ajaxSetup (jQuery, глобально)

<!-- в <head> layout -->
<meta name="csrf-token" content="{{ csrf_token() }}">

// в JS — один раз при загрузке страницы
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
    },
});

// Все последующие $.ajax / $.post будут отправлять токен автоматически

Способ 2 — hidden input в форме (@csrf blade директива)

<form method="POST">
    @csrf
    <!-- Blade вставит: <input type="hidden" name="_token" value="..."> -->
    ...
</form>

// при $.serialize() — _token пойдёт вместе с формой

Способ 3 — vanilla fetch API

const csrf = document.querySelector('meta[name="csrf-token"]').content;

await fetch('/api/x', {
    method: 'POST',
    headers: {
        'X-CSRF-TOKEN': csrf,
        'Content-Type': 'application/json',
        'Accept': 'application/json',
    },
    body: JSON.stringify(data),
});
После длительного простоя — token может протухнуть (session expired). Правильно ловить 419 и делать window.location.reload() для получения нового.
Частые вопросы

15 базовых вопросов JS + jQuery

  1. Разница var / let / const → block vs function scope, hoisting, TDZ, реассигн.
  2. == vs === → strict сравнивает тип+значение, loose приводит типы. В production всегда ===.
  3. this в arrow vs обычной function → arrow берёт lexical this, обычная — зависит от вызова.
  4. Как работает event delegation → listener на родителе + e.target.matches(). В jQuery: $('#p').on('click', '.b', fn). Плюс: работает для новых элементов.
  5. Promise 3 состояния → pending / fulfilled / rejected. Один раз переходит и всё.
  6. async/await vs .then/.catch → сахар над Promise. Async всегда возвращает Promise. await разворачивает.
  7. fetch vs $.ajax → fetch native, jQuery — обёртка + автоопределение JSON. fetch не бросает на 4xx/5xx — надо res.ok.
  8. Разница textContent и innerHTML → text безопасен, HTML экранируется. innerHTML — риск XSS от user input.
  9. Как передать CSRF в Laravel через AJAX → meta[name=csrf-token] + X-CSRF-TOKEN header. Через $.ajaxSetup глобально.
  10. jQuery .click() vs .on('click', ...) → .click() только на существующие. .on() с делегацией — работает на новые.
  11. Как отправить форму AJAX'ом → e.preventDefault() + $(form).serialize() + $.ajax().
  12. Что даёт jQuery что в vanilla сложно → delegated events, chaining, cross-browser (тогда), animation shortcuts.
  13. Truthy/Falsy → 6 falsy: false, 0, '', null, undefined, NaN. Всё остальное truthy (включая [] и {}).
  14. Destructuring + spread → извлечение полей, дефолты, переименование. Spread для copy/merge.
  15. Promise.all vs Promise.allSettled → all падает если один rejected. allSettled ждёт все, возвращает статусы.