Поведенческие контракты JS
В JavaScript нет интерфейсов как в TypeScript/Java, но язык опирается на набор "поведенческих контрактов": Callable, Thenable, Iterable, Async Iterable, Array-like, Observable, Streamable, Disposable. Каждый — это договор о наличии определённых методов/символов.
Что это / Зачем
Языковые конструкции (await, for-of, for await-of, ...spread, using) полагаются не на конкретный класс, а на контракт. Если ваш объект соответствует контракту — он вписывается в синтаксис. Это глубокий duck typing, удобный для интероперабельности библиотек без наследования.
Контракты
| Контракт | Метод/символ | Где используется |
|---|---|---|
| Callable | `` (любая функция) | fn, Function.prototype.call/apply |
| Thenable | .then(onF, onR) |
await, Promise.resolve |
| Iterable | [Symbol.iterator] |
for-of, ...spread, [...obj] |
| Async Iterable | [Symbol.asyncIterator] |
for await-of |
| Array-like | length + индексы |
Array.from(obj), Array.prototype.slice.call(obj) |
| Observable | .subscribe(observer) |
RxJS, библиотеки реактивности |
| Streamable | events data/end + pipe |
Node streams, ReadableStream |
| Disposable | [Symbol.dispose] или [Symbol.asyncDispose] |
using x = ... (ES2023) |
Ключевые моменты
- Контракты дополняют, не конкурируют: Promise — это Thenable. Array — Iterable + Array-like. Generator — Iterable.
awaitищет только.then— поэтому работает с любым thenable, не только с Promise.for-ofищет[Symbol.iterator],for-await-of— оба (предпочитает asyncIterator).- Array-like ≠ Iterable: старый объект
argumentsarray-like, но не iterable (поэтому нуженArray.fromили rest-параметры). Observableстандартизирован в нескольких proposals, но в спеку не вошёл. RxJS — де-факто стандарт.- Сигналы (Angular Signals, React, SolidJS) вытесняют Observable в новых фреймворках — они проще и не требуют RxJS-операторов.
using x = makeResource(ES2023, TC39 Stage 4) вызываетx[Symbol.dispose]при выходе из scope — аналог Pythonwith, C#using.
Подводные камни
- Не называйте свои методы
then/catchв обычных бизнес-объектах —await objнечаянно вызовет вашthen. instanceofне работает между контекстами (iframe, vm.runInContext) — лучше проверять контракт (typeof x.then === 'function').Symbol.disposeиusing— ещё новые, требуют Node 22+ или транспиляции.- Не наследовать User от EventEmitter — это композиция через has-a, а не is-a.
🎓 Источники
- 🎓 [JavaScript Callable, Thenable, Promise, Array-like, Iterable, Observable, Streamable] · 2025-10-24 · YouTube
- Тезисы:
- Callable — контракт "оператор вызова ``". Proxy
applyсрабатывает только если target — callable. - Error-first: не больше двух параметров в callback,
nullвместоundefinedдля ошибки. - Не сводить всё к undefined — Infinity, NaN, специальные значения часто читаемее, чем union-типы.
Promiseстроже чем thenable: гарантирует асинхронность колбэка, однократный resolve, ассимиляцию thenable.- Сигналы проще RxJS — большинству приложений не нужны операторы вроде
mergeMap. - Стримы в object mode — гибридный контракт между Streamable и Iterable.
- Disposable /
using— финализационный код в классе, аналог Pythonwith.
- Callable — контракт "оператор вызова ``". Proxy
- Цитата:
«Не наследовать User от EventEmitter — это композиция через has-a, а не is-a. Наследование нужно, но редко.»
- Тезисы:
- 🎓 [Explicit resource management with Using and Symbol.dispose] · 2025-05-22 · YouTube
- Тезис:
usingкорректно отрабатывает финализатор даже при exception, в отличие от ручногоtry/finally.
- Тезис: