TL;DR

Посмотреть код целиком можно тут: https://github.com/kubarskii/simple-di
Статья скорее рассчитана на начинающих разработчиков, но для ее понимания потребуется некоторое время и базовое представление об архитектуре приложений и паттернах. Перед чтением рекомендую познакомиться с принципами DDD и SOLID, особенно с буквой (D).


Я уже давно пытаюсь придумать, как можно сделать достойный DI-контейнер на ванильном JavaScript. Несколько раз я уже подходил к этой задаче, пробовал разные варианты, но отсутствие полноценной рефлексии и строгой типизации каждый раз сильно ограничивало возможные решения. Конечно, можно явно перечислять зависимости, хранить их в статических свойствах классов или использовать отдельный реестр метаданных, даже парсить исходный код классов, но обычно в какой-то момент такой контейнер либо становится слишком многословным, либо начинает напоминать плохо реализованный service locator.

В этот раз мне, кажется, удалось найти более интересный подход (возможно его находили и до меня). Он основан на том, что в JavaScript классы являются обычными значениями: их можно динамически создавать, передавать в функции, дополнять метаданными и использовать как токены. В результате получился небольшой DI-контейнер без TypeScript, декораторов и reflect-metadata.

Пока это эксперимент и попытка проверить саму идею, а не готовая библиотека. Тем не менее результат уже выглядит достойно, и после небольшой доработки я вполне мог бы попробовать использовать такой подход в реальном проекте. В статье разберем, как устроен контейнер, почему я решил отказаться от автоматического анализа конструкторов и как применить его в небольшом DDD-примере с корзиной товаров.


Когда речь заходит о dependency injection в JavaScript, разговор обычно быстро сводится к TypeScript, декораторам и reflect-metadata. Класс помечается как injectable, типы параметров конструктора превращаются в метаданные, а контейнер пытается самостоятельно понять, что именно нужно передать в каждый аргумент.

При этом сам JavaScript уже содержит почти все, что нужно для простого DI-контейнера. Класс в JavaScript является обычным runtime-значением. Его можно создать внутри функции, передать в другую функцию, использовать как ключ в Map, дополнить собственными свойствами и вернуть наружу. Значит, список зависимостей можно объявить прямо в момент создания класса, не пытаясь восстановить его позднее из типов, имен аргументов или результата Function.prototype.toString().

Далее мы соберем небольшой DI-контейнер на чистом JavaScript, добавим singleton, scoped и transient lifetime, а затем применим его в более реалистичном примере с DDD-корзиной. Доменный слой останется независимым от контейнера, а DI будет отвечать только за сборку application и infrastructure слоев.

Почему обычный JavaScript не может вывести зависимости автоматически

Представим простой сервис:

class CartApplicationService {
  constructor(repository, catalog, discountPolicy) {
    this.repository = repository;
    this.catalog = catalog;
    this.discountPolicy = discountPolicy;
  }
}

Во время выполнения JavaScript видит функцию-конструктор, но не знает, что repository должен реализовывать порт репозитория корзин, catalog должен быть каталогом товаров, а discountPolicy должен рассчитывать скидку. Имена аргументов не являются контрактом. После минификации они легко превращаются в a, b и c, а значения по умолчанию, деструктуризация и rest-параметры делают разбор сигнатуры еще менее надежным.

Можно написать собственный аналог reflect-metadata на WeakMap, но он решит только задачу хранения. Кто-то все равно должен положить туда список зависимостей. Поэтому можно убрать промежуточное хранилище и объявить зависимости в функции, которая создает класс.

Желаемый API будет выглядеть примерно так:

const CartApplicationService = component(
  {
    name: "CartApplicationService",
    lifetime: "scoped",
    inject: {
      carts: CART_REPOSITORY,
      catalog: PRODUCT_CATALOG,
      discounts: DISCOUNT_POLICY,
    },
  },
  () => class {
    constructor({ carts, catalog, discounts }) {
      this.carts = carts;
      this.catalog = catalog;
      this.discounts = discounts;
    }
  },
);

