Создание самореферентных свойств в JavaScript-объектах
Узнайте, как в литералах объектов JavaScript создавать свойства, ссылающиеся на другие свойства, используя фабричные функции, геттеры и вычисляемые свойства.
Как можно создать самореферентные свойства в литералах объектов JavaScript?
Есть ли способ заставить работать что‑то вроде следующего кода?
var foo = {
a: 5,
b: 6,
c: this.a + this.b // Не работает
};
В текущей форме этот код, очевидно, вызывает ошибку ссылки, поскольку this не ссылается на foo. Но есть ли способ, чтобы значения свойств в литерале объекта зависели от ранее объявленных свойств?
JavaScript‑объектные литералы напрямую не могут ссылаться на другие свойства внутри того же литерала с помощью this, потому что объект ещё не существует во время инициализации. Однако существует несколько обходных путей, позволяющих добиться самореференциального поведения в объектах JavaScript.
Содержание
- Почему самореференции не работают в объектных литералах
- Подход с фабричными функциями
- Решение с геттерами
- Подход с вычисляемыми свойствами и квадратными скобками
- Метод с временной переменной
- Лучшие практики и рекомендации
Почему самореференции не работают в объектных литералах
Основная проблема самореференций в объектных литералах заключается в том, что синтаксис литерала не предоставляет способа ссылаться на создаваемый объект во время инициализации. Когда вы пишете:
var foo = {
a: 5,
b: 6,
c: this.a + this.b // Не работает
};
Ключевое слово this ссылается на контекст, который существует в момент создания объекта, а не на сам объект foo. Как объясняется в ответе на Stack Overflow, «Никакого способа внутри инициализатора объекта обратиться к другому ключу того же объекта, потому что нет ссылки на создаваемый объект до завершения инициализации».
Эта ограниченность возникает потому, что объектные литералы оцениваются как одно выражение, и объект появляется только после того, как весь литерал полностью обработан.
Подход с фабричными функциями
Самый распространённый и гибкий способ — использовать фабричные функции, которые создают и возвращают объекты со самореференциальными свойствами.
function createFoo() {
const obj = {
a: 5,
b: 6
};
obj.c = obj.a + obj.b;
obj.d = obj.c * 2;
return obj;
}
var foo = createFoo();
console.log(foo); // {a: 5, b: 6, c: 11, d: 22}
Преимущества:
- Ясное разделение логики создания объекта
- Позволяет реализовать сложную инициализацию
- Легко обрабатывает зависимости между свойствами
- Поддерживает приватные переменные и инкапсуляцию
Расширенная фабричная функция с умолчаниями:
Как упомянуто в статье Эрика Эллиотта о фабричных функциях, можно использовать параметры по умолчанию и вычисляемые геттеры:
function createFoo({ a = 5, b = 6 } = {}) {
const obj = { a, b };
return {
...obj,
get sum() { return this.a + this.b; },
get doubleSum() { return this.sum * 2; }
};
}
var foo = createFoo();
console.log(foo.sum); // 11
console.log(foo.doubleSum); // 22
Решение с геттерами
Ещё один элегантный подход — использовать геттеры, которые вычисляют значения «на лету» на основе других свойств.
var foo = {
a: 5,
b: 6,
get sum() {
return this.a + this.b;
},
get doubleSum() {
return this.sum * 2;
}
};
console.log(foo.sum); // 11
console.log(foo.doubleSum); // 22
Преимущества:
- Значения вычисляются лениво при обращении
- Структура объекта остаётся чистой
- Автоматически обновляется при изменении зависимых свойств
- Как отмечено в статье clubmate.fi, «это можно реализовать с помощью геттера»
Комбинация с сеттерами:
var foo = {
_a: 5,
_b: 6,
get a() { return this._a; },
set a(value) { this._a = value; },
get b() { return this._b; },
set b(value) { this._b = value; },
get sum() { return this.a + this.b; }
};
foo.a = 10;
console.log(foo.sum); // 16 (автоматически обновляется)
Подход с вычисляемыми свойствами
ES6‑вычисляемые свойства в основном предназначены для имён свойств, но их можно комбинировать с другими техниками:
const base = { a: 5, b: 6 };
const computedProps = {
sum: base.a + base.b,
doubleSum: (base.a + base.b) * 2
};
var foo = { ...base, ...computedProps };
console.log(foo); // {a: 5, b: 6, sum: 11, doubleSum: 22}
Для динамических имён вычисляемых свойств (а не значений) можно использовать квадратные скобки, как описано в документации MDN:
var propName = 'dynamicProp';
var foo = {
a: 5,
b: 6,
[propName]: this.a + this.b // Всё равно не сработает для значений
};
Метод с временной переменной
Можно создать временную ссылку, чтобы держать объект во время создания:
var foo = {
a: 5,
b: 6,
c: function() {
return this.a + this.b;
}()
};
// Но это всё равно не сработает корректно, потому что 'this' не привязан к foo
// Вместо этого используйте временную переменную:
var temp = { a: 5, b: 6 };
temp.c = temp.a + temp.b;
var foo = temp;
Или более элегантно:
var foo = (() => {
const obj = { a: 5, b: 6 };
obj.c = obj.a + obj.b;
return obj;
})();
Лучшие практики и рекомендации
Когда использовать каждый подход:
- Фабричные функции – лучше всего для сложных объектов с логикой инициализации, приватными переменными или при необходимости создавать несколько подобных объектов.
- Геттеры – идеальны для вычисляемых свойств, которые должны автоматически обновляться при изменении зависимостей, либо когда хочется сохранить структуру объекта чистой.
- Вычисляемые свойства – подходят для простых статических вычислений во время создания объекта.
- Временные переменные – полезны при однократном создании объекта, когда нужно ссылаться на другие свойства.
Проблемы производительности:
- Геттеры имеют небольшую накладную стоимость, так как вычисляют значения при каждом обращении.
- Фабричные функции почти не влияют на производительность после создания объекта.
- Предвычисленные свойства (используя временную переменную) быстрее при доступе, но не обновляются автоматически.
Современные паттерны JavaScript:
С ES6+ можно комбинировать подходы для более чистого кода:
class Foo {
constructor({ a = 5, b = 6 } = {}) {
this.a = a;
this.b = b;
}
get sum() { return this.a + this.b; }
get doubleSum() { return this.sum * 2; }
}
// Или используя сокращённый синтаксис и вычисляемые свойства:
const createFoo = ({ a = 5, b = 6 } = {}) => ({
a,
b,
get sum() { return a + b; },
get doubleSum() { return this.sum * 2; }
});
Как отмечено в статье Ronald Chen о фабричных функциях, фабричные функции обеспечивают отличную инкапсуляцию и тестируемость, избегая сложностей с привязкой this.
Заключение
Хотя объектные литералы JavaScript не могут напрямую ссылаться на другие свойства во время создания из‑за ограничений области видимости, существует несколько эффективных обходных путей:
- Фабричные функции предоставляют наиболее гибкое и поддерживаемое решение для сложного создания объектов с самореференциальными свойствами.
- Геттеры дают элегантные вычисляемые значения, которые автоматически обновляются при изменении зависимостей.
- Вычисляемые свойства с временной переменной подходят для статических вычислений во время создания.
- Классы объединяют преимущества фабрик с современным синтаксисом JavaScript.
Выбор подхода зависит от конкретных требований: нужна ли автоматическая реакция на изменения, оптимизация производительности или удобство поддержки кода. В большинстве случаев фабричные функции являются наиболее надёжным и масштабируемым решением для работы с самореференциальными объектами в JavaScript.
Источники
- Self‑references in object literals / initializers - Stack Overflow
- How can a JavaScript object refer to values in itself? - Stack Overflow
- Self referencing object literal in JavaScript – clubmate.fi
- JavaScript Factory Functions with ES6+ - Eric Elliott | Medium
- Object initializer - JavaScript | MDN
- Factory Function Pattern In‑Depth - Ronald Chen | Medium