Другое

Создание самореферентных свойств в JavaScript-объектах

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

Как можно создать самореферентные свойства в литералах объектов JavaScript?

Есть ли способ заставить работать что‑то вроде следующего кода?

javascript
var foo = {
    a: 5,
    b: 6,
    c: this.a + this.b  // Не работает
};

В текущей форме этот код, очевидно, вызывает ошибку ссылки, поскольку this не ссылается на foo. Но есть ли способ, чтобы значения свойств в литерале объекта зависели от ранее объявленных свойств?

JavaScript‑объектные литералы напрямую не могут ссылаться на другие свойства внутри того же литерала с помощью this, потому что объект ещё не существует во время инициализации. Однако существует несколько обходных путей, позволяющих добиться самореференциального поведения в объектах JavaScript.


Содержание


Почему самореференции не работают в объектных литералах

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

javascript
var foo = {
    a: 5,
    b: 6,
    c: this.a + this.b  // Не работает
};

Ключевое слово this ссылается на контекст, который существует в момент создания объекта, а не на сам объект foo. Как объясняется в ответе на Stack Overflow, «Никакого способа внутри инициализатора объекта обратиться к другому ключу того же объекта, потому что нет ссылки на создаваемый объект до завершения инициализации».

Эта ограниченность возникает потому, что объектные литералы оцениваются как одно выражение, и объект появляется только после того, как весь литерал полностью обработан.


Подход с фабричными функциями

Самый распространённый и гибкий способ — использовать фабричные функции, которые создают и возвращают объекты со самореференциальными свойствами.

javascript
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}

Преимущества:

  • Ясное разделение логики создания объекта
  • Позволяет реализовать сложную инициализацию
  • Легко обрабатывает зависимости между свойствами
  • Поддерживает приватные переменные и инкапсуляцию

Расширенная фабричная функция с умолчаниями:

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

javascript
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

Решение с геттерами

Ещё один элегантный подход — использовать геттеры, которые вычисляют значения «на лету» на основе других свойств.

javascript
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, «это можно реализовать с помощью геттера»

Комбинация с сеттерами:

javascript
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‑вычисляемые свойства в основном предназначены для имён свойств, но их можно комбинировать с другими техниками:

javascript
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:

javascript
var propName = 'dynamicProp';
var foo = {
    a: 5,
    b: 6,
    [propName]: this.a + this.b  // Всё равно не сработает для значений
};

Метод с временной переменной

Можно создать временную ссылку, чтобы держать объект во время создания:

javascript
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;

Или более элегантно:

javascript
var foo = (() => {
    const obj = { a: 5, b: 6 };
    obj.c = obj.a + obj.b;
    return obj;
})();

Лучшие практики и рекомендации

Когда использовать каждый подход:

  1. Фабричные функции – лучше всего для сложных объектов с логикой инициализации, приватными переменными или при необходимости создавать несколько подобных объектов.
  2. Геттеры – идеальны для вычисляемых свойств, которые должны автоматически обновляться при изменении зависимостей, либо когда хочется сохранить структуру объекта чистой.
  3. Вычисляемые свойства – подходят для простых статических вычислений во время создания объекта.
  4. Временные переменные – полезны при однократном создании объекта, когда нужно ссылаться на другие свойства.

Проблемы производительности:

  • Геттеры имеют небольшую накладную стоимость, так как вычисляют значения при каждом обращении.
  • Фабричные функции почти не влияют на производительность после создания объекта.
  • Предвычисленные свойства (используя временную переменную) быстрее при доступе, но не обновляются автоматически.

Современные паттерны JavaScript:

С ES6+ можно комбинировать подходы для более чистого кода:

javascript
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 не могут напрямую ссылаться на другие свойства во время создания из‑за ограничений области видимости, существует несколько эффективных обходных путей:

  1. Фабричные функции предоставляют наиболее гибкое и поддерживаемое решение для сложного создания объектов с самореференциальными свойствами.
  2. Геттеры дают элегантные вычисляемые значения, которые автоматически обновляются при изменении зависимостей.
  3. Вычисляемые свойства с временной переменной подходят для статических вычислений во время создания.
  4. Классы объединяют преимущества фабрик с современным синтаксисом JavaScript.

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


Источники

  1. Self‑references in object literals / initializers - Stack Overflow
  2. How can a JavaScript object refer to values in itself? - Stack Overflow
  3. Self referencing object literal in JavaScript – clubmate.fi
  4. JavaScript Factory Functions with ES6+ - Eric Elliott | Medium
  5. Object initializer - JavaScript | MDN
  6. Factory Function Pattern In‑Depth - Ronald Chen | Medium
Авторы
Проверено модерацией
Модерация