В этом варианте класс создается динамически, получает стабильное runtime-имя и одновременно сохраняет описание своих зависимостей. Сам класс можно использовать как DI-токен, а для абстрактных портов удобно использовать Symbol.

Создаем компонент

Начнем с минимальной функции component():

const COMPONENT_METADATA = Symbol("di.component.metadata");

const LIFETIMES = new Set([
  "singleton",
  "scoped",
  "transient",
]);

function component(
  {
    name,
    inject = {},
    lifetime = "transient",
  },
  classFactory,
) {
  if (!name) {
    throw new TypeError("Component name is required");
  }

  if (!LIFETIMES.has(lifetime)) {
    throw new TypeError(`Unsupported lifetime: ${lifetime}`);
  }

  const ComponentClass = classFactory();

  if (typeof ComponentClass !== "function") {
    throw new TypeError("classFactory must return a class");
  }

  Object.defineProperties(ComponentClass, {
    name: {
      value: name,
      configurable: true,
    },
    [COMPONENT_METADATA]: {
      value: Object.freeze({
        inject: Object.freeze({ ...inject }),
        lifetime,
      }),
      enumerable: false,
      configurable: false,
      writable: false,
    },
  });

  return ComponentClass;
}

Мы не добавляем ничего в глобальный Reflect и не заводим отдельный реестр метаданных. Описание хранится прямо на функции класса под закрытым Symbol. Снаружи его нельзя случайно получить по строковому имени, оно не появляется при обычном перечислении свойств, но контейнер может прочитать его в момент регистрации.

Имя класса задается явно. Для работы DI оно не требуется, потому что идентичность класса определяется самой функцией, но хорошее имя полезно в stack trace, сообщениях об ошибках и отладчике.

Для получения описания достаточно небольшой функции:

function getComponentMetadata(ComponentClass) {
  return ComponentClass[COMPONENT_METADATA] ?? null;
}

Токены: классы и Symbol

Если зависимость представлена конкретным классом, отдельный токен не нужен:

container.registerClass(Logger);
container.resolve(Logger);

Класс здесь одновременно является конструктором и ключом в Map. Это удобно для конкретных сервисов, которые не имеют нескольких реализаций.

Для портов лучше использовать Symbol:

const CART_REPOSITORY = Symbol("CartRepository");
const PRODUCT_CATALOG = Symbol("ProductCatalog");
const DISCOUNT_POLICY = Symbol("DiscountPolicy");

Application-слой зависит от таких токенов, а composition root связывает их с конкретными реализациями:

container.registerClass(InMemoryCartRepository, {
  token: CART_REPOSITORY,
});

Так доменная и прикладная логика не знают, используется ли память, PostgreSQL, Redis или внешний HTTP API.

Реализуем контейнер

Контейнер будет поддерживать три вида провайдеров:

  1. Готовое значение.

  2. Класс.

  3. Фабрику.

Также добавим три времени жизни:

  • singleton создает один экземпляр на корневой контейнер;

  • scoped создает один экземпляр внутри request scope;

  • transient создает новый экземпляр при каждом разрешении.

Основная часть контейнера выглядит так:

class Container {
  #providers = new Map();
  #singletons = new Map();

  registerValue(token, value) {
    this.#providers.set(token, {
      type: "value",
      value,
      lifetime: "singleton",
      inject: {},
    });

    return this;
  }

  registerClass(
    ComponentClass,
    {
      token = ComponentClass,
      inject,
      lifetime,
    } = {},
  ) {
    const metadata = getComponentMetadata(ComponentClass) ?? {
      inject: {},
      lifetime: "transient",
    };

    this.#providers.set(token, {
      type: "class",
      ComponentClass,
      inject: inject ?? metadata.inject,
      lifetime: lifetime ?? metadata.lifetime,
    });

    return this;
  }

  registerFactory(
    token,
    {
      inject = {},
      lifetime = "transient",
      factory,
    },
  ) {
    this.#providers.set(token, {
      type: "factory",
      inject,
      lifetime,
      factory,
    });

    return this;
  }
}

