Callable, Thenable, Iterable, Observable
Поведенческие контракты JavaScript: что-то Callable если можно вызвать, Thenable если есть метод then, Iterable если есть Symbol.iterator, Observable если есть subscribe. Это не классы, это duck-typed контракты, на которые опирается среда (await, for-of, spread, RxJS).
Callable
typeof fn === 'function'; // вот и весь контракт
Callable бывают синхронные и асинхронные. Если в
array.filterпередали callable, он отработает синхронно — там нет перехода на event loop.
Контракты callback:
- Sync:
arr.filter(item => item > 0)— данные первым аргументом - Async error-first:
readFile(path, (err, data) => {})— ошибка первая
В асинхронные callable передаём первым параметром ошибку, вторым — данные.
Не больше двух параметров в callback
Я не рекомендую делать аргументов больше двух — ошибка и данные. Лучше объедините данные в один объект.
Упрощает контракт и V8-оптимизацию.
null вместо undefined для ошибки
cb(null, data); // нет ошибки — null
return value ?? undefined; // для скаляро-подобных — undefined
Если ошибка не приходит — должен быть null, а не undefined. Функция и объект совместимы с nullable type. Для скаляро-подобных значений — undefined.
В JavaScript реально есть один скаляр — это undefined. Всё остальное — определённые боксы, даже null и boolean — боксированные типы.
Infinity и NaN вместо union-типов
// плохо: number | Error → union, лишние проверки
// хорошо: NaN при невалидном результате
function calc(x) { return x < 0 ? NaN : Math.sqrt(x); }
Используйте Infinity и NaN — они совместимы по типам, не нужны union-types. V8 плохо оптимизирует union number | error.
Proxy и callable
const callableProxy = new Proxy(fn, {
apply(target, thisArg, args) { return target(...args); }
});
Можно сделать перехват вызова в Proxy только если это до этого была asynchronous function, функция, конструктор — что-то уже callable. Над массивом apply-trap работать не будет.
Thenable
const thenable = {
then(onFulfilled, onRejected) {
onFulfilled(42);
return this; // может возвращать себя
}
};
await thenable; // работает
Thenable — объект с методом then. У него может не быть других методов, catch не обязателен.
Promise строже thenable
Promise — более строгий контракт. Он наследует thenable, потому что thenable используется для await.
| Свойство | thenable | Promise |
|---|---|---|
| Метод then | да | да |
| catch | опционально | да |
| Иммутабельность состояния | нет | да |
| Цепочка с новыми Promise | нет | да |
await требует только then
Если у объекта есть метод then, от чего бы он ни наследовал — это может быть какой-то MySuperObject — оно вызовется. await вызовет этот метод.
Thenable быстрее Promise
Создание Promise в V8 — очень затратная операция. Если можно обойти, лучше обойти. Thenable создаётся на уровне самого V8, а за Promise во многом отвечает host-среда. Это работает медленнее, намного медленнее.
function createFastResolved(value) {
return { then(resolve) { resolve(value); } };
}
// Вместо: return Promise.resolve(value);
Iterable
const iterable = {
[Symbol.iterator] {
let i = 0;
return {
next {
return i < 3 ? { value: i++, done: false } : { value: undefined, done: true };
}
};
}
};
for (const x of iterable) console.log(x); // 0, 1, 2
[...iterable]; // [0, 1, 2]
Контракт: [Symbol.iterator] возвращает iterator с методом next. for-of, spread, destructuring, Array.from — всё работает через iterable.
Array-like
const arrLike = { 0: 'a', 1: 'b', length: 2 };
Array.from(arrLike); // ['a', 'b']
Не iterable, но Array.from понимает. Также arguments в обычной функции, NodeList в DOM.
Observable
const observable = {
subscribe(observer) {
setInterval( => observer.next(Math.random), 1000);
return { unsubscribe {} };
}
};
Контракт: subscribe(observer) → subscription. observer имеет next/error/complete. Не стандартизировано в JS, но фактически принято в RxJS.
Stream
Node.js streams — это Readable/Writable/Duplex/Transform. Реализуют свой контракт через emit('data'), но также являются async iterable начиная с Node 10.
Связанные темы
- Поведенческие контракты JS
- Итераторы и протокол итерации
- Promise
- Observer Observable паттерн
- Thenable
Ресурсы
- Лекция: vNYRwSNUnvY