Примеры решений в JavaScript
1. Работа с массивами
1.1. Группировка элементов по ключу
function groupBy(array, keyFn) {
return array.reduce((acc, item) => {
const key = keyFn(item);
(acc[key] = acc[key] || []).push(item);
return acc;
}, {});
}
// Пример использования:
const users = [
{ name: 'Анна', age: 25, city: 'Москва' },
{ name: 'Борис', age: 32, city: 'Санкт-Петербург' },
{ name: 'Вера', age: 25, city: 'Москва' }
];
const byCity = groupBy(users, u => u.city);
// → {
// 'Москва': [{ name: 'Анна', … }, { name: 'Вера', … }],
// 'Санкт-Петербург': [{ name: 'Борис', … }]
// }
1.2. Удаление дубликатов (по значению или объекту)
// Для примитивов:
const uniqPrimitives = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3]
// Для объектов — по ключу:
const uniqByKey = (arr, key) =>
arr.filter((item, idx, self) =>
idx === self.findIndex(obj => obj[key] === item[key])
);
// Пример:
const items = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 1, name: 'C' } // дубль по id
];
uniqByKey(items, 'id');
// → [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
1.3. Плоское преобразование вложенных массивов с фильтрацией
function flattenAndFilter(arr, predicate = () => true) {
return arr.flat(Infinity).filter(predicate);
}
// Пример:
const nested = [1, [2, [3, 4]], [5, [6, 7, [8]]]];
flattenAndFilter(nested, x => x % 2 === 0); // [2, 4, 6, 8]
2. Работа с объектами
2.1. Глубокое копирование (без structuredClone, совместимо с циклическими ссылками)
function deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return seen.get(obj);
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Map) {
const result = new Map();
seen.set(obj, result);
for (const [k, v] of obj) {
result.set(k, deepClone(v, seen));
}
return result;
}
if (obj instanceof Set) {
const result = new Set();
seen.set(obj, result);
for (const v of obj) {
result.add(deepClone(v, seen));
}
return result;
}
const result = Array.isArray(obj) ? [] : {};
seen.set(obj, result);
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
result[key] = deepClone(obj[key], seen);
}
}
return result;
}
Примечание: для большинства случаев достаточно
structuredClone(obj), но оно не поддерживает функции и некоторые нестандартные объекты (например,Mapдо ES2024 — поддержка есть, но не во всех средах выполнения, например, старых Node.js). Приведённая реализация — fallback, совместимый с большинством runtime.
2.2. Слияние объектов с учётом вложенности (deep merge)
function deepMerge(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (source === null || typeof source !== 'object') return target;
Object.keys(source).forEach(key => {
const value = source[key];
if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof RegExp)) {
if (!target[key] || typeof target[key] !== 'object') target[key] = {};
deepMerge(target[key], value);
} else {
target[key] = value;
}
});
return deepMerge(target, ...sources);
}
// Пример:
const config = { server: { host: 'localhost', port: 3000 } };
const overrides = { server: { port: 8080 }, debug: true };
deepMerge({}, config, overrides);
// → { server: { host: 'localhost', port: 8080 }, debug: true }
3. Асинхронные операции
3.1. Параллельное выполнение с ограничением concurrency
async function asyncPool(concurrency, items, fn) {
const results = [];
const executing = new Set();
for (const item of items) {
const promise = Promise.resolve().then(() => fn(item));
results.push(promise);
const exec = promise.then(() => executing.delete(promise));
executing.add(exec);
if (executing.size >= concurrency) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// Пример: обработка 10 запросов пачками по 3
const urls = Array.from({ length: 10 }, (_, i) => `https://api.example.com/${i}`);
const responses = await asyncPool(3, urls, url => fetch(url).then(r => r.json()));
3.2. Таймаут с отменой (через AbortController)
function timeout(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(new DOMException('Aborted', 'AbortError'));
const timer = setTimeout(() => {
reject(new DOMException('Timeout', 'TimeoutError'));
}, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
// Использование:
const controller = new AbortController();
setTimeout(() => controller.abort(), 500);
try {
await Promise.race([
fetch('/slow-endpoint', { signal: controller.signal }),
timeout(1000, controller.signal)
]);
} catch (e) {
if (e.name === 'AbortError') console.log('Отменено');
else if (e.name === 'TimeoutError') console.log('Таймаут');
}
3.3. Повтор вызова с экспоненциальной задержкой (retry with backoff)
async function retry(fn, {
maxAttempts = 3,
delayMs = 100,
backoffFactor = 2,
jitter = true,
signal
} = {}) {
let lastError;
for (let i = 0; i < maxAttempts; i++) {
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
try {
return await fn();
} catch (err) {
lastError = err;
if (i === maxAttempts - 1) break;
const delay = delayMs * Math.pow(backoffFactor, i);
const jitterDelay = jitter ? delay * (0.5 + Math.random() * 0.5) : delay;
await new Promise(resolve => setTimeout(resolve, jitterDelay));
}
}
throw lastError;
}
// Пример:
await retry(() => fetch('/unstable-api').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}), { maxAttempts: 5, delayMs: 200 });
4. Обработка строк и регулярных выражений
4.1. Безопасное форматирование строк (аналог printf)
function format(template, ...args) {
return template.replace(/\{(\d+)\}/g, (match, idx) => {
const n = Number(idx);
return n < args.length ? args[n] : match;
});
}
// Пример:
format('Привет, {0}! Ваш баланс: {1} ₽.', 'Анна', 1500);
// → 'Привет, Анна! Ваш баланс: 1500 ₽.'
4.2. Экранирование HTML
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return str.replace(/[&<>"']/g, c => map[c]);
}
// Или через DOM (если доступен):
function escapeHtmlDOM(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
4.3. Простой парсер CSV (без зависимостей, устойчив к кавычкам)
function parseCSV(text, delimiter = ',') {
const lines = text.split(/\r?\n/).filter(line => line.trim() !== '');
const result = [];
const regex = new RegExp(
`(?:"([^"]*(?:""[^"]*)*)"|([^${delimiter}\\r\\n]*))(?:${delimiter}|$)`,
'g'
);
for (const line of lines) {
const row = [];
let match;
regex.lastIndex = 0;
while ((match = regex.exec(line)) !== null) {
row.push(match[1] ? match[1].replace(/""/g, '"') : match[2]);
}
result.push(row);
}
return result;
}
// Пример:
const csv = `name,age,quote
Анна,25,"Здравствуйте, ""мир""!"
Борис,30,Просто текст`;
parseCSV(csv);
// → [
// ['name', 'age', 'quote'],
// ['Анна', '25', 'Здравствуйте, "мир"!'],
// ['Борис', '30', 'Просто текст']
// ]
5. Валидация форм и данных
5.1. Композируемая валидация (функциональный стиль)
// Базовые валидаторы — чистые функции
const required = (value) => value != null && value !== '';
const minLength = (n) => (value) => typeof value === 'string' && value.length >= n;
const email = (value) =>
typeof value === 'string' &&
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
const matches = (field, getValue) => (value) =>
value === getValue(field);
// Композиция через `and`
const and = (...validators) => (value) =>
validators.every(v => v(value));
// Пример поля:
const passwordValidators = and(required, minLength(8));
const confirmPasswordValidators = and(
required,
matches('password', (field) => document.querySelector(`[name="${field}"]`).value)
);
// Общий движок валидации
function validateForm(formElement, schema) {
const errors = {};
const formData = new FormData(formElement);
for (const [name, validators] of Object.entries(schema)) {
const value = formData.get(name);
if (!validators(value)) {
errors[name] = true;
}
}
// Применение классов
for (const field of formElement.querySelectorAll('[name]')) {
const hasError = errors[field.name];
field.classList.toggle('is-invalid', hasError);
}
return Object.keys(errors).length === 0;
}
// Использование:
const formSchema = {
email: and(required, email),
password: passwordValidators,
confirmPassword: confirmPasswordValidators
};
document.getElementById('signupForm').addEventListener('submit', e => {
e.preventDefault();
if (validateForm(e.target, formSchema)) {
console.log('✅ Валидация пройдена');
}
});
5.2. Асинхронная валидация (например, проверка уникальности логина)
async function asyncValidate(fieldName, value, endpoint, signal) {
const response = await fetch(`${endpoint}?${fieldName}=${encodeURIComponent(value)}`, {
signal
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { available } = await response.json();
return available;
}
// Интеграция с debounce и abort
class AsyncFieldValidator {
#controller = null;
#lastPromise = null;
constructor(debounceMs = 300) {
this.debounceMs = debounceMs;
}
validate(fieldName, value, endpoint, onResult) {
this.#controller?.abort();
this.#controller = new AbortController();
clearTimeout(this.#timeoutId);
this.#timeoutId = setTimeout(async () => {
try {
const isValid = await asyncValidate(fieldName, value, endpoint, this.#controller.signal);
onResult(isValid);
} catch (e) {
if (e.name !== 'AbortError') onResult(null, e);
}
}, this.debounceMs);
}
destroy() {
this.#controller?.abort();
clearTimeout(this.#timeoutId);
}
}
// Пример:
const usernameValidator = new AsyncFieldValidator(500);
document.querySelector('#username').addEventListener('input', e => {
const val = e.target.value.trim();
if (val.length < 3) return;
usernameValidator.validate('username', val, '/api/check-username', (isValid, err) => {
const el = e.target;
el.setCustomValidity(isValid ? '' : 'Логин занят');
el.reportValidity();
});
});
6. Реактивность «с нуля»
6.1. Примитивный Signal (аналог Solid.js/React useSignal)
class Signal {
#value;
#subscribers = new Set();
constructor(initialValue) {
this.#value = initialValue;
}
get value() {
return this.#value;
}
set value(newValue) {
if (Object.is(this.#value, newValue)) return;
this.#value = newValue;
this.#notify();
}
#notify() {
for (const fn of this.#subscribers) fn();
}
subscribe(fn) {
this.#subscribers.add(fn);
return () => this.#subscribers.delete(fn);
}
}
// Пример использования:
const count = new Signal(0);
const unsubscribe = count.subscribe(() => {
document.getElementById('counter').textContent = count.value;
});
document.getElementById('inc').onclick = () => count.value++;
// unsubscribe() — при размонтировании
6.2. Вычисляемый сигнал (computed)
class ComputedSignal {
#fn;
#deps;
#cachedValue;
#dirty = true;
#subscribers = new Set();
constructor(fn, deps) {
this.#fn = fn;
this.#deps = deps;
this.#subscribeToDeps();
}
#subscribeToDeps() {
this.#deps.forEach(dep => {
dep.subscribe(() => {
this.#dirty = true;
this.#notify();
});
});
}
get value() {
if (this.#dirty) {
this.#cachedValue = this.#fn();
this.#dirty = false;
}
return this.#cachedValue;
}
#notify() {
for (const fn of this.#subscribers) fn();
}
subscribe(fn) {
this.#subscribers.add(fn);
return () => this.#subscribers.delete(fn);
}
}
// Пример:
const firstName = new Signal('Анна');
const lastName = new Signal('Иванова');
const fullName = new ComputedSignal(
() => `${firstName.value} ${lastName.value}`,
[firstName, lastName]
);
fullName.subscribe(() => {
console.log('ФИО:', fullName.value); // → «Анна Иванова»
});
lastName.value = 'Петрова'; // → «Анна Петрова»
Такой подход легко расширяется до batched-обновлений (через
queueMicrotask), lazy-вычислений, отмены подписок и интеграции с DOM (например,effect(() => { el.textContent = signal.value })).
7. Оптимизация DOM-операций
7.1. Виртуальный скролл (минималистично, без библиотек)
class VirtualScroller {
constructor(container, { itemHeight, itemCount, renderItem }) {
this.container = container;
this.itemHeight = itemHeight;
this.itemCount = itemCount;
this.renderItem = renderItem;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2;
this.scrollTop = 0;
container.style.overflow = 'auto';
container.style.position = 'relative';
container.innerHTML = `
<div style="height: ${itemCount * itemHeight}px; position: relative;"></div>
`;
this.content = container.firstElementChild;
container.addEventListener('scroll', () => {
this.scrollTop = container.scrollTop;
this.#update();
});
this.#update();
}
#update() {
const start = Math.max(0, Math.floor(this.scrollTop / this.itemHeight) - 1);
const end = Math.min(this.itemCount, start + this.visibleCount);
// Очистка: перерисовываем только изменившиеся
this.content.innerHTML = '';
for (let i = start; i < end; i++) {
const el = this.renderItem(i);
el.style.position = 'absolute';
el.style.top = `${i * this.itemHeight}px`;
el.style.width = '100%';
this.content.appendChild(el);
}
}
}
// Пример использования:
new VirtualScroller(document.getElementById('scroll'), {
itemHeight: 40,
itemCount: 100_000,
renderItem: (i) => {
const div = document.createElement('div');
div.className = 'item';
div.textContent = `Элемент #${i + 1}`;
return div;
}
});
7.2. Дебаунсированное обновление DOM (например, при ресайзе)
function debouncedDOMUpdate(fn, delay = 100) {
let frameId = null;
let timeoutId = null;
return (...args) => {
cancelAnimationFrame(frameId);
clearTimeout(timeoutId);
frameId = requestAnimationFrame(() => {
timeoutId = setTimeout(() => fn(...args), delay);
});
};
}
// Пример: адаптивная сетка
const updateLayout = debouncedDOMUpdate(() => {
const width = window.innerWidth;
document.body.className = width < 768 ? 'mobile' : width < 1024 ? 'tablet' : 'desktop';
});
window.addEventListener('resize', updateLayout);
8. Работа с датами и временем (без moment.js / date-fns)
8.1. Парсинг ISO 8601 с поддержкой часовых поясов (в т.ч. Z, +03:00)
function parseISO8601(isoString) {
// Поддержка: 2025-11-13T14:30:00Z, 2025-11-13T14:30:00+03:00, 2025-11-13T14:30:00.123+05:30
const re = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:Z|([+-]\d{2}):?(\d{2}))?$/;
const m = isoString.match(re);
if (!m) throw new Error('Invalid ISO 8601 string');
const [, y, mo, d, h, mi, s, ms = '0', tzSign, tzH, tzM] = m;
const date = new Date(Date.UTC(
y, mo - 1, d, h, mi, s, ms.padEnd(3, '0')
));
if (tzSign) {
const offset = (parseInt(tzH) * 60 + parseInt(tzM || 0)) * (tzSign === '-' ? -1 : 1);
date.setTime(date.getTime() - offset * 60 * 1000);
}
// Если нет Z/±, считаем как local — но так не рекомендуется; лучше требовать явный offset
return date;
}
// Пример:
parseISO8601('2025-11-13T14:30:00+03:00'); // → Date, корректно смещённая
8.2. Форматирование даты в локальном формате (без Intl, если нужна простая строка)
function formatDate(date, template = 'YYYY-MM-DD HH:mm:ss') {
const pad = (n) => String(n).padStart(2, '0');
const map = {
YYYY: date.getFullYear(),
YY: String(date.getFullYear()).slice(-2),
MM: pad(date.getMonth() + 1),
M: date.getMonth() + 1,
DD: pad(date.getDate()),
D: date.getDate(),
HH: pad(date.getHours()),
H: date.getHours(),
mm: pad(date.getMinutes()),
m: date.getMinutes(),
ss: pad(date.getSeconds()),
s: date.getSeconds()
};
return template.replace(/YYYY|YY|MM|M|DD|D|HH|H|mm|m|ss|s/g, match => map[match]);
}
// Пример:
formatDate(new Date(), 'DD.MM.YYYY HH:mm'); // → '13.11.2025 14:30'
Для продакшена рекомендуется
Intl.DateTimeFormat, т.к. он поддерживает локали, порядок полей, ам/пм и т.д. Приведённый пример — для случаев, когда требуется предсказуемый шаблон независимо от locale.
9. Web Workers и многопоточность
9.1. Обёртка для удобной работы с Worker
class WorkerPool {
constructor(workerScript, size = navigator.hardwareConcurrency || 4) {
this.workers = Array.from({ length: size }, () =>
new Worker(workerScript, { type: 'module' })
);
this.queue = [];
this.busy = new Set();
}
postMessage(data) {
return new Promise((resolve, reject) => {
this.queue.push({ data, resolve, reject });
this.#schedule();
});
}
#schedule() {
if (this.queue.length === 0 || this.busy.size >= this.workers.length) return;
const task = this.queue.shift();
const worker = this.workers.find(w => !this.busy.has(w));
if (!worker) return;
this.busy.add(worker);
const handler = (e) => {
worker.removeEventListener('message', handler);
worker.removeEventListener('error', errorHandler);
this.busy.delete(worker);
task.resolve(e.data);
this.#schedule();
};
const errorHandler = (e) => {
worker.removeEventListener('message', handler);
worker.removeEventListener('error', errorHandler);
this.busy.delete(worker);
task.reject(e);
this.#schedule();
};
worker.addEventListener('message', handler);
worker.addEventListener('error', errorHandler);
worker.postMessage(task.data);
}
terminate() {
this.workers.forEach(w => w.terminate());
}
}
// Пример worker.js:
// self.onmessage = (e) => {
// const result = heavyComputation(e.data);
// self.postMessage(result);
// };
// function heavyComputation(n) { … }
// Использование:
const pool = new WorkerPool('/worker.js', 4);
await pool.postMessage({ type: 'fib', n: 40 });
9.2. Обмен SharedArrayBuffer между потоками
// main.js
if (crossOriginIsolated) {
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
const worker = new Worker('/counter-worker.js');
worker.postMessage({ sab });
// Инкремент из основного потока
Atomics.add(view, 0, 1);
console.log('Main incremented →', Atomics.load(view, 0));
}
// counter-worker.js
self.onmessage = (e) => {
const { sab } = e.data;
const view = new Int32Array(sab);
// Инкремент в worker'е
Atomics.add(view, 0, 10);
console.log('Worker incremented →', Atomics.load(view, 0));
};
Требует:
- заголовков
Cross-Origin-Embedder-Policy: require-corp+Cross-Origin-Opener-Policy: same-origin,crossOriginIsolated === trueв основном потоке.
Без этогоSharedArrayBufferнедоступен (защита от Spectre).
10. Proxy: отладка, валидация, моки
10.1. Логирующий прокси (для отладки доступов к объекту)
function createLoggingProxy(target, name = 'target') {
return new Proxy(target, {
get(obj, prop, receiver) {
console.debug(`[GET] ${name}.${String(prop)}`);
return Reflect.get(obj, prop, receiver);
},
set(obj, prop, value, receiver) {
console.debug(`[SET] ${name}.${String(prop)} =`, value);
return Reflect.set(obj, prop, value, receiver);
},
has(obj, prop) {
console.debug(`[IN] ${name} has ${String(prop)}?`);
return Reflect.has(obj, prop);
},
deleteProperty(obj, prop) {
console.debug(`[DELETE] ${name}.${String(prop)}`);
return Reflect.deleteProperty(obj, prop);
}
});
}
// Пример:
const config = createLoggingProxy({ debug: false, timeout: 5000 }, 'config');
config.debug = true; // → [SET] config.debug = true
console.log(config.timeout); // → [GET] config.timeout, затем 5000
10.2. Валидирующий прокси (строгая типизация «на лету»)
function createValidatedProxy(schema) {
const target = {};
return new Proxy(target, {
set(obj, prop, value) {
if (!schema.hasOwnProperty(prop)) {
throw new TypeError(`Свойство "${String(prop)}" не объявлено в схеме`);
}
const validator = schema[prop];
if (typeof validator === 'function') {
if (!validator(value)) {
throw new TypeError(`Некорректное значение для "${String(prop)}": ${value}`);
}
} else if (value.constructor !== validator) {
throw new TypeError(
`Ожидался тип ${validator.name}, получен ${value.constructor.name}`
);
}
return Reflect.set(obj, prop, value);
},
get(obj, prop) {
if (prop === '__schema') return schema;
return Reflect.get(obj, prop);
}
});
}
// Пример:
const user = createValidatedProxy({
id: Number,
name: (v) => typeof v === 'string' && v.length >= 2,
email: (v) => typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
});
user.id = 123; // ✅
user.name = 'А'; // ❌ TypeError: Некорректное значение для "name": А
10.3. Мок-объект для инъекции зависимостей (unit-тесты)
function createMock(spec = {}) {
const handlers = {};
const target = {};
// Поддержка методов
Object.keys(spec).forEach(key => {
if (typeof spec[key] === 'function') {
target[key] = function (...args) {
const h = handlers[key] || spec[key];
return h.call(this, ...args);
};
} else {
target[key] = spec[key];
}
});
return {
proxy: new Proxy(target, {}),
when(methodName) {
return {
resolve: (value) => {
handlers[methodName] = () => Promise.resolve(value);
return this;
},
reject: (reason) => {
handlers[methodName] = () => Promise.reject(reason);
return this;
},
impl: (fn) => {
handlers[methodName] = fn;
return this;
}
};
},
reset() {
Object.keys(handlers).forEach(k => delete handlers[k]);
}
};
}
// Пример:
const apiMock = createMock({
fetchUser: () => Promise.reject(new Error('not mocked'))
});
apiMock.when('fetchUser').resolve({ id: 1, name: 'Тест' });
apiMock.proxy.fetchUser().then(u => console.log(u.name)); // → 'Тест'
11. Кастомные хуки (React-совместимые, но без зависимости от React)
Эти реализации демонстрируют принципы, а не замену библиотек. Их можно использовать в собственных фреймворках или для обучения.
11.1. useDebounce
function useDebounce(value, delay) {
const ref = { current: value };
const timeoutRef = { current: null };
return new Promise(resolve => {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
ref.current = value;
resolve(value);
}, delay);
}).finally(() => {
// Возвращаем актуальное значение по требованию
return ref.current;
});
}
// Или в стиле hook (с подпиской):
function createDebounceHook() {
let timeoutId = null;
let resolveQueue = [];
return function debounce(value, delay) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
resolveQueue.forEach(r => r(value));
resolveQueue = [];
}, delay);
return new Promise(resolve => resolveQueue.push(resolve));
};
}
11.2. useIntersectionObserver
function createIntersectionObserver({ root = null, rootMargin = '0px', threshold = 0 } = {}) {
const callbacks = new Map();
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const cb = callbacks.get(entry.target);
if (cb) cb(entry);
});
}, { root, rootMargin, threshold });
return {
observe(element, callback) {
callbacks.set(element, callback);
observer.observe(element);
},
unobserve(element) {
callbacks.delete(element);
observer.unobserve(element);
},
disconnect() {
callbacks.clear();
observer.disconnect();
}
};
}
// Пример: lazy-load изображений
const io = createIntersectionObserver({ threshold: 0.1 });
document.querySelectorAll('img[data-src]').forEach(img => {
io.observe(img, entry => {
if (entry.isIntersecting) {
img.src = img.dataset.src;
delete img.dataset.src;
io.unobserve(img);
}
});
});
11.3. useAsync — управление состоянием асинхронной операции
class AsyncState {
constructor() {
this.status = 'idle'; // idle | pending | fulfilled | rejected
this.data = null;
this.error = null;
this.subscribers = new Set();
}
setState(updates) {
Object.assign(this, updates);
this.subscribers.forEach(cb => cb());
}
subscribe(cb) {
this.subscribers.add(cb);
return () => this.subscribers.delete(cb);
}
}
function useAsync(fn, deps = []) {
const state = new AsyncState();
const execute = async (...args) => {
state.setState({ status: 'pending', error: null });
try {
const data = await fn(...args);
state.setState({ status: 'fulfilled', data });
} catch (err) {
state.setState({ status: 'rejected', error: err });
}
};
return { ...state, execute };
}
// Пример:
const { execute, status, data, error } = useAsync(() => fetch('/api').then(r => r.json()));
execute();
// → status: 'pending' → 'fulfilled', data заполнен
12. Генерация и трансформация кода: работа с AST
12.1. Парсинг и модификация через acorn (ESM-совместимо)
Предполагается, что
acornиestree-walkerподключены как зависимости (npm i acorn estree-walker).
Ниже — пример без внешних зависимостей не получится, так как AST — сложная структура.
// Пример: замена всех `console.log` на `debugger`
import * as acorn from 'acorn';
import { walk } from 'estree-walker';
function replaceConsoleLogWithDebugger(code) {
const ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' });
walk(ast, {
enter(node) {
if (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'console' &&
node.callee.property.name === 'log'
) {
this.replace({
type: 'DebuggerStatement'
});
}
}
});
return generateCode(ast); // упрощённая генерация ниже
}
// Минималистичный кодогенератор (только для демонстрации)
function generateCode(node) {
if (node.type === 'Program') {
return node.body.map(generateCode).join('\n');
}
if (node.type === 'ExpressionStatement') {
return generateCode(node.expression) + ';';
}
if (node.type === 'CallExpression') {
return generateCode(node.callee) + '(' + node.arguments.map(generateCode).join(', ') + ')';
}
if (node.type === 'MemberExpression') {
return generateCode(node.object) + '.' + generateCode(node.property);
}
if (node.type === 'Identifier') {
return node.name;
}
if (node.type === 'Literal') {
return JSON.stringify(node.value);
}
if (node.type === 'DebuggerStatement') {
return 'debugger';
}
throw new Error(`Unsupported node: ${node.type}`);
}
// Пример:
const input = `console.log("start"); f(); console.log("end");`;
replaceConsoleLogWithDebugger(input);
// → 'debugger;\nf();\ndebugger;'
В продакшене используйте
@babel/core,recast, илиastringдля надёжной генерации.
13. Мини-фреймворк для unit-тестов
13.1. Core: describe, it, expect
const testSuite = {
suites: [],
currentSuite: null,
results: { passed: 0, failed: 0, errors: [] }
};
function describe(name, fn) {
const suite = { name, tests: [] };
const parent = testSuite.currentSuite;
testSuite.currentSuite = suite;
fn();
testSuite.currentSuite = parent;
(parent ? parent.tests : testSuite.suites).push(suite);
}
function it(name, fn) {
if (!testSuite.currentSuite) throw new Error('it() must be inside describe()');
testSuite.currentSuite.tests.push({ name, fn });
}
function expect(actual) {
return {
toBe(expected) {
if (actual !== expected) throw new Error(`Expected ${expected}, got ${actual}`);
},
toEqual(expected) {
const isEqual = (a, b) => {
if (a === b) return true;
if (typeof a !== 'object' || typeof b !== 'object' || a == null || b == null) return false;
const aKeys = Object.keys(a), bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every(k => isEqual(a[k], b[k]));
};
if (!isEqual(actual, expected)) throw new Error(`Objects not equal`);
},
toThrow() {
if (typeof actual !== 'function') throw new Error('toThrow() expects a function');
try {
actual();
throw new Error('Expected function to throw');
} catch (e) {
if (e.message === 'Expected function to throw') throw e;
}
}
};
}
// Запуск
async function runTests() {
console.log('🧪 Запуск тестов...\n');
async function runSuite(suite, indent = '') {
console.log(`${indent}ᐅ ${suite.name}`);
for (const test of suite.tests) {
if (test.tests) {
await runSuite(test, indent + ' ');
continue;
}
try {
if (test.fn.constructor.name === 'AsyncFunction') {
await test.fn();
} else {
test.fn();
}
testSuite.results.passed++;
console.log(`${indent} ✅ ${test.name}`);
} catch (e) {
testSuite.results.failed++;
testSuite.results.errors.push({ suite: suite.name, test: test.name, error: e });
console.log(`${indent} ❌ ${test.name} — ${e.message}`);
}
}
}
for (const suite of testSuite.suites) {
await runSuite(suite);
}
const { passed, failed } = testSuite.results;
console.log(`\n✅ ${passed} прошло, ❌ ${failed} упало`);
if (failed > 0) process?.exit?.(1);
}
// Пример:
describe('Math', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
it('deep equality', () => {
expect({ a: 1, b: [2] }).toEqual({ a: 1, b: [2] });
});
it('throws on invalid input', () => {
expect(() => JSON.parse('')).toThrow();
});
});
runTests();
14. Реактивный кэш поверх localStorage
14.1. ReactiveStorage — синхронизация между вкладками через storage
class ReactiveStorage {
#key;
#defaultValue;
#subscribers = new Set();
#cleanup;
constructor(key, defaultValue = null) {
this.#key = key;
this.#defaultValue = defaultValue;
// Слушатель изменений из других вкладок
this.#cleanup = () => {
window.removeEventListener('storage', this.#onStorage);
};
window.addEventListener('storage', this.#onStorage = (e) => {
if (e.key === this.#key) {
this.#notify();
}
});
}
get value() {
const raw = localStorage.getItem(this.#key);
return raw === null ? this.#defaultValue : JSON.parse(raw);
}
set value(newValue) {
localStorage.setItem(this.#key, JSON.stringify(newValue));
this.#notify(); // локальное обновление
}
#notify() {
for (const fn of this.#subscribers) fn(this.value);
}
subscribe(fn) {
fn(this.value); // сразу вызываем с текущим значением
this.#subscribers.add(fn);
return () => this.#subscribers.delete(fn);
}
destroy() {
this.#cleanup();
this.#subscribers.clear();
}
}
// Пример: тема интерфейса
const theme = new ReactiveStorage('ui.theme', 'light');
theme.subscribe((val) => {
document.documentElement.setAttribute('data-theme', val);
});
// Переключение:
document.getElementById('theme-toggle').onclick = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light';
};
// → изменение применится во всех вкладках мгновенно
14.2. Расширение до IndexedDB с автоматической сериализацией
class ReactiveDB {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.#initPromise = this.#initDB();
}
async #initDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(this.dbName, this.version);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains('store')) {
db.createObjectStore('store');
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async set(key, value) {
const db = await this.#initPromise;
const tx = db.transaction('store', 'readwrite');
const store = tx.objectStore('store');
store.put(value, key);
return new Promise((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
async get(key, defaultValue = null) {
const db = await this.#initPromise;
const tx = db.transaction('store', 'readonly');
const store = tx.objectStore('store');
const req = store.get(key);
return new Promise((resolve) => {
req.onsuccess = () => resolve(req.result ?? defaultValue);
});
}
async subscribe(key, callback, defaultValue = null) {
const currentValue = await this.get(key, defaultValue);
callback(currentValue);
// Слушаем изменения — можно через broadcast-channel или shared worker в реальных сценариях
// Здесь — упрощённо: публикация через событие
const handler = (e) => {
if (e.detail?.key === key) {
callback(e.detail.value);
}
};
window.addEventListener('reactive-db-update', handler);
return () => window.removeEventListener('reactive-db-update', handler);
}
async publish(key, value) {
await this.set(key, value);
window.dispatchEvent(new CustomEvent('reactive-db-update', {
detail: { key, value }
}));
}
}
15. Паттерн «Команда» с undo/redo
15.1. Базовая реализация (Command, Invoker, History)
class CommandHistory {
constructor() {
this.done = [];
this.undone = [];
}
execute(command) {
command.execute();
this.done.push(command);
this.undone.length = 0; // сброс redo при новом действии
}
canUndo() { return this.done.length > 0; }
canRedo() { return this.undone.length > 0; }
undo() {
if (!this.canUndo()) return false;
const command = this.done.pop();
command.undo();
this.undone.push(command);
return true;
}
redo() {
if (!this.canRedo()) return false;
const command = this.undone.pop();
command.execute();
this.done.push(command);
return true;
}
}
// Пример: редактор текста
class TextEditCommand {
constructor(editor, newText, oldText, cursorPos) {
this.editor = editor;
this.newText = newText;
this.oldText = oldText;
this.cursorPos = cursorPos;
}
execute() {
this.editor.setText(this.newText);
this.editor.setCursor(this.cursorPos);
}
undo() {
this.editor.setText(this.oldText);
this.editor.setCursor(this.cursorPos);
}
}
// Инвокер (менеджер)
class TextEditor {
constructor(element) {
this.element = element;
this.history = new CommandHistory();
this.#setupListeners();
}
setText(text) {
this.element.value = text;
}
getText() {
return this.element.value;
}
setCursor(pos) {
this.element.setSelectionRange(pos, pos);
}
getCursor() {
return this.element.selectionStart;
}
#setupListeners() {
let lastText = this.getText();
let lastCursor = this.getCursor();
this.element.addEventListener('input', () => {
const newText = this.getText();
const newCursor = this.getCursor();
const cmd = new TextEditCommand(this, newText, lastText, newCursor);
this.history.execute(cmd);
lastText = newText;
lastCursor = newCursor;
});
}
undo() { this.history.undo(); }
redo() { this.history.redo(); }
}
// Пример использования:
const editor = new TextEditor(document.getElementById('textarea'));
document.getElementById('undo').onclick = () => editor.undo();
document.getElementById('redo').onclick = () => editor.redo();
Примечание: этот подход масштабируется на любые действия — перемещение элементов, изменение состояния, вызов API с обратной операцией.
16. Конечный автомат (Finite State Machine, FSM)
16.1. Минималистичный FSM для управления UI-состоянием
class FSM {
constructor(initialState, transitions = {}) {
this.state = initialState;
this.transitions = transitions;
this.listeners = new Set();
}
can(event) {
const trans = this.transitions[this.state];
return trans && trans[event] !== undefined;
}
send(event, payload) {
if (!this.can(event)) {
console.warn(`[FSM] Недопустимое событие "${event}" в состоянии "${this.state}"`);
return false;
}
const nextState = this.transitions[this.state][event];
const prevState = this.state;
this.state = typeof nextState === 'function' ? nextState(payload) : nextState;
this.listeners.forEach(fn => fn({ event, payload, prevState, nextState: this.state }));
return true;
}
subscribe(fn) {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
}
// Пример: загрузка данных
const dataLoader = new FSM('idle', {
idle: {
FETCH: 'loading'
},
loading: {
SUCCESS: 'loaded',
ERROR: (err) => ({ name: 'error', error: err.message })
},
loaded: {
REFETCH: 'loading'
},
error: {
RETRY: 'loading'
}
});
// Связь с UI
dataLoader.subscribe(({ state, event }) => {
const status = typeof state === 'object' ? state.name : state;
document.getElementById('status').textContent = `Статус: ${status}`;
if (status === 'error') {
document.getElementById('error-msg').textContent = state.error;
}
});
// Использование:
document.getElementById('load-btn').onclick = async () => {
dataLoader.send('FETCH');
try {
const res = await fetch('/data');
if (res.ok) {
dataLoader.send('SUCCESS', await res.json());
} else {
throw new Error(`HTTP ${res.status}`);
}
} catch (e) {
dataLoader.send('ERROR', e);
}
};
Преимущества:
- явное описание допустимых переходов,
- исключение «незаконных» состояний,
- простая сериализация (
JSON.stringify({ state: fsm.state })),- тестируемость (можно прогнать сценарии событий).
17. Компиляция шаблонных строк в функции (runtime-рендеринг)
17.1. Простой шаблонизатор без eval (на основе Function)
function compileTemplate(template) {
// Замена {{expr}} → $1, экранирование
const sanitized = template
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
const fnBody = `
return "${sanitized}"
.replace(/\\{\\{([^}]+)\\}\\}/g, (match, expr) => {
try {
return (0, eval)(expr.trim());
} catch (e) {
return \`[Error in \${expr}: \${e.message}]\`;
}
});
`;
// Безопасная обёртка: запрещаем доступ к глобальным (кроме Math, Date и т.п.)
return new Function('scope', `
with (scope || {}) {
${fnBody}
}
`);
}
// Пример:
const renderUser = compileTemplate('Привет, {{name}}! Возраст: {{age}}, через 5 лет: {{age + 5}}');
renderUser({ name: 'Анна', age: 25 });
// → 'Привет, Анна! Возраст: 25, через 5 лет: 30'
Важно:
withиevalсчитаются небезопасными в открытых веб-приложениях. Этот подход допустим только если шаблон контролируется разработчиком (не вводится пользователем). Для пользовательских шаблонов используйте парсер с AST и строгой белой list’ой разрешённых выражений.
17.2. Безопасная версия (разрешённые выражения: только свойства и арифметика)
function safeCompile(template) {
const exprRegex = /\{\{([^}]+)\}\}/g;
const parts = [];
let lastIndex = 0;
let match;
while ((match = exprRegex.exec(template)) !== null) {
parts.push(JSON.stringify(template.slice(lastIndex, match.index)));
const expr = match[1].trim();
// Проверка: только идентификаторы, точки, [] (ограниченно), + - * / ()
if (!/^[\w\[\].()\s+\-/*]+$/.test(expr)) {
throw new Error(`Недопустимое выражение: ${expr}`);
}
parts.push(`(${expr})`);
lastIndex = exprRegex.lastIndex;
}
parts.push(JSON.stringify(template.slice(lastIndex)));
const body = `return [${parts.join(', ')}].join('');`;
return new Function('data', body);
}
// Пример:
const safeRender = safeCompile('ID: {{user.id}}, сумма: {{a + b * 2}}');
safeRender({ user: { id: 101 }, a: 5, b: 3 }); // → 'ID: 101, сумма: 11'
18. Измерение производительности и сбор метрик
18.1. Кастомная метрика: время выполнения функции + маркировка
class PerfTracker {
static #entries = new Map();
static mark(name) {
performance.mark(`start:${name}`);
}
static measure(name, fn) {
const startMark = `start:${name}`;
const endMark = `end:${name}`;
const measureName = `measure:${name}`;
performance.mark(startMark);
try {
const result = fn();
return result;
} finally {
performance.mark(endMark);
performance.measure(measureName, startMark, endMark);
const entry = performance.getEntriesByName(measureName)[0];
this.#entries.set(name, entry.duration);
// Очистка
performance.clearMarks(startMark);
performance.clearMarks(endMark);
performance.clearMeasures(measureName);
}
}
static getDuration(name) {
return this.#entries.get(name) ?? null;
}
static report() {
console.table(
Array.from(this.#entries.entries()).map(([name, duration]) => ({
'Метрика': name,
'Длительность, мс': duration.toFixed(2)
}))
);
}
}
// Пример:
PerfTracker.mark('init');
// ... инициализация ...
PerfTracker.measure('data-fetch', async () => {
const res = await fetch('/api/data');
return res.json();
});
PerfTracker.measure('render', () => {
document.body.innerHTML = '<div>Готово</div>';
});
setTimeout(() => PerfTracker.report(), 100);
18.2. Сбор Web Vitals (CLS, FID, LCP) без web-vitals
function observeWebVitals(callback) {
const metrics = {};
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
metrics.lcp = last.startTime;
callback({ name: 'LCP', value: last.startTime });
}).observe({ type: 'largest-contentful-paint', buffered: true });
// CLS
let cls = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
}
}
metrics.cls = cls;
callback({ name: 'CLS', value: cls });
}).observe({ type: 'layout-shift', buffered: true });
// FID (заменён на INP в 2025, но FID — legacy)
let firstInput = null;
const handler = (e) => {
if (!firstInput) {
firstInput = e;
const fid = e.processingStart - e.startTime;
metrics.fid = fid;
callback({ name: 'FID', value: fid });
removeEventListener('pointerdown', handler, { capture: true });
removeEventListener('keydown', handler, { capture: true });
}
};
addEventListener('pointerdown', handler, { capture: true, once: true });
addEventListener('keydown', handler, { capture: true, once: true });
return () => {
// Отмена наблюдателей при необходимости
};
}
// Запуск:
observeWebVitals(({ name, value }) => {
console.log(`[Vitals] ${name}: ${value.toFixed(2)} мс`);
});
19. Изоляция побочных эффектов: effectScope и onCleanup
Вдохновлено Vue 3 / Solid.js. Позволяет группировать эффекты (таймеры, подписки, DOM-мутации) и гарантированно отменять их.
19.1. Реализация effectScope
class EffectScope {
constructor(detached = false) {
this.detached = detached;
this.effects = [];
this.cleanups = [];
this.parent = detached ? null : EffectScope.current;
if (this.parent) this.parent.effects.push(this);
}
run(fn) {
const prev = EffectScope.current;
EffectScope.current = this;
try {
return fn();
} finally {
EffectScope.current = prev;
}
}
on() {
this.prev = EffectScope.current;
EffectScope.current = this;
}
off() {
EffectScope.current = this.prev;
}
stop() {
for (const cleanup of this.cleanups) {
cleanup();
}
this.cleanups.length = 0;
for (const effect of this.effects) {
effect.stop?.();
}
this.effects.length = 0;
}
}
EffectScope.current = null;
function onCleanup(fn) {
if (!EffectScope.current) {
throw new Error('onCleanup() вызван вне effectScope');
}
EffectScope.current.cleanups.push(fn);
}
// Пример: компонент с жизненным циклом
function createComponent() {
const scope = new EffectScope();
scope.run(() => {
const interval = setInterval(() => {
console.log('Тик');
}, 1000);
onCleanup(() => clearInterval(interval));
const clickHandler = () => console.log('Клик');
document.addEventListener('click', clickHandler);
onCleanup(() => document.removeEventListener('click', clickHandler));
});
return {
destroy() {
scope.stop();
}
};
}
// Использование:
const comp = createComponent();
// ... через 5 секунд:
setTimeout(() => comp.destroy(), 5000); // → все эффекты остановлены
19.2. Реактивный эффект (watchEffect-подобный)
function watchEffect(fn) {
if (!EffectScope.current) {
throw new Error('watchEffect() требует активной области');
}
let deps = new Set();
let cleanup = null;
const observe = (signal) => {
deps.add(signal);
return signal.value; // триггер подписки
};
const reRun = () => {
cleanup?.();
cleanup = null;
deps.clear();
// Временное переопределение чтения сигналов
const originalGet = Signal.prototype.get;
Signal.prototype.get = function () {
deps.add(this);
return originalGet.call(this);
};
try {
const result = fn(observe);
if (typeof result === 'function') {
cleanup = result;
}
} finally {
Signal.prototype.get = originalGet;
}
// Переподписка
deps.forEach(sig => {
sig.subscribe(reRun);
});
};
reRun();
EffectScope.current.cleanups.push(() => {
deps.forEach(sig => {
// Удалить только эту подписку — требует доработки Signal
// В простейшем случае — остановка всех, как выше
});
cleanup?.();
});
}
// Пример:
const count = new Signal(0);
const scope = new EffectScope();
scope.run(() => {
watchEffect((get) => {
console.log('Счётчик:', get(count));
return () => console.log('Очистка эффекта');
});
});
count.value = 1; // → 'Счётчик: 1'
scope.stop(); // → 'Очистка эффекта'
Для продакшена рекомендуется использовать
@vue/reactivityилиsolid-js, но эта реализация демонстрирует принципы.
20. Реактивные коллекции
20.1. ReactiveArray с событиями изменения
class ReactiveArray extends Array {
constructor(...items) {
super(...items);
this.#subscribers = new Set();
}
#notify(change) {
for (const fn of this.#subscribers) fn(change);
}
// Переопределяем мутационные методы
push(...items) {
const start = this.length;
const result = super.push(...items);
this.#notify({ type: 'push', start, added: items });
return result;
}
pop() {
const item = super.pop();
if (item !== undefined) {
this.#notify({ type: 'pop', index: this.length, removed: [item] });
}
return item;
}
splice(start, deleteCount, ...items) {
const removed = super.splice(start, deleteCount, ...items);
this.#notify({
type: 'splice',
start,
deleteCount: removed.length,
addedCount: items.length,
removed,
added: items
});
return removed;
}
sort(compareFn) {
const snapshot = [...this];
const result = super.sort(compareFn);
const changes = this.map((item, i) => snapshot[i] !== item ? i : -1).filter(i => i !== -1);
if (changes.length > 0) {
this.#notify({ type: 'sort', indices: changes });
}
return result;
}
// Подписка
subscribe(fn) {
this.#subscribers.add(fn);
return () => this.#subscribers.delete(fn);
}
}
// Пример: отслеживание изменений списка задач
const tasks = new ReactiveArray({ id: 1, title: 'Начать', done: false });
tasks.subscribe(({ type, ...rest }) => {
console.log(`[Array] ${type}`, rest);
renderTasks();
});
tasks.push({ id: 2, title: 'Продолжить', done: true }); // → push { start: 1, added: [ … ] }
tasks[0].done = true;
tasks.splice(0, 1); // → splice { start: 0, deleteCount: 1, … }
20.2. ReactiveMap и ReactiveSet
class ReactiveMap extends Map {
constructor(entries) {
super(entries);
this.#subscribers = new Set();
}
#notify(type, key, value = undefined, oldValue = undefined) {
for (const fn of this.#subscribers) fn({ type, key, value, oldValue });
}
set(key, value) {
const oldValue = this.has(key) ? this.get(key) : undefined;
const result = super.set(key, value);
this.#notify('set', key, value, oldValue);
return result;
}
delete(key) {
const value = this.get(key);
const result = super.delete(key);
if (result) this.#notify('delete', key, undefined, value);
return result;
}
clear() {
const snapshot = new Map(this);
const result = super.clear();
this.#notify('clear', undefined, undefined, snapshot);
return result;
}
subscribe(fn) {
this.#subscribers.add(fn);
return () => this.#subscribers.delete(fn);
}
}
class ReactiveSet extends Set {
constructor(values) {
super(values);
this.#subscribers = new Set();
}
#notify(type, value) {
for (const fn of this.#subscribers) fn({ type, value });
}
add(value) {
const existed = this.has(value);
const result = super.add(value);
if (!existed) this.#notify('add', value);
return result;
}
delete(value) {
const existed = this.has(value);
const result = super.delete(value);
if (existed) this.#notify('delete', value);
return result;
}
clear() {
const snapshot = new Set(this);
const result = super.clear();
this.#notify('clear', snapshot);
return result;
}
subscribe(fn) {
this.#subscribers.add(fn);
return () => this.#subscribers.delete(fn);
}
}
// Пример: кэш API-ответов
const apiCache = new ReactiveMap();
apiCache.subscribe(({ type, key, value }) => {
if (type === 'set') console.log(`Кэш обновлён: ${key} →`, value);
});
apiCache.set('/users/1', { name: 'Тимур' });
Эти коллекции легко интегрируются с
Proxyдля вложенной реактивности (например, при изменении свойства объекта внутриReactiveMap).
21. Снапшоты и гидратация состояния
21.1. Сериализация с поддержкой Date, RegExp, Map, Set, undefined
function serializeState(obj) {
return JSON.stringify(obj, (key, value) => {
if (value instanceof Date) {
return { __type: 'Date', value: value.toISOString() };
}
if (value instanceof RegExp) {
return { __type: 'RegExp', value: value.source, flags: value.flags };
}
if (value instanceof Map) {
return { __type: 'Map', value: Array.from(value.entries()) };
}
if (value instanceof Set) {
return { __type: 'Set', value: Array.from(value) };
}
if (typeof value === 'function') {
return undefined; // или throw, если функции недопустимы
}
return value;
});
}
function deserializeState(str) {
return JSON.parse(str, (key, value) => {
if (value && typeof value === 'object' && value.__type) {
switch (value.__type) {
case 'Date': return new Date(value.value);
case 'RegExp': return new RegExp(value.value, value.flags);
case 'Map': return new Map(value.value);
case 'Set': return new Set(value.value);
default: return value;
}
}
return value;
});
}
// Пример:
const state = {
timestamp: new Date(),
pattern: /\d+/g,
cache: new Map([['key', 'value']]),
flags: new Set(['a', 'b'])
};
const serialized = serializeState(state);
// → '{"timestamp":{"__type":"Date","value":"2025-11-13T10:30:00.000Z"},…}'
const restored = deserializeState(serialized);
console.log(restored.timestamp instanceof Date); // true
21.2. Менеджер снапшотов с историей (для undo/SSR/hydration)
class SnapshotManager {
constructor(options = {}) {
this.maxSnapshots = options.maxSnapshots || 50;
this.snapshots = [];
this.currentIndex = -1;
}
take(snapshot) {
// Удалить будущее, если есть (после undo → новые действия)
if (this.currentIndex < this.snapshots.length - 1) {
this.snapshots = this.snapshots.slice(0, this.currentIndex + 1);
}
this.snapshots.push(snapshot);
if (this.snapshots.length > this.maxSnapshots) {
this.snapshots.shift();
} else {
this.currentIndex++;
}
}
canUndo() { return this.currentIndex > 0; }
canRedo() { return this.currentIndex < this.snapshots.length - 1; }
undo() {
if (!this.canUndo()) return null;
return this.snapshots[--this.currentIndex];
}
redo() {
if (!this.canRedo()) return null;
return this.snapshots[++this.currentIndex];
}
current() {
return this.snapshots[this.currentIndex] ?? null;
}
hydrate(serializedSnapshots, currentIndex) {
this.snapshots = serializedSnapshots.map(s => deserializeState(s));
this.currentIndex = currentIndex;
}
export() {
return {
snapshots: this.snapshots.map(serializeState),
currentIndex: this.currentIndex
};
}
}
// Пример: редактор с историей
const editorState = { content: 'Начало', cursor: 7 };
const sm = new SnapshotManager({ maxSnapshots: 10 });
sm.take(editorState);
// Позже:
sm.take({ content: 'Начало и продолжение', cursor: 22 });
sm.take({ content: 'Начало и конец', cursor: 15 });
sm.undo(); // → { content: 'Начало и продолжение', ... }
Можно сохранять
sm.export()вlocalStorageили отправлять на сервер для восстановления сессии.
22. Парсинг JSDoc и генерация документации
22.1. Простой парсер JSDoc-блоков (без AST)
function parseJSDoc(code) {
const blocks = [];
const re = /\/\*\*([\s\S]*?)\*\//g;
let match;
while ((match = re.exec(code)) !== null) {
const comment = match[1]
.split('\n')
.map(line => line.replace(/^\s*\*\s?/, '').trim())
.filter(line => line && !line.startsWith('@'));
const tags = [];
const tagRe = /@(\w+)(?:\s+({[^}]+}|[^@\n]+))?/g;
let tagMatch;
while ((tagMatch = tagRe.exec(match[1])) !== null) {
const [, name, value = ''] = tagMatch;
tags.push({
name,
value: value.startsWith('{') ? value : value.trim()
});
}
blocks.push({
description: comment.join(' ').trim(),
tags
});
}
return blocks;
}
// Пример:
const source = `
/**
* Складывает два числа.
* Поддерживает целые и дробные значения.
* @param {number} a - Первое слагаемое
* @param {number} b - Второе слагаемое
* @returns {number} Сумма a и b
* @example
* add(2, 3); // 5
*/
function add(a, b) { return a + b; }
`;
const docs = parseJSDoc(source);
/*
→ [{
description: 'Складывает два числа. Поддерживает целые и дробные значения.',
tags: [
{ name: 'param', value: '{number} a - Первое слагаемое' },
{ name: 'param', value: '{number} b - Второе слагаемое' },
{ name: 'returns', value: '{number} Сумма a и b' },
{ name: 'example', value: 'add(2, 3); // 5' }
]
}]
*/
22.2. Генерация HTML из JSDoc
function renderJSDoc(blocks, options = {}) {
const { title = 'API Reference', theme = 'light' } = options;
const html = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>${title}</title>
<style>
body { font-family: sans-serif; background: ${theme === 'dark' ? '#1e1e1e' : '#fff'}; color: ${theme === 'dark' ? '#ccc' : '#333'}; }
.block { margin: 1.5em 0; padding: 1em; border-left: 4px solid #007acc; background: ${theme === 'dark' ? '#2d2d2d' : '#f8f9fa'}; }
.description { margin-bottom: 0.8em; }
.tags dt { font-weight: bold; }
.tags dd { margin: 0.2em 0 0.5em 1em; }
.example { background: #2d2d2d; color: #d4d4d4; padding: 0.5em; font-family: monospace; }
</style>
</head>
<body>
<h1>${title}</h1>
${blocks.map(block => `
<div class="block">
<p class="description">${block.description.replace(/\n/g, '<br>')}</p>
<dl class="tags">
${block.tags.map(tag => `
<dt>@${tag.name}</dt>
<dd>${tag.value}</dd>
`).join('')}
</dl>
</div>
`).join('')}
</body>
</html>
`;
return html;
}
// Использование:
const htmlDoc = renderJSDoc(docs, { title: 'Математические функции', theme: 'dark' });
// → HTML-строка, которую можно записать в файл или встроить в SPA
В продакшене используйте
documentation.jsилиTypeDoc, но этот подход подходит для внутренних инструментов и легковесных генераторов.
23. Песочница (Sandbox) для выполнения кода
23.1. Изоляция через iframe + CSP (браузер)
class CodeSandbox {
constructor(container) {
this.container = container;
this.iframe = document.createElement('iframe');
this.iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
this.iframe.style.cssText = 'width:100%; height:300px; border:1px solid #ccc;';
// Генерируем уникальный origin через Blob URL
const html = `
<!DOCTYPE html>
<meta charset="utf-8">
<script>
// Безопасное окружение: нет доступа к parent, top, opener
window.parent = null;
window.top = null;
window.opener = null;
// Ограниченный глобальный объект
const safeGlobals = {
console: {
log: (...args) => parent.postMessage({ type: 'log', args }, '*'),
error: (...args) => parent.postMessage({ type: 'error', args }, '*')
},
setTimeout,
clearTimeout,
setInterval,
clearInterval,
Date,
Math,
JSON,
URL,
URLSearchParams
};
// Выполнение кода
window.execute = (code) {
try {
const fn = new Function(...Object.keys(safeGlobals), 'code',
\`with (arguments[arguments.length - 1]) {
try {
(function() {${code}})();
} catch (e) {
console.error('Ошибка выполнения:', e.message);
}
}\`
);
fn(...Object.values(safeGlobals), {});
} catch (e) {
console.error('Ошибка компиляции:', e.message);
}
};
</script>
`;
this.iframe.src = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
container.appendChild(this.iframe);
}
run(code) {
// Очистка вывода
this.container.querySelectorAll('.output').forEach(el => el.remove());
// Обработка сообщений из iframe
const listener = (e) => {
if (e.source !== this.iframe.contentWindow) return;
const { type, args } = e.data;
const div = document.createElement('div');
div.className = `output ${type}`;
div.textContent = args.map(String).join(' ');
this.container.appendChild(div);
};
window.addEventListener('message', listener);
// Запуск
this.iframe.contentWindow.postMessage({ type: 'run', code }, '*');
// Отложенная очистка (например, при повторном запуске)
return () => {
window.removeEventListener('message', listener);
};
}
}
// Пример:
const sandbox = new CodeSandbox(document.getElementById('sandbox-container'));
document.getElementById('run-btn').onclick = () => {
const code = document.getElementById('code-input').value;
sandbox.run(code);
};
23.2. Node.js-песочница через vm (с ограничением)
// Только для Node.js (не работает в браузере!)
const vm = require('vm');
function safeEval(code, timeout = 1000) {
const context = {
console: {
log: (...args) => process.stdout.write(args.join(' ') + '\n')
},
setTimeout,
clearTimeout,
setInterval,
clearInterval,
Date,
Math,
JSON,
Buffer: undefined, // запрещаем
require: undefined, // запрещаем
process: undefined,
global: undefined
};
const script = new vm.Script(code, {
timeout,
lineOffset: 0,
columnOffset: 0
});
try {
return script.runInNewContext(context, { timeout });
} catch (e) {
throw new Error(`Песочница: ${e.message}`);
}
}
// Пример:
try {
safeEval(`
console.log('Результат:', Math.sqrt(144));
setTimeout(() => console.log('Таймаут'), 100);
`, 2000);
} catch (e) {
console.error(e.message);
}
Предупреждение:
vmне обеспечивает 100% изоляции — для высоконадёжных сценариев используйте отдельные процессы (worker_threads) или контейнеры.