Самая важная часть находится в resolve(). Контейнер получает описание провайдера, рекурсивно разрешает зависимости, после чего создает экземпляр:

#resolve(token, scope, path) {
  if (scope?.hasOverride(token)) {
    return scope.getOverride(token);
  }

  const provider = this.#providers.get(token);

  if (!provider) {
    throw new Error(`Provider is not registered: ${String(token)}`);
  }

  if (
    provider.lifetime === "singleton" &&
    this.#singletons.has(token)
  ) {
    return this.#singletons.get(token);
  }

  if (provider.lifetime === "scoped") {
    if (!scope) {
      throw new Error("Scoped provider must be resolved from a scope");
    }

    if (scope.hasCached(token)) {
      return scope.getCached(token);
    }
  }

  if (path.includes(token)) {
    throw new Error("Circular dependency detected");
  }

  const nextPath = [...path, token];
  const dependencies = {};

  for (const [name, dependencyToken] of Object.entries(provider.inject)) {
    dependencies[name] = this.#resolve(
      dependencyToken,
      scope,
      nextPath,
    );
  }

  const instance = provider.type === "class"
    ? new provider.ComponentClass(dependencies)
    : provider.type === "factory"
      ? provider.factory(dependencies)
      : provider.value;

  if (provider.lifetime === "singleton") {
    this.#singletons.set(token, instance);
  }

  if (provider.lifetime === "scoped") {
    scope.setCached(token, instance);
  }

  return instance;
}

Здесь же проверяется циклическая зависимость. В полной версии из примера ошибка содержит всю цепочку токенов, например:

Circular dependency detected: CartService -> PricingService -> CartService

Почему зависимости передаются объектом

Контейнер мог бы хранить массив токенов и вызывать класс так:

new Service(repository, logger, clock);

Но объект с именованными зависимостями удобнее:

new Service({ repository, logger, clock });

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

inject: {
  carts: CART_REPOSITORY,
  catalog: PRODUCT_CATALOG,
  discounts: DISCOUNT_POLICY,
}

Контейнер при этом не выполняет property injection. Все зависимости по-прежнему приходят через конструктор, поэтому экземпляр нельзя получить в частично инициализированном состоянии.

Добавляем request scope

Для веб-приложения одного singleton и transient обычно недостаточно. Например, application service, unit of work или контекст текущего пользователя должны жить в пределах одного запроса.

Scope хранит собственный кеш и набор переопределений:

class Scope {
  #cache = new Map();

  constructor(container, overrides) {
    this.container = container;
    this.overrides = overrides;
  }

  resolve(token) {
    return this.container.resolveFromScope(token, this);
  }

  hasCached(token) {
    return this.#cache.has(token);
  }

  getCached(token) {
    return this.#cache.get(token);
  }

  setCached(token, value) {
    this.#cache.set(token, value);
  }
}

Контекст запроса не нужно регистрировать глобально. Его можно передать при создании scope:

const request = container.createScope([
  [REQUEST_CONTEXT, { id: "request-42" }],
]);

const service = request.resolve(CartApplicationService);

Любой scoped-компонент будет повторно использоваться внутри этого request scope, но другой запрос получит другой экземпляр.

Посмотрим на примере

Теперь перейдем к простому вполне прикладному примеру. В нем есть aggregate Cart, value object Money, application service, репозиторий, каталог товаров, скидочная политика и event bus. Если на этом моменте воозникают вопросы, то рекомендую глянуть DDD. DDD делят обычно на тактический и стратегический, сейчас нас интересует тактический.

Важно, что Cart и Money ничего не знают о component(), контейнере и lifetime. DI не должен проникать в каждую сущность проекта. Доменная модель создается обычным JavaScript-кодом и защищает свои инварианты самостоятельно.

