В некоторых современных объектно-ориентированных языках есть понятие идексаторов – свойств, позволяющих работать с экземпляром класса как с массивом, используя [] нотацию. В этой статье я хочу продемонстрировать как это сделать на современном 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.