JavaScript + jQuery — база
Базовые конструкции JS и jQuery. Advanced (React, TypeScript) в этот раздел не входит.
Раздел покрывает базовые конструкции 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 + addEventListener | DOM/Events |
| Promise / async-await | Async |
| fetch API / AJAX | Fetch |
jQuery $(...) selectors | jQuery |
| jQuery events + delegated | jQuery |
$.ajax / $.post | jQuery AJAX |
| CSRF token в Laravel AJAX | Интеграция |
Примитивы (7 типов) + Object
| Тип | Пример | typeof |
|---|---|---|
| number | 42, 3.14, NaN, Infinity | 'number' |
| bigint | 9007199254740992n | 'bigint' |
| string | 'hello', "a", `template ${x}` | 'string' |
| boolean | true / false | 'boolean' |
| null | null | 'object' ← классический баг |
| undefined | undefined | 'undefined' |
| symbol | Symbol('id') | 'symbol' |
| object | {}, [], new Date() | 'object' / 'function' для fn |
var / let / const — главное отличие
var | let | const | |
|---|---|---|---|
| Область видимости | Функция (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 | Заново объявляют переменную с тем же именем в том же scope | let x = 1; let x = 2; |
| Мутация | mutation | Меняют содержимое объекта или массива, не трогая саму переменную | const a = [1]; a.push(2); |
| Затенение | shadowing | Объявление с тем же именем во вложенном scope | let 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 (!)
[] falsy. В JS [] truthy. Часто путают.Что это и зачем
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 не в том, поднимается ли объявление, а в том, инициализировано ли оно к этому моменту.
var | let / const | |
|---|---|---|
| Объявление поднимается | Да | Да |
| Значение до строки объявления | undefined | Переменная в TDZ |
| Обращение раньше объявления | Тихо вернёт undefined | ReferenceError |
| Куда поднимается в блоке внутри функции | До начала функции | До начала блока |
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.
var живёт в функции, let и const — в блоке. Одинаковое имя во вложенном scope — затенение, а не переобъявление.Ключевое отличие: == 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
?? и ?-> — эти концепции одинаково работают.Тернарный + 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 + 3 | 8 |
- | Вычитание | 5 - 3 | 2 |
* | Умножение | 5 * 3 | 15 |
/ | Деление | 10 / 3 | 3.333... |
% | Остаток от деления | 10 % 3 | 1 |
** | Возведение в степень | 2 ** 8 | 256 |
++ | Инкремент (+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 |
Логические
| Оператор | Значение | Пример |
|---|---|---|
&& | И — все true | a && b |
|| | ИЛИ — хотя бы один true | a || b |
! | НЕ — инверсия | !true → false |
?? | Nullish — дефолт только для null/undefined | val ?? "def" |
?. | Optional chaining — безопасный доступ | user?.profile?.name |
? : | Тернарный — короткий if/else | age > 18 ? "adult" : "minor" |
Присваивания
| Оператор | Эквивалент |
|---|---|
x = 5 | Простое присваивание |
x += 3 | x = x + 3 |
x -= 3 | x = x - 3 |
x *= 3 | x = x * 3 |
x /= 3 | x = x / 3 |
x %= 3 | x = x % 3 |
x **= 3 | x = x ** 3 |
x ||= y | x = x || y (если x falsy) |
x &&= y | x = x && y (если x truthy) |
x ??= y | x = 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 | Всегда возвращает undefined | void 0 → undefined |
Битовые (редко)
| Оператор | Значение |
|---|---|
& | Битовое И |
| | Битовое ИЛИ |
^ | Битовое XOR |
~ | Битовое НЕ |
<< | Сдвиг влево |
>> | Сдвиг вправо |
>>> | Сдвиг вправо без знака |
Зарезервированные слова языка. Строят структуру программы.
Объявление
| Слово | Что |
|---|---|
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 X | import X from 'mod' — без скобок, имя произвольное |
export const X = ... | import { X } from 'mod' — в скобках, имя должно совпадать |
| оба одновременно | import Def, { X, Y } from 'mod' |
| переименовать при импорте | import { X as MyX } from 'mod' |
| всё сразу как namespace | import * 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" проекте |
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.env | Vite (не стандарт) | Переменные из .env-файлов |
import.meta.hot | Vite / HMR | API для hot module replacement |
import.meta.glob | Vite | Массовый импорт файлов по glob-паттерну |
import.meta.resolve | Node.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.DEV | true в dev, false в prod |
import.meta.env.PROD | true в prod-сборке |
import.meta.env.BASE_URL | Base 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 | Не присвоено |
NaN | Not a Number |
this | Контекст вызова |
super | Родительский класс |
new.target | Был ли вызов через new |
debugger | Точка останова в DevTools |
Условия — стандартно
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);
}
break/continue/return НЕ работают как ожидаешь. Если нужен break — используй for...of или some()/find().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' });
Топ-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) | Первый совпавший элемент или undefined | array_filter + [0] |
arr.some(fn) | Есть ли ХОТЯ БЫ ОДИН по условию → bool | цикл |
arr.every(fn) | ВСЕ ли по условию → bool | цикл |
arr.includes(val) | Есть ли значение → bool | in_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, unshift | map, filter, slice |
splice, sort, reverse, fill | concat, 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 не тронут
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 |
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));
Поиск элементов
// По 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
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 (с потомками)
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 и держит основной сценарий слева, без лишних отступов.
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) |
DOMContentLoaded | DOM готов (аналог $(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);
}
});
// Плюс: работает для НОВЫХ элементов добавленных после — не нужно перепривязывать
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)]);
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();
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 — текстовый формат обмена данными. Два глобальных метода в JS: JSON.parse и JSON.stringify.
Формат JSON — что можно и нельзя
| Можно | Нельзя |
|---|---|
| string, number, boolean, null | undefined, 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
Библиотека JS созданная в 2006 (John Resig). Основная цель — устранить кросс-браузерные несовместимости (IE6-8) и упростить DOM-операции. Один глобальный объект $ (или jQuery) даёт удобное API.
Актуальность в 2026
| Аспект | Статус |
|---|---|
| Новые проекты | Не используют — vanilla JS + fetch API покрывают всё |
| Legacy проекты | Активно поддерживаются, замена стоит миллионы |
| WordPress и другие CMS | Встроены — при работе с плагинами jQuery неизбежен |
Как подключить
<!-- CDN — стандартный способ подключить jQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- Проверка версии -->
<script>
console.log($.fn.jquery); // "3.7.1"
</script>
$() = 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)
Текст / 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();
// Классы
$('.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();
$(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-подгрузке).
$.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. Полезно знать оба, чтобы переводить legacy код в современный.
| Задача | jQuery | Vanilla 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 => ...) |
- Delegated events в одну строку (в vanilla нужно писать вручную)
- Кросс-браузерность для древних IE (сейчас не актуально)
- Chaining —
.addClass().attr().fadeIn() - Анимации из коробки —
fadeIn,slideDown - Плагины — тысячи готовых компонентов (slider, datepicker и т.п.)
Самый базовый способ: 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 — куда попадают данные
| Массив | Источник |
|---|---|
$_GET | Query string (?name=x) — для method="GET" |
$_POST | Тело POST-запроса (form-urlencoded или multipart) |
$_FILES | Загруженные файлы через <input type="file"> |
$_REQUEST | GET + POST + COOKIE (не надёжно, лучше явно) |
$_COOKIE | Cookie от браузера |
$_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.
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— чтобы русские буквы не превращались вА
Вариант 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));
});
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 |
Способ 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>
$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>
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
Полный поток — форма логина
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',
];
}
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),
});
window.location.reload() для получения нового.
15 базовых вопросов JS + jQuery
- Разница
var/let/const→ block vs function scope, hoisting, TDZ, реассигн. ==vs===→ strict сравнивает тип+значение, loose приводит типы. В production всегда===.thisв arrow vs обычной function → arrow берёт lexicalthis, обычная — зависит от вызова.- Как работает event delegation → listener на родителе +
e.target.matches(). В jQuery:$('#p').on('click', '.b', fn). Плюс: работает для новых элементов. - Promise 3 состояния → pending / fulfilled / rejected. Один раз переходит и всё.
- async/await vs .then/.catch → сахар над Promise. Async всегда возвращает Promise. await разворачивает.
- fetch vs $.ajax → fetch native, jQuery — обёртка + автоопределение JSON. fetch не бросает на 4xx/5xx — надо
res.ok. - Разница
textContentиinnerHTML→ text безопасен, HTML экранируется. innerHTML — риск XSS от user input. - Как передать CSRF в Laravel через AJAX → meta[name=csrf-token] +
X-CSRF-TOKENheader. Через$.ajaxSetupглобально. - jQuery
.click()vs.on('click', ...)→.click()только на существующие..on()с делегацией — работает на новые. - Как отправить форму AJAX'ом →
e.preventDefault()+$(form).serialize()+$.ajax(). - Что даёт jQuery что в vanilla сложно → delegated events, chaining, cross-browser (тогда), animation shortcuts.
- Truthy/Falsy → 6 falsy: false, 0, '', null, undefined, NaN. Всё остальное truthy (включая
[]и{}). - Destructuring + spread → извлечение полей, дефолты, переименование. Spread для copy/merge.
- Promise.all vs Promise.allSettled → all падает если один rejected. allSettled ждёт все, возвращает статусы.