Упрощенный value object денег:

class Money {
  constructor(cents, currency = "EUR") {
    if (!Number.isSafeInteger(cents)) {
      throw new TypeError("Money must be stored as integer cents");
    }

    if (cents < 0) {
      throw new Error("Money cannot be negative");
    }

    this.cents = cents;
    this.currency = currency;
    Object.freeze(this);
  }

  add(other) {
    if (this.currency !== other.currency) {
      throw new Error("Currency mismatch");
    }

    return new Money(this.cents + other.cents, this.currency);
  }

  multiply(quantity) {
    return new Money(this.cents * quantity, this.currency);
  }
}

Aggregate корзины отвечает за добавление товаров, изменение количества, применение купона и checkout. Он не позволяет добавлять товар после оформления и не разрешает оформить пустую корзину:

class Cart {
  #items = new Map();
  #events = [];

  addProduct({ sku, name, unitPrice, quantity = 1 }) {
    this.#assertOpen();
    this.#assertQuantity(quantity);

    const current = this.#items.get(sku);
    const nextQuantity = (current?.quantity ?? 0) + quantity;

    this.#assertQuantity(nextQuantity);

    this.#items.set(sku, {
      sku,
      name,
      unitPrice,
      quantity: nextQuantity,
    });
  }

  checkout({ discountPolicy, now }) {
    this.#assertOpen();

    if (this.#items.size === 0) {
      throw new Error("Cannot checkout an empty cart");
    }

    const subtotal = this.subtotal();
    const discount = discountPolicy.calculate({ cart: this, subtotal });
    const total = subtotal.subtract(discount);

    this.status = "checked-out";

    this.#events.push({
      type: "CartCheckedOut",
      occurredAt: now.toISOString(),
      payload: {
        cartId: this.id,
        total: total.toJSON(),
      },
    });

    return { subtotal, discount, total };
  }
}

Скидочная политика передается в доменную операцию явно. Это доменный сервис, который может иметь несколько реализаций. Сам aggregate не обращается к контейнеру и не ищет сервис через service locator.

Application service как компонент

Application service координирует сценарий. Он загружает aggregate, получает товар из каталога, вызывает доменную операцию и сохраняет новое состояние:

const CartApplicationService = component(
  {
    name: "CartApplicationService",
    lifetime: "scoped",
    inject: {
      carts: CART_REPOSITORY,
      catalog: PRODUCT_CATALOG,
      discounts: DISCOUNT_POLICY,
      events: EVENT_BUS,
      createId: ID_GENERATOR,
      clock: CLOCK,
      request: REQUEST_CONTEXT,
    },
  },
  () => class {
    constructor({
      carts,
      catalog,
      discounts,
      events,
      createId,
      clock,
      request,
    }) {
      this.carts = carts;
      this.catalog = catalog;
      this.discounts = discounts;
      this.events = events;
      this.createId = createId;
      this.clock = clock;
      this.request = request;
    }

    async addProduct(cartId, sku, quantity = 1) {
      const cart = await this.carts.getById(cartId);
      const product = await this.catalog.getBySku(sku);

      cart.addProduct({
        ...product,
        quantity,
      });

      await this.carts.save(cart);
      return cart.toView(this.discounts);
    }

    async checkout(cartId) {
      const cart = await this.carts.getById(cartId);
      const totals = cart.checkout({
        discountPolicy: this.discounts,
        now: this.clock(),
      });

      await this.carts.save(cart);
      await this.events.publishAll(cart.pullDomainEvents());

      return totals;
    }
  },
);

Scoped lifetime здесь выбран не потому, что сам сервис хранит важное состояние, а потому, что позднее в этот же scope можно добавить unit of work, transaction context или identity map. В небольшом примере сервис можно сделать transient, но request scope показывает, как контейнер будет вести себя в настоящем приложении.

Infrastructure-реализации

