Трансформация приватных полей классов

Приватные поля вида ECMAScript и обеспечивают инкапсуляцию на уровне языка. В отличие от соглашений через _underscore, они имеют жёсткую семантическую защиту: доступ вне тела класса приводит к ошибке выполнения.

class User {
  #token = "secret";

  getToken() {
    return this.#token;
  }
}

const u = new User();
u.getToken(); // "secret"
u.#token;     // SyntaxError

На уровне транспиляции такие конструкции требуют специальной обработки, поскольку старые версии JavaScript-движков не поддерживают приватные поля. Именно здесь поведение компилятора становится критическим: необходимо не только сохранить семантику доступа, но и обеспечить безопасность и производительность.

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


Семантика приватных полей и ограничения трансформации

Приватные поля обладают рядом свойств, которые усложняют их преобразование:

  • невозможность динамического доступа (obj[“#field”] недопустимо)
  • строгая привязка к классу-определению
  • отдельные пространства имён для каждого класса
  • различие между #field и публичными свойствами с тем же именем

Следовательно, трансформация должна имитировать:

  • изоляцию данных
  • контроль доступа на уровне экземпляра
  • отсутствие конфликтов между наследниками

Основные стратегии трансформации в SWC

SWC использует несколько подходов в зависимости от настроек:

WeakMap-основанная модель

Наиболее близкая к спецификации реализация строится через WeakMap.

const _token = new WeakMap();

class User {
  constructor() {
    _token.set(this, "secret");
  }

  getToken() {
    return _token.get(this);
  }
}

Особенности:

  • корректная изоляция данных
  • автоматическая сборка мусора (GC-safe)
  • высокая совместимость с наследованием

Минусы:

  • дополнительные накладные расходы на доступ
  • увеличение количества вспомогательных структур
  • снижение производительности в горячих циклах

Closure-based трансформация

Альтернативный подход — замыкания внутри фабричной функции класса.

var User = (function () {
  var _token = new WeakMap();

  function User() {
    _token.set(this, "secret");
  }

  User.prototype.getToken = function () {
    return _token.get(this);
  };

  return User;
})();

Этот вариант часто используется в более старых target-окружениях или при специфических настройках транспиляции.


Inline private storage (оптимизированные режимы)

В некоторых конфигурациях SWC может использовать оптимизированные структуры, если:

  • target >= ES2015
  • включён loose режим
  • отсутствуют сложные случаи наследования

Пример упрощённой модели:

class User {
  constructor() {
    this._token = "secret";
  }

  getToken() {
    return this._token;
  }
}

Ограничения:

  • не соответствует спецификации приватности
  • доступ возможен извне
  • используется только как оптимизация

Конфигурация SWC для приватных полей

Основной контроль осуществляется через .swcrc:

{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "privateMethod": true,
      "classPrivateProperty": true
    },
    "transform": {
      "legacyDecorator": false
    },
    "target": "es5"
  }
}

Ключевые параметры:

  • classPrivateProperty — включает поддержку трансформации #field
  • target — определяет уровень JavaScript-выхода
  • loose — влияет на стратегию эмуляции приватности

Трансформация приватных полей: внутренний механизм SWC

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

  1. Разбор AST

Исходный код:

class Counter {
  #value = 0;
  inc() {
 this.#value++;
  }
}

Преобразуется в AST с узлами типа:

  • ClassDeclaration
  • PrivateProperty
  • PrivateName

  1. Hoisting приватного состояния

SWC извлекает приватные поля в область модуля:

var _value = new WeakMap();

или:

var _value = new WeakSet(); // для методов

  1. Переписывание конструкторов

class Counter {
  constructor() {
 _value.set(this, 0);
  }
}

Если конструктор отсутствует, SWC генерирует его автоматически.


  1. Замена доступа (#field)

Операции:

Чтение

this.#value

заменяется на:

_value.get(this)

Запись

this.#value = 1

становится:

_value.set(this, 1)

Инкремент/декремент

this.#value++

разворачивается в:

_value.set(this, _value.get(this) + 1)

Наследование и приватные поля

Приватные поля не наследуются. SWC строго сохраняет это поведение.

class A { #x = 1; }




class B extends A { getX() { return this.#x; // ошибка на этапе
трансформации/выполнения } }

После трансформации SWC создаёт отдельные WeakMap для каждого класса:

var _xA = new WeakMap();

class A {
  constructor() {
    _xA.set(this, 1);
  }
}

Класс B не имеет доступа к _xA.


Статические приватные поля

Статические поля преобразуются отдельно, так как они принадлежат классу, а не экземпляру.

class Service {
  static #cache = new Map();
}

Трансформация:

var _cache = new WeakMap();

class Service {
  static getCache() {
    return _cache.get(Service);
  }
}

Особенность: вместо this используется объект класса.


Оптимизация доступа

SWC может применять кеширование доступа при частых обращениях:

const _value = new WeakMap();

function get(obj) {
  return _value.get(obj);
}

и далее:

const v = get(this);

Это уменьшает количество вызовов .get/.set, особенно в циклах.


Взаимодействие с Babel-экосистемой

Хотя SWC и Babel решают одинаковую задачу, их подход отличается:

  • SWC ориентирован на скорость компиляции (Rust)
  • Babel — на расширяемость через плагины

В трансформации приватных полей различия проявляются в:

Характеристика SWC Babel
Стратегия WeakMap / оптимизации WeakMap / helper runtime
Скорость выше ниже
Настройка ограниченная гибкая
Плагины минимальные широкие

Edge-cases и проблемные сценарии

Деструктуризация экземпляра

const { method } = instance;
method();

Если метод использует #private, возможна потеря контекста. SWC не всегда может предупредить об этом.


Reflect и Proxy

const p = new Proxy(obj, {});
p.#field;

Приватные поля не перехватываются Proxy, и SWC сохраняет это поведение.


Сериализация

JSON.stringify(instance)

Приватные поля не участвуют в сериализации, поскольку хранятся в WeakMap или замыканиях.


Производственные настройки и влияние на bundle

Использование приватных полей через WeakMap увеличивает:

  • размер bundle (дополнительные структуры)
  • количество аллокаций
  • нагрузку на GC

Однако:

  • улучшает инкапсуляцию
  • предотвращает утечки состояния
  • делает код ближе к спецификации

Итоговая модель трансформации

Обобщённо SWC превращает приватные поля в одну из форм:

  • WeakMap(this → value) — основная модель
  • closure state — функциональная модель
  • inline property — оптимизированная, но небезопасная модель

Каждый вариант выбирается автоматически на основе:

  • target окружения
  • режима loose
  • структуры класса
  • требований совместимости

Результатом становится код, который сохраняет поведение ECMAScript-приватности даже в средах без её нативной поддержки, при этом балансируя между корректностью и производительностью.