← JavaScript/Флаги и дескрипторы свойств#113 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Флаги и дескрипторы свойств

В SDK для платёжного процессора нужно сделать так, чтобы config.API_URL нельзя было случайно перезаписать, а служебное поле _buildId не попадало в JSON.stringify или Object.keys. Обычные свойства это не умеют — но дескрипторы умеют.

На основе предыдущих уроков

  • «Объекты» — создание свойств, перебор через for...in и Object.keys
  • «JSON» — JSON.stringify игнорирует non-enumerable свойства
  • «Object.keys/entries» — Object.keys возвращает только enumerable свойства
  • Object.getOwnPropertyDescriptor()

    Получить дескриптор конкретного свойства:

    const user = { name: 'Иван', age: 30 }
    
    Object.getOwnPropertyDescriptor(user, 'name')
    // {
    //   value: 'Иван',
    //   writable: true,      — можно изменять
    //   enumerable: true,    — виден в for...in и Object.keys()
    //   configurable: true   — можно удалять и переопределять
    // }

    Три флага

  • writable — разрешает или запрещает запись нового значения
  • enumerable — делает свойство видимым в for...in, Object.keys(), JSON.stringify()
  • configurable — разрешает удаление свойства и изменение его дескриптора
  • Object.defineProperty()

    Создаёт или изменяет свойство с явным заданием флагов:

    const config = {}
    
    Object.defineProperty(config, 'API_URL', {
      value: 'https://api.example.com',
      writable: false,      // нельзя перезаписать
      enumerable: true,
      configurable: false,  // нельзя удалить или переопределить
    })
    
    config.API_URL = 'другой URL'  // В strict mode — TypeError! Молча игнорируется иначе.
    console.log(config.API_URL)    // 'https://api.example.com'

    Object.defineProperties() — задать несколько сразу

    const app = {}
    
    Object.defineProperties(app, {
      version: { value: '2.1.0', writable: false, enumerable: true, configurable: false },
      _internalId: { value: 'x9f2', writable: false, enumerable: false, configurable: false },
    })
    
    Object.keys(app)  // ['version'] — _internalId не виден

    Object.freeze() и Object.seal()

    // freeze — полная заморозка: нельзя добавлять, удалять или изменять
    const settings = Object.freeze({ theme: 'dark', lang: 'ru' })
    settings.theme = 'light'  // молча игнорируется (или TypeError в strict)
    settings.newProp = 'x'    // то же самое
    console.log(settings.theme)  // 'dark'
    
    // seal — нельзя добавлять или удалять, но можно изменять
    const profile = Object.seal({ name: 'Иван', score: 0 })
    profile.score = 100   // OK — изменение разрешено
    profile.email = 'x'  // игнорируется — добавление запрещено

    Object.keys vs Object.getOwnPropertyNames

    const obj = { a: 1 }
    Object.defineProperty(obj, 'hidden', { value: 42, enumerable: false })
    
    Object.keys(obj)                  // ['a'] — только enumerable
    Object.getOwnPropertyNames(obj)   // ['a', 'hidden'] — все, включая non-enumerable

    Типичные ошибки

    1. Забыть, что defineProperty без `writable: true` создаёт read-only свойство по умолчанию:

    // Плохо: хотели обычное свойство, но получили read-only
    const obj = {}
    Object.defineProperty(obj, 'name', { value: 'Иван' })
    // writable по умолчанию = false!
    obj.name = 'Пётр'
    console.log(obj.name)  // 'Иван' — не изменилось!
    
    // Хорошо: явно укажи writable
    Object.defineProperty(obj, 'name', {
      value: 'Иван',
      writable: true,
      enumerable: true,
      configurable: true,
    })

    2. Object.freeze замораживает только первый уровень (shallow freeze):

    const config = Object.freeze({
      db: { host: 'localhost', port: 5432 }
    })
    
    config.db = {}         // игнорируется — первый уровень заморожен
    config.db.port = 9999  // РАБОТАЕТ — вложенный объект не заморожен!
    console.log(config.db.port)  // 9999
    
    // Для глубокой заморозки нужна рекурсия:
    function deepFreeze(obj) {
      Object.getOwnPropertyNames(obj).forEach(name => {
        if (typeof obj[name] === 'object' && obj[name] !== null) {
          deepFreeze(obj[name])
        }
      })
      return Object.freeze(obj)
    }

    3. configurable: false нельзя отменить:

    const obj = {}
    Object.defineProperty(obj, 'id', { value: 1, configurable: false })
    
    // Попытка изменить дескриптор — TypeError
    Object.defineProperty(obj, 'id', { writable: true })
    // TypeError: Cannot redefine property: id

    В реальных проектах

  • SDK конфигурация — API_URL, API_KEY делают read-only через writable: false
  • Константы HTTP-статусов — объект HTTP с non-configurable полями вместо const с объектом
  • Скрытые метаданные — _version, _buildId как non-enumerable: не попадают в JSON и логи
  • Полифилы — определяют методы на прототипах с enumerable: false, чтобы не ломать for...in
  • Примеры

    Конфиг приложения с read-only полями и скрытыми метаданными через defineProperty

    // Конфигурация приложения с защищёнными полями
    const appConfig = {
      apiUrl: 'https://api.myapp.ru',
      timeout: 5000,
    }
    
    // Добавляем скрытое служебное поле (не попадёт в JSON и Object.keys)
    Object.defineProperty(appConfig, '_buildId', {
      value: 'build-20240315-abc',
      writable: false,
      enumerable: false,   // скрыто от перечисления
      configurable: false,
    })
    
    // Делаем apiUrl read-only (критически важный параметр)
    Object.defineProperty(appConfig, 'apiUrl', {
      value: appConfig.apiUrl,
      writable: false,
      enumerable: true,
      configurable: false,
    })
    
    console.log('Ключи конфига:', Object.keys(appConfig))
    // ['apiUrl', 'timeout'] — _buildId скрыт
    
    console.log('Все свойства:', Object.getOwnPropertyNames(appConfig))
    // ['apiUrl', 'timeout', '_buildId']
    
    console.log('JSON:', JSON.stringify(appConfig))
    // '{"apiUrl":"https://api.myapp.ru","timeout":5000}' — _buildId не включён
    
    // Попытка изменить read-only поле — молча игнорируется
    appConfig.apiUrl = 'https://hacker.example.com'
    console.log(appConfig.apiUrl)  // 'https://api.myapp.ru' — не изменилось!
    
    // Служебное поле доступно напрямую (если знаешь имя)
    console.log(appConfig._buildId)  // 'build-20240315-abc'
    
    // Дескриптор
    const desc = Object.getOwnPropertyDescriptor(appConfig, 'apiUrl')
    console.log(desc.writable)      // false
    console.log(desc.configurable)  // false
    console.log(desc.enumerable)    // true

    Флаги и дескрипторы свойств

    В SDK для платёжного процессора нужно сделать так, чтобы config.API_URL нельзя было случайно перезаписать, а служебное поле _buildId не попадало в JSON.stringify или Object.keys. Обычные свойства это не умеют — но дескрипторы умеют.

    На основе предыдущих уроков

  • «Объекты» — создание свойств, перебор через for...in и Object.keys
  • «JSON» — JSON.stringify игнорирует non-enumerable свойства
  • «Object.keys/entries» — Object.keys возвращает только enumerable свойства
  • Object.getOwnPropertyDescriptor()

    Получить дескриптор конкретного свойства:

    const user = { name: 'Иван', age: 30 }
    
    Object.getOwnPropertyDescriptor(user, 'name')
    // {
    //   value: 'Иван',
    //   writable: true,      — можно изменять
    //   enumerable: true,    — виден в for...in и Object.keys()
    //   configurable: true   — можно удалять и переопределять
    // }

    Три флага

  • writable — разрешает или запрещает запись нового значения
  • enumerable — делает свойство видимым в for...in, Object.keys(), JSON.stringify()
  • configurable — разрешает удаление свойства и изменение его дескриптора
  • Object.defineProperty()

    Создаёт или изменяет свойство с явным заданием флагов:

    const config = {}
    
    Object.defineProperty(config, 'API_URL', {
      value: 'https://api.example.com',
      writable: false,      // нельзя перезаписать
      enumerable: true,
      configurable: false,  // нельзя удалить или переопределить
    })
    
    config.API_URL = 'другой URL'  // В strict mode — TypeError! Молча игнорируется иначе.
    console.log(config.API_URL)    // 'https://api.example.com'

    Object.defineProperties() — задать несколько сразу

    const app = {}
    
    Object.defineProperties(app, {
      version: { value: '2.1.0', writable: false, enumerable: true, configurable: false },
      _internalId: { value: 'x9f2', writable: false, enumerable: false, configurable: false },
    })
    
    Object.keys(app)  // ['version'] — _internalId не виден

    Object.freeze() и Object.seal()

    // freeze — полная заморозка: нельзя добавлять, удалять или изменять
    const settings = Object.freeze({ theme: 'dark', lang: 'ru' })
    settings.theme = 'light'  // молча игнорируется (или TypeError в strict)
    settings.newProp = 'x'    // то же самое
    console.log(settings.theme)  // 'dark'
    
    // seal — нельзя добавлять или удалять, но можно изменять
    const profile = Object.seal({ name: 'Иван', score: 0 })
    profile.score = 100   // OK — изменение разрешено
    profile.email = 'x'  // игнорируется — добавление запрещено

    Object.keys vs Object.getOwnPropertyNames

    const obj = { a: 1 }
    Object.defineProperty(obj, 'hidden', { value: 42, enumerable: false })
    
    Object.keys(obj)                  // ['a'] — только enumerable
    Object.getOwnPropertyNames(obj)   // ['a', 'hidden'] — все, включая non-enumerable

    Типичные ошибки

    1. Забыть, что defineProperty без `writable: true` создаёт read-only свойство по умолчанию:

    // Плохо: хотели обычное свойство, но получили read-only
    const obj = {}
    Object.defineProperty(obj, 'name', { value: 'Иван' })
    // writable по умолчанию = false!
    obj.name = 'Пётр'
    console.log(obj.name)  // 'Иван' — не изменилось!
    
    // Хорошо: явно укажи writable
    Object.defineProperty(obj, 'name', {
      value: 'Иван',
      writable: true,
      enumerable: true,
      configurable: true,
    })

    2. Object.freeze замораживает только первый уровень (shallow freeze):

    const config = Object.freeze({
      db: { host: 'localhost', port: 5432 }
    })
    
    config.db = {}         // игнорируется — первый уровень заморожен
    config.db.port = 9999  // РАБОТАЕТ — вложенный объект не заморожен!
    console.log(config.db.port)  // 9999
    
    // Для глубокой заморозки нужна рекурсия:
    function deepFreeze(obj) {
      Object.getOwnPropertyNames(obj).forEach(name => {
        if (typeof obj[name] === 'object' && obj[name] !== null) {
          deepFreeze(obj[name])
        }
      })
      return Object.freeze(obj)
    }

    3. configurable: false нельзя отменить:

    const obj = {}
    Object.defineProperty(obj, 'id', { value: 1, configurable: false })
    
    // Попытка изменить дескриптор — TypeError
    Object.defineProperty(obj, 'id', { writable: true })
    // TypeError: Cannot redefine property: id

    В реальных проектах

  • SDK конфигурация — API_URL, API_KEY делают read-only через writable: false
  • Константы HTTP-статусов — объект HTTP с non-configurable полями вместо const с объектом
  • Скрытые метаданные — _version, _buildId как non-enumerable: не попадают в JSON и логи
  • Полифилы — определяют методы на прототипах с enumerable: false, чтобы не ломать for...in
  • Примеры

    Конфиг приложения с read-only полями и скрытыми метаданными через defineProperty

    // Конфигурация приложения с защищёнными полями
    const appConfig = {
      apiUrl: 'https://api.myapp.ru',
      timeout: 5000,
    }
    
    // Добавляем скрытое служебное поле (не попадёт в JSON и Object.keys)
    Object.defineProperty(appConfig, '_buildId', {
      value: 'build-20240315-abc',
      writable: false,
      enumerable: false,   // скрыто от перечисления
      configurable: false,
    })
    
    // Делаем apiUrl read-only (критически важный параметр)
    Object.defineProperty(appConfig, 'apiUrl', {
      value: appConfig.apiUrl,
      writable: false,
      enumerable: true,
      configurable: false,
    })
    
    console.log('Ключи конфига:', Object.keys(appConfig))
    // ['apiUrl', 'timeout'] — _buildId скрыт
    
    console.log('Все свойства:', Object.getOwnPropertyNames(appConfig))
    // ['apiUrl', 'timeout', '_buildId']
    
    console.log('JSON:', JSON.stringify(appConfig))
    // '{"apiUrl":"https://api.myapp.ru","timeout":5000}' — _buildId не включён
    
    // Попытка изменить read-only поле — молча игнорируется
    appConfig.apiUrl = 'https://hacker.example.com'
    console.log(appConfig.apiUrl)  // 'https://api.myapp.ru' — не изменилось!
    
    // Служебное поле доступно напрямую (если знаешь имя)
    console.log(appConfig._buildId)  // 'build-20240315-abc'
    
    // Дескриптор
    const desc = Object.getOwnPropertyDescriptor(appConfig, 'apiUrl')
    console.log(desc.writable)      // false
    console.log(desc.configurable)  // false
    console.log(desc.enumerable)    // true

    Задание

    В сервисе конфигурации нужна функция makeReadOnly(obj), которая клонирует объект и делает все его свойства non-writable и non-configurable. Исходный объект должен остаться неизменным.

    Подсказка

    Object.getOwnPropertyDescriptors(obj) возвращает объект со всеми дескрипторами. Перебери ключи и для каждого поставь writable: false, configurable: false. Затем Object.defineProperties({}, descriptors) создаст новый объект с этими дескрипторами.

    Загружаем среду выполнения...
    Загружаем AI-помощника...