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

Proxy и Reflect

В форме регистрации ты хочешь, чтобы при записи некорректного email немедленно бросалась ошибка — ещё до отправки на сервер. Можно добавить проверку в каждый сеттер вручную, а можно обернуть объект данных в Proxy и перехватывать все записи в одном месте. Так работает реактивность во Vue 3.

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

  • «Объекты» — свойства объектов, чтение и запись
  • «Дескрипторы свойств» — управление доступом к свойствам
  • «Пользовательские ошибки» — TypeError, RangeError при некорректных данных
  • «Прототипы» — Proxy работает как обёртка вокруг любого объекта
  • new Proxy(target, handler)

    Прокси создаётся с двумя аргументами: целевой объект target и обработчик handler с ловушками:

    const target = { name: 'Иван' }
    
    const proxy = new Proxy(target, {
      get(target, prop) {
        console.log(`Читаем: ${prop}`)
        return target[prop]
      }
    })
    
    console.log(proxy.name)  // Читаем: name → 'Иван'

    Ловушки (traps)

    get — перехват чтения свойства

    const config = new Proxy({}, {
      get(target, prop) {
        if (prop in target) return target[prop]
        return `Свойство '${prop}' не найдено`  // дефолт вместо undefined
      }
    })
    
    config.apiUrl = 'https://api.example.com'
    console.log(config.apiUrl)    // 'https://api.example.com'
    console.log(config.timeout)   // "Свойство 'timeout' не найдено"

    set — перехват записи

    const userSchema = new Proxy({}, {
      set(target, prop, value) {
        if (prop === 'age') {
          if (typeof value !== 'number') throw new TypeError('Возраст должен быть числом')
          if (value < 0 || value > 150) throw new RangeError(`Недопустимый возраст: ${value}`)
        }
        target[prop] = value
        return true  // обязательно возвращать true при успехе
      }
    })
    
    userSchema.name = 'Мария'   // OK
    userSchema.age = 25          // OK
    // userSchema.age = -5       // RangeError!

    has — перехват оператора in

    const hiddenProps = new Proxy({ _secret: 'abc', public: 'data' }, {
      has(target, prop) {
        if (prop.startsWith('_')) return false  // скрываем приватные свойства
        return prop in target
      }
    })
    
    console.log('public' in hiddenProps)  // true
    console.log('_secret' in hiddenProps) // false — скрыто!

    deleteProperty и apply

    // Запрет удаления важных ключей:
    const locked = new Proxy({ id: 1, name: 'Продукт' }, {
      deleteProperty(target, prop) {
        if (prop === 'id') throw new Error('Нельзя удалить id!')
        return delete target[prop]
      }
    })
    
    // Перехват вызова функции:
    function multiply(a, b) { return a * b }
    const loggedMultiply = new Proxy(multiply, {
      apply(target, thisArg, args) {
        console.log(`Вызов с аргументами: ${args}`)
        return target.apply(thisArg, args)
      }
    })
    
    console.log(loggedMultiply(3, 4))  // Вызов с аргументами: 3,4 → 12

    Reflect API

    Reflect предоставляет те же операции, что и стандартные JS-операторы, но в виде методов. Удобно внутри ловушек Proxy: перехватываешь операцию, делаешь что нужно, передаёшь дальше:

    const obj = { value: 42 }
    
    const loggingProxy = new Proxy(obj, {
      get(target, prop, receiver) {
        console.log(`GET: ${prop}`)
        return Reflect.get(target, prop, receiver)  // аналог target[prop]
      },
      set(target, prop, value, receiver) {
        console.log(`SET: ${prop} = ${JSON.stringify(value)}`)
        return Reflect.set(target, prop, value, receiver)  // аналог target[prop] = value
      }
    })

    Прокси с отрицательными индексами для массива

    function createFlexArray(...items) {
      return new Proxy(items, {
        get(target, prop) {
          const index = Number(prop)
          if (Number.isInteger(index) && index < 0) {
            return target[target.length + index]
          }
          return Reflect.get(target, prop)
        }
      })
    }
    
    const tags = createFlexArray('новый', 'активный', 'завершён', 'архив')
    console.log(tags[-1])  // 'архив'
    console.log(tags[-2])  // 'завершён'
    console.log(tags[0])   // 'новый'

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

    1. Забыть вернуть true из ловушки set — в strict mode будет TypeError:

    // Плохо: ловушка set ничего не возвращает
    const p = new Proxy({}, {
      set(target, prop, value) {
        target[prop] = value
        // забыли return true!
      }
    })
    // В strict mode: TypeError: 'set' on proxy: trap returned falsish
    
    // Хорошо: всегда return true
    const p2 = new Proxy({}, {
      set(target, prop, value) {
        target[prop] = value
        return true
      }
    })

    2. Прокси вокруг примитива — TypeError:

    // Плохо: Proxy работает только с объектами
    const p = new Proxy(42, {})  // TypeError: Cannot create proxy with a non-object as target
    
    // Хорошо: target должен быть объектом или функцией
    const p2 = new Proxy({ value: 42 }, {})

    3. Бесконечная рекурсия — чтение target[prop] внутри get вместо Reflect.get:

    // Плохо: если target — тоже прокси, можно попасть в рекурсию
    const p = new Proxy(obj, {
      get(target, prop) {
        return target[prop]  // потенциальная рекурсия при вложенных прокси
      }
    })
    
    // Хорошо: используй Reflect.get с receiver
    const p2 = new Proxy(obj, {
      get(target, prop, receiver) {
        return Reflect.get(target, prop, receiver)  // корректно обрабатывает цепочку
      }
    })

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

  • Vue 3 — вся реактивность построена на Proxy: отслеживает чтение/запись свойств компонентов
  • Валидация форм — схема валидации в обработчике set
  • Логирование — автоматическая запись всех изменений состояния
  • Кэширование — перехват get для ленивых вычислений
  • Мокирование в тестах — Proxy создаёт объект, отслеживающий все вызовы
  • Примеры

    Валидирующий прокси — выбрасывает ошибку при записи некорректных значений в профиль пользователя

    // Схема валидации для профиля пользователя
    const userValidators = {
      name(value) {
        if (typeof value !== 'string') throw new TypeError('Имя должно быть строкой')
        if (value.trim().length < 2)   throw new RangeError('Имя слишком короткое')
      },
      age(value) {
        if (typeof value !== 'number') throw new TypeError('Возраст должен быть числом')
        if (value < 0 || value > 150)  throw new RangeError(`Недопустимый возраст: ${value}`)
      },
      email(value) {
        if (typeof value !== 'string')           throw new TypeError('Email должен быть строкой')
        if (!/^[^@]+@[^@]+\.[^@]+$/.test(value)) throw new Error(`Некорректный email: ${value}`)
      },
      balance(value) {
        if (typeof value !== 'number') throw new TypeError('Баланс должен быть числом')
        if (value < 0)                 throw new RangeError('Баланс не может быть отрицательным')
      },
    }
    
    function createValidatedProfile(validators) {
      const data = {}
    
      return new Proxy(data, {
        set(target, prop, value) {
          if (validators[prop]) {
            validators[prop](value)  // выбрасывает ошибку если невалидно
          }
          return Reflect.set(target, prop, value)
        },
        get(target, prop) {
          if (prop === 'toJSON') {
            return () => ({ ...target })
          }
          return Reflect.get(target, prop)
        }
      })
    }
    
    const profile = createValidatedProfile(userValidators)
    
    // Корректные данные
    profile.name = 'Елена Иванова'
    profile.age = 32
    profile.email = 'elena@company.ru'
    profile.balance = 15000
    profile.city = 'Москва'  // нет валидатора — записывается свободно
    
    console.log(profile.name)    // 'Елена Иванова'
    console.log(profile.age)     // 32
    console.log(profile.balance) // 15000
    
    // Некорректные данные — ловим ошибки
    const errors = []
    
    try { profile.age = -5 }
    catch (e) { errors.push(`age=-5: ${e.message}`) }
    
    try { profile.email = 'не-email' }
    catch (e) { errors.push(`email='не-email': ${e.message}`) }
    
    try { profile.balance = -100 }
    catch (e) { errors.push(`balance=-100: ${e.message}`) }
    
    try { profile.name = 'А' }
    catch (e) { errors.push(`name='А': ${e.message}`) }
    
    errors.forEach(msg => console.log('Ошибка:', msg))
    // Ошибка: age=-5: Недопустимый возраст: -5
    // Ошибка: email='не-email': Некорректный email: не-email
    // Ошибка: balance=-100: Баланс не может быть отрицательным
    // Ошибка: name='А': Имя слишком короткое
    
    // Данные не изменились после ошибок:
    console.log(profile.age)     // 32
    console.log(profile.balance) // 15000

    Proxy и Reflect

    В форме регистрации ты хочешь, чтобы при записи некорректного email немедленно бросалась ошибка — ещё до отправки на сервер. Можно добавить проверку в каждый сеттер вручную, а можно обернуть объект данных в Proxy и перехватывать все записи в одном месте. Так работает реактивность во Vue 3.

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

  • «Объекты» — свойства объектов, чтение и запись
  • «Дескрипторы свойств» — управление доступом к свойствам
  • «Пользовательские ошибки» — TypeError, RangeError при некорректных данных
  • «Прототипы» — Proxy работает как обёртка вокруг любого объекта
  • new Proxy(target, handler)

    Прокси создаётся с двумя аргументами: целевой объект target и обработчик handler с ловушками:

    const target = { name: 'Иван' }
    
    const proxy = new Proxy(target, {
      get(target, prop) {
        console.log(`Читаем: ${prop}`)
        return target[prop]
      }
    })
    
    console.log(proxy.name)  // Читаем: name → 'Иван'

    Ловушки (traps)

    get — перехват чтения свойства

    const config = new Proxy({}, {
      get(target, prop) {
        if (prop in target) return target[prop]
        return `Свойство '${prop}' не найдено`  // дефолт вместо undefined
      }
    })
    
    config.apiUrl = 'https://api.example.com'
    console.log(config.apiUrl)    // 'https://api.example.com'
    console.log(config.timeout)   // "Свойство 'timeout' не найдено"

    set — перехват записи

    const userSchema = new Proxy({}, {
      set(target, prop, value) {
        if (prop === 'age') {
          if (typeof value !== 'number') throw new TypeError('Возраст должен быть числом')
          if (value < 0 || value > 150) throw new RangeError(`Недопустимый возраст: ${value}`)
        }
        target[prop] = value
        return true  // обязательно возвращать true при успехе
      }
    })
    
    userSchema.name = 'Мария'   // OK
    userSchema.age = 25          // OK
    // userSchema.age = -5       // RangeError!

    has — перехват оператора in

    const hiddenProps = new Proxy({ _secret: 'abc', public: 'data' }, {
      has(target, prop) {
        if (prop.startsWith('_')) return false  // скрываем приватные свойства
        return prop in target
      }
    })
    
    console.log('public' in hiddenProps)  // true
    console.log('_secret' in hiddenProps) // false — скрыто!

    deleteProperty и apply

    // Запрет удаления важных ключей:
    const locked = new Proxy({ id: 1, name: 'Продукт' }, {
      deleteProperty(target, prop) {
        if (prop === 'id') throw new Error('Нельзя удалить id!')
        return delete target[prop]
      }
    })
    
    // Перехват вызова функции:
    function multiply(a, b) { return a * b }
    const loggedMultiply = new Proxy(multiply, {
      apply(target, thisArg, args) {
        console.log(`Вызов с аргументами: ${args}`)
        return target.apply(thisArg, args)
      }
    })
    
    console.log(loggedMultiply(3, 4))  // Вызов с аргументами: 3,4 → 12

    Reflect API

    Reflect предоставляет те же операции, что и стандартные JS-операторы, но в виде методов. Удобно внутри ловушек Proxy: перехватываешь операцию, делаешь что нужно, передаёшь дальше:

    const obj = { value: 42 }
    
    const loggingProxy = new Proxy(obj, {
      get(target, prop, receiver) {
        console.log(`GET: ${prop}`)
        return Reflect.get(target, prop, receiver)  // аналог target[prop]
      },
      set(target, prop, value, receiver) {
        console.log(`SET: ${prop} = ${JSON.stringify(value)}`)
        return Reflect.set(target, prop, value, receiver)  // аналог target[prop] = value
      }
    })

    Прокси с отрицательными индексами для массива

    function createFlexArray(...items) {
      return new Proxy(items, {
        get(target, prop) {
          const index = Number(prop)
          if (Number.isInteger(index) && index < 0) {
            return target[target.length + index]
          }
          return Reflect.get(target, prop)
        }
      })
    }
    
    const tags = createFlexArray('новый', 'активный', 'завершён', 'архив')
    console.log(tags[-1])  // 'архив'
    console.log(tags[-2])  // 'завершён'
    console.log(tags[0])   // 'новый'

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

    1. Забыть вернуть true из ловушки set — в strict mode будет TypeError:

    // Плохо: ловушка set ничего не возвращает
    const p = new Proxy({}, {
      set(target, prop, value) {
        target[prop] = value
        // забыли return true!
      }
    })
    // В strict mode: TypeError: 'set' on proxy: trap returned falsish
    
    // Хорошо: всегда return true
    const p2 = new Proxy({}, {
      set(target, prop, value) {
        target[prop] = value
        return true
      }
    })

    2. Прокси вокруг примитива — TypeError:

    // Плохо: Proxy работает только с объектами
    const p = new Proxy(42, {})  // TypeError: Cannot create proxy with a non-object as target
    
    // Хорошо: target должен быть объектом или функцией
    const p2 = new Proxy({ value: 42 }, {})

    3. Бесконечная рекурсия — чтение target[prop] внутри get вместо Reflect.get:

    // Плохо: если target — тоже прокси, можно попасть в рекурсию
    const p = new Proxy(obj, {
      get(target, prop) {
        return target[prop]  // потенциальная рекурсия при вложенных прокси
      }
    })
    
    // Хорошо: используй Reflect.get с receiver
    const p2 = new Proxy(obj, {
      get(target, prop, receiver) {
        return Reflect.get(target, prop, receiver)  // корректно обрабатывает цепочку
      }
    })

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

  • Vue 3 — вся реактивность построена на Proxy: отслеживает чтение/запись свойств компонентов
  • Валидация форм — схема валидации в обработчике set
  • Логирование — автоматическая запись всех изменений состояния
  • Кэширование — перехват get для ленивых вычислений
  • Мокирование в тестах — Proxy создаёт объект, отслеживающий все вызовы
  • Примеры

    Валидирующий прокси — выбрасывает ошибку при записи некорректных значений в профиль пользователя

    // Схема валидации для профиля пользователя
    const userValidators = {
      name(value) {
        if (typeof value !== 'string') throw new TypeError('Имя должно быть строкой')
        if (value.trim().length < 2)   throw new RangeError('Имя слишком короткое')
      },
      age(value) {
        if (typeof value !== 'number') throw new TypeError('Возраст должен быть числом')
        if (value < 0 || value > 150)  throw new RangeError(`Недопустимый возраст: ${value}`)
      },
      email(value) {
        if (typeof value !== 'string')           throw new TypeError('Email должен быть строкой')
        if (!/^[^@]+@[^@]+\.[^@]+$/.test(value)) throw new Error(`Некорректный email: ${value}`)
      },
      balance(value) {
        if (typeof value !== 'number') throw new TypeError('Баланс должен быть числом')
        if (value < 0)                 throw new RangeError('Баланс не может быть отрицательным')
      },
    }
    
    function createValidatedProfile(validators) {
      const data = {}
    
      return new Proxy(data, {
        set(target, prop, value) {
          if (validators[prop]) {
            validators[prop](value)  // выбрасывает ошибку если невалидно
          }
          return Reflect.set(target, prop, value)
        },
        get(target, prop) {
          if (prop === 'toJSON') {
            return () => ({ ...target })
          }
          return Reflect.get(target, prop)
        }
      })
    }
    
    const profile = createValidatedProfile(userValidators)
    
    // Корректные данные
    profile.name = 'Елена Иванова'
    profile.age = 32
    profile.email = 'elena@company.ru'
    profile.balance = 15000
    profile.city = 'Москва'  // нет валидатора — записывается свободно
    
    console.log(profile.name)    // 'Елена Иванова'
    console.log(profile.age)     // 32
    console.log(profile.balance) // 15000
    
    // Некорректные данные — ловим ошибки
    const errors = []
    
    try { profile.age = -5 }
    catch (e) { errors.push(`age=-5: ${e.message}`) }
    
    try { profile.email = 'не-email' }
    catch (e) { errors.push(`email='не-email': ${e.message}`) }
    
    try { profile.balance = -100 }
    catch (e) { errors.push(`balance=-100: ${e.message}`) }
    
    try { profile.name = 'А' }
    catch (e) { errors.push(`name='А': ${e.message}`) }
    
    errors.forEach(msg => console.log('Ошибка:', msg))
    // Ошибка: age=-5: Недопустимый возраст: -5
    // Ошибка: email='не-email': Некорректный email: не-email
    // Ошибка: balance=-100: Баланс не может быть отрицательным
    // Ошибка: name='А': Имя слишком короткое
    
    // Данные не изменились после ошибок:
    console.log(profile.age)     // 32
    console.log(profile.balance) // 15000

    Задание

    В конфигурационном модуле нужна защита: после загрузки настройки нельзя менять. Напиши функцию readOnly(obj), которая возвращает Proxy, где любая попытка установить или удалить свойство выбрасывает TypeError с сообщением "Object is read-only". Чтение свойств должно работать нормально.

    Подсказка

    В ловушках set и deleteProperty просто выбрасывай throw new TypeError('Object is read-only'). Ловушку get не трогаем — чтение работает стандартно через target[prop]. Не забудь вернуть true из set если нужно разрешить запись (здесь не нужно — сразу throw).

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