Репозиторий можно зарегистрировать под абстрактным токеном:

const InMemoryCartRepository = component(
  {
    name: "InMemoryCartRepository",
    lifetime: "singleton",
  },
  () => class {
    #storage = new Map();

    async getById(id) {
      const snapshot = this.#storage.get(id);
      return snapshot ? new Cart(snapshot) : null;
    }

    async save(cart) {
      this.#storage.set(cart.id, cart.toSnapshot());
    }
  },
);

In-memory реализация хранит snapshots, а не ссылки на aggregate. Это небольшая, но полезная деталь. Если репозиторий просто вернет тот же объект из Map, изменение aggregate произойдет еще до вызова save(), и тестовый репозиторий будет скрывать ошибки, которые появились бы при работе с настоящей базой данных.

Скидочная политика также является отдельным компонентом:

const CouponDiscountPolicy = component(
  {
    name: "CouponDiscountPolicy",
    lifetime: "singleton",
  },
  () => class {
    calculate({ cart, subtotal }) {
      if (cart.coupon === "HABR10") {
        return subtotal.percent(10);
      }

      return Money.zero(subtotal.currency);
    }
  },
);

Composition root

Все связи собираются в одном месте:

const container = new Container()
  .registerClass(InMemoryCartRepository, {
    token: CART_REPOSITORY,
  })
  .registerClass(InMemoryProductCatalog, {
    token: PRODUCT_CATALOG,
  })
  .registerClass(CouponDiscountPolicy, {
    token: DISCOUNT_POLICY,
  })
  .registerClass(InMemoryEventBus, {
    token: EVENT_BUS,
  })
  .registerValue(ID_GENERATOR, randomUUID)
  .registerValue(CLOCK, () => new Date())
  .registerClass(CartApplicationService);

Создание request scope выглядит так:

const request = container.createScope([
  [REQUEST_CONTEXT, { id: randomUUID() }],
]);

const carts = request.resolve(CartApplicationService);

После этого можно выполнить прикладной сценарий:

const created = await carts.createCart();
const cartId = created.cart.id;

await carts.addProduct(cartId, "coffee", 2);
await carts.addProduct(cartId, "mug", 1);
await carts.applyCoupon(cartId, "HABR10");

console.dir(await carts.checkout(cartId), {
  depth: null,
});

Для двух чашек кофе по 12,90 евро и одной кружки за 15,90 евро получится subtotal 41,70 евро. Купон HABR10 даст скидку 4,17 евро, итоговая сумма составит 37,53 евро.

Что в итоге

В результате получился небольшой DI-контейнер, который умеет регистрировать классы, значения и фабрики, поддерживает разные lifetime и при этом не требует TypeScript, декораторов или reflect-metadata.

Сам подход довольно простой. Вместо того чтобы пытаться угадать зависимости класса по его конструктору, мы указываем их в момент создания компонента. Класс при этом остается обычным классом: его можно создать через new, проверить через instanceof, увидеть нормальное имя в stack trace и протестировать отдельно от контейнера.

Конечно, это эксперимент, а не библиотека, которую стоит сразу тащить в продакшен. Здесь нет асинхронной инициализации, автоматического освобождения ресурсов, модулей, optional dependencies и еще десятка вещей, которые постепенно появляются в любом взрослом DI-контейнере. Но основная идея уже работает, а дальше все зависит от того, насколько далеко захочется ее развивать.

Мне в первую очередь было интересно проверить, можно ли сделать удобный DI на чистом JavaScript, не пытаясь превратить его в TypeScript без типов. Похоже, что можно. JavaScript позволяет динамически создавать классы, использовать их как токены и хранить описание зависимостей прямо на них. Этого уже достаточно, чтобы собрать вполне рабочий граф объектов.

При этом я бы не стал оборачивать в component() вообще каждый класс приложения. Cart, Money и другие объекты доменной модели отлично живут без контейнера. DI полезнее оставить на внешнем уровне, где приложение связывается с репозиториями, базами данных, очередями, HTTP-клиентами и другими инфраструктурными деталями.

