В некоторых современных объектно-ориентированных языках есть понятие идексаторов – свойств, позволяющих работать с экземпляром класса как с массивом, используя [] нотацию. В этой статье я хочу продемонстрировать как это сделать на современном JavaScript.
Приведу пример на C#:
class Colors { private Dictionary<string, Color> inner = new Dictionary<string, Color>(); // реализуем индексатор public Color this[string name] { get => inner[name]; set => inner[name] = value; } } class Program { static void Main(string[] args) { Colors c = new Colors(); // обращаемся к свойству используя [] нотацию c["red"] = Color.Red; c["yellow"] = Color.Yellow; } }
К сожалению, пока такой возможности современный ES6 напрямую не предлагает. Тем не менее, в ES6 есть механизмы позволяющие добиться подобного поведения, хоть это будет выглядеть и работать не так изящно, как в том же C#.
В ES6 есть особый класс Proxy, позволяющий перехватывать обращения к базовому классу. Таким образом, появляется возможность особым образом обрабатывать обращения к полям класса.
Давайте повторим наш C# пример на JS без каких-либо хитростей.
class Colors { #colors = new Map(); getColor(key) { // тут может быть сложная логика return this.#colors.get(key); } setColor(key, value) { // тут может быть сложная логика this.#colors.set(key, value); } hasColor(key) { return this.#colors.has(key); } } const colors = new Colors(); colors.setColor('red', ...); colors.setColor('yellow', ...); colors.hasColor(‘red’); => true
Чтобы добавить цвет в набор мы вместо нотации [] используем метод setColor(). Исправим это дело добавив магию Proxy. Для этого объявим в классе Colors конструктор и будем вместо экземпляра Colors возвращать его Proxy обертку с обработчиками get и set.
constructor() { return new Proxy(this, { // перекрывает получение Colors.name get(target, name) { if (name in target) { const result = target[name]; return typeof result === 'function' ? result.bind(target) : result; } return target.getColor(name); }, // перекрывает установку Colors.name set(target, name, value) { if (name in target) target[name] = value; else target.setColor(name, value); return true; } }); }
Тут необходимо дополнительно пояснить некоторый код обработчика get:
if (name in target) {…}
Этот фрагмент нужен чтобы дать доступ к свойствам и методам класса (например, hasColor), и только если такого свойства или метода в классе нет, то вызов пойдет в getColor().
return typeof result === 'function' ? result.bind(target) : result;
Тут функции биндятся к target (т.е. к экземпляру Color), так как иначе внутри они получат this = Proxy.
После создания такого конструктора нам становится доступен требуемый синтаксис:
const colors = new Colors(); colors['red'] = …; colors['yellow'] = …; colors.has('red'); // true console.log(colors['yellow']);
Несмотря на то, что мы добились требуемого результата, код на JS выглядит скорее как костыль и использовать его я бы рекомендовал только если вы переносите существующий код с другого языка (C#, Delphi), где такие свойства активно использовались.
В свою очередь, мне было бы интересно узнать, есть ли еще способы реализовать [] нотацию для вызова геттера/сеттера без использования Proxy.