Полный код примера лежит в репозитории:https://github.com/kubarskii/simple-di

Спасибо за внимание :)

Комментарии (22)


  1. SergeyEgorov
    17.07.2026 17:16

    Зачем Javasript-у DI контейнер?


    1. alexander_kubarski Автор
      17.07.2026 17:16

      Да по той же причине по которой его использует Nest.js или Angular. У современных приложений очень высокая сложность. По мере роста проекта сервисы начинают зависеть от репозиториев, логгеров, клиентов API, контролировать все связи становится сложно, а при необходимости сменить один логгер на другой можно испытать сильную боль. В этом JS не отличается от других языков. JS и Node.js давно перестали быть просто скриптовыми языками.


      1. SergeyEgorov
        17.07.2026 17:16

        а при необходимости сменить один логгер на другой можно испытать сильную боль


        1. alexander_kubarski Автор
          17.07.2026 17:16

          На самом деле ваш пример показывает инверсию зависимостей, которую на масштабе решает DI контейнер.


          1. SergeyEgorov
            17.07.2026 17:16

            ваш пример показывает инверсию зависимостей

            Хоть горшком назовите, не могу понять от чего тут можно испытать "сильную боль". Передать потребителю ссылку на другой экземпляр?


    1. nihil-pro
      17.07.2026 17:16

      Нужен. Очень не хватает. Конечно, все себе по разному представляют js, но если отбросить предрассудки типа «корочки красят в своих интерфейсах», то вполне понятно зачем. JS такой же ЯП как и многие другие, с такими же примерно проблемами, на нем решают примерно такие же задачи и т.д.


      1. SergeyEgorov
        17.07.2026 17:16

        Зачем? Примеры кода можете? Чтобы отчетливо было видно что DI-контейнер это прям революция?


        1. flancer
          17.07.2026 17:16

          DI-контейнер - это никакая не революция. Им сто лет в обед. Я со Spring'ом работал в 2010-х. Это уже была 3-я версия платформы. Затем я использовал DI в PHP - в Magento. Очень своеобразный DI, особенно в первой версии (2008 - 2015 год). Использовал DI из Symfony 2. В PHP вообще с этим делом всё хорошо, даже стандарт есть - PSR-11.

          Когда окончательно перешёл в JS даже "запилил" свой DI - чтобы писать в привычной для себя манере. Правда я позволил ИИ его "под себя" переписать (под "силиконовых", а не под "кожаных").

          Что касается примеров кода, то могу предложить посмотреть вот этот код - @flancer32/teq-cms. Я писал его "ручками", ещё на "рукописном" DI. А код в @flancer32/teq-web уже написан ИИ (Codex) с "его" DI. Я в код заглядывал, но руками старался не править ничего. Самый, что ни на есть "машинный код".

          Рабочий код на фронте - вот. Зайдите в DevTools и посмотрите, что видит браузер:

          То же самое, что видит разработчик в IDE
          То же самое, что видит разработчик в IDE

          Это старый код, но сделан тоже в той же IoC-парадигме с моим рукописным DI в основе. Для построения UI используется vue и Quasar.

          Если вы посмотрите все es6-модули, то там не будет ни одного (!) статического импорта. Статические импорты нужны в головном скрипте для загрузки DI-библиотеки, а дальше контейнер сам динамически (через import()) загружает нужные зависимости по мере необходимости. Как следствие, код становится изоморфным (без изменений может использоваться как в браузере, так и в nodejs). Вот вообще без изменений! Поначалу я на бэке импортировал node'овские библиотеки, но сейчас и они инжектятся. То есть, можно и бэкендовый код в браузере запускать, только node-библиотеки нужно замокать (делается через препроцессор контейнера). А сам код приложения остаётся без изменений.

          И в отличие от многих других DI-контейнеров (включая контейнер автора статьи), у моего контейнера ограничена возможность что-то зарегистрировать специально:

          const container = new Container()
            .registerValue(ID_GENERATOR, randomUUID)
            .registerValue(CLOCK, () => new Date())
            .registerClass(CartApplicationService);

          У меня регистрировать можно только если перевести контейнер в тестовый режим:

          container.enableTestMode();
          container.register("App_Service$", mockService);

          То есть, в prod-режиме все es6-модули резолвятся и подгружаются автоматически, сколько бы их там ни было. Нужно только "потянуть" головной модуль, а дальше строится дерево зависимостей и подтягиваются исходники (можно прямо с unpkg.com тянуть). Более того, с учётом наличия в контейнере препроцессора, можно менять маппинг идентификаторов зависимостей на исходный код и реализовывать такие посторонние для "ванильного" JS вещи, как интерфейсы. А с учётом наличия там же постпроцессора, можно использовать в JS и AOP.

          Все эти приёмы и технологии давным давно используются в программировании и никакая не революция. Просто они используются там, где кодовая база проекта начинает заваливать за миллионы строк кода (в Magento 2, например, больше 2 млн. и для работы с JS используется requirejs, тоже своего рода DI).

          Много у вас примеров таких объёмных кодовых баз именно на JavaScript? А для кодирования на одной веб-страничке чего угодно такие навороты действительно не нужны.


          1. SergeyEgorov
            17.07.2026 17:16

            Spring - это понятно. Это Java и приколоченное гвоздями ООП. А в Javascript то зачем? В Javascript классы это просто "синтаксический сахар". Зачем вообще тащить в программирование на Javascript ООП шаблоны? В нем без них значительно проще, быстрее и дешевле.


            1. nihil-pro
              17.07.2026 17:16

              Javascript классы это просто "синтаксический сахар"

              Предрассудки...

              class Bar {
                method1() {
                  // ...
                }
              
                method2() {
                  // ...
                }
              }
              
              class Foo extends Bar {
                
                // Static initialization block
                static {
                  try {
                    
                  } catch {
                    
                  }
                }
              
                // Private static field
                static #field = 1;
              
                // Private static method
                static #method() {}
              
                // Private static getter
                static get #foo() {
                  return this.#privateField;
                }
              
                // override parent method
                method1() {
                  // ...
                }
              
                // extend parent method
                method2() {
                  super.method2()
                  // ...
                }
              
                // child private method
                #method() {}
              }

              Подскажите, это «синтаксический сахар» над чем?


              1. SergeyEgorov
                17.07.2026 17:16

                Подскажите, это «синтаксический сахар» над чем?

                Над механизмом прототипов


                1. nihil-pro
                  17.07.2026 17:16

                  Built on, не значит «сахар». Вы выделили то, что вам понравилось, но ведь сразу за выделенным текстом идет: but have some syntax and semantics that are unique to classes.


                  1. Lucifurry
                    17.07.2026 17:16

                    хаха, обожаю такой черрипикинг, где скрин опровергает самого человека, что-то доказывающего)


            1. alexander_kubarski Автор
              17.07.2026 17:16

              Вы приявязыветесь к языку, паттерны же решают какой-то класс задач и language-agnostic. Так например signals, reactor, observable, тот же di, они сейчас в разных библиотеках и вы ими неосознанно пользуетесь каждый день. На счёт классов, они стали не совсем синтаксическим сахаром, сейчас есть небольшие различия во внутренней реализации, но это тоже не важно. Прототипы и наследование через них просто один из вариантов реализации ООП. Мне лично нравится объяснение дядюшки Боба про различия разных парадигм.


              1. SergeyEgorov
                17.07.2026 17:16

                Все! Я понял. Вы не можете без ООП. А Javascript прекрасен как раз тем, что на нем можно быстро, дешево и качественно делать код без ООП. Поэтому я на него и пересел после C++, потом Java, потом C#, потом Python...