Décorer un objet avec les Proxy

Voir la vidéo

JavaScript côté navigateur

Description Sommaire

Les Proxy permettent de décorer un objet en JavaScript pour venir intercepter les accès et les écritures dans ses propriétés.

Sommaire

00:00 Principe général
04:07 Cas d'usage : Valider des propriétés
07:19 Cas d'usage : Valeur par défaut
10:58 Cas d'usage : Réactivité
13:40 Quelques détails

Utilisation

Un Proxy s'initialise comme un objet classique avec 2 paramètre :

  • L'objet à décorer
  • Un handler, sous forme d'objet, qui contient différentes méthodes qui vont permettre d'intercepter certaines choses.
const obj = {
    name: 'John'
}

const p = new Proxy(obj, {})

On peut intercepter les accès, en implémentant la méthode get. On peut aussi utiliser la réflexion pour renvoyer la valeur originale.

const p = new Proxy(obj, {
    get (target, prop) {
        console.log('Il y a un accès à la propriété ' + prop)
        return Reflect.get(...arguments)
    }
})

De la même manière, la méthode set permet d'intercepter les écritures.

const p = new Proxy(obj, {
    set (target, prop, value) {
        console.log('On modifier la valeur de ' + prop, value)
        return Reflect.set(...arguments)
    }
})

Ce sont les 2 méthodes principales mais il existe d'autres méthodes sur les handlers qui permettent d'intercepter d'autres choses sur l'objet original.

Cas d'usage

Maintenant que l'on a les grandes lignes je vous propose quelques cas concret d'utilisation.

Objet avec validation

Les proxy peuvent être un bon moyen de décorer un objet pour ajouter des validations au niveau de ses propriétées.

function withValidation(obj, rules) {
  const rulesMap = new Map(Object.entries(rules))
  return new Proxy(obj, {
    set (target, prop, value) {
      if (rulesMap.has(prop) && !rulesMap.get(prop)(value)) {
        throw new Error(`La propriété ${prop} ne peut pas contenir la valeur ${value}`)
      }
      return Reflect.set(...arguments)
    }
  })
}

const person = {
  name: 'John',
  lastname: 'Doe',
  age: 18
}

// On restreint certaines propriétées
const restrictedPerson = withValidation(person, {
  age: (age) => age > 18 && age < 120,
  name: (name) => name.length > 3 && name.length < 100 
})

updateUser(restrictedPerson)

Objet avec défaut

Un autre cas d'usage peut être de créer un objet qui a une valeur par défaut sur les propriétés qui n'existent pas. Par exemple on veut compter la distribution de noms.

const countNames = {
  'Jane': 1
}

countNames['John']++
countNames['John']++
countNames['Jane']++
countNames['John']++

console.log(countNames) // {Jane: 2, John: NaN}

Pour éviter les problème il faudrait vérifier en amont si une propriété existe avant de faire un assignement. Les proxy peuvent permettre de créer un objet particulier.

function withDefault(obj, initial) {
  return new Proxy(obj, {
    get(target, prop) {
      if (prop !== 'toJSON' && !Reflect.has(target, prop)) {
        Reflect.set(target, prop, structuredClone(initial))
      }
      return Reflect.get(...arguments)
    }
  })
}

const countNames = withDefault({
  'Jane': 1
}, 0)

countNames['John']++
countNames['John']++
countNames['Jane']++
countNames['John']++

console.log(countNames) // {Jane: 2, John: 3}

Objet réactif

Un autre cas d'usage, que l'on retrouve d'ailleurs dans des frameworks comme VueJS, est l'ajout d'effet de bord lors de la modification de certaines variables. Cela permet par exemple, dans le cas du code côté client, de muter le DOM lorsque l'on modifie l'état de l'application.

const state = {
    count: 0
}

const input = document.querySelector('input')!

document.getElementById('increment')!.addEventListener('click', () => {
    state.count++
})
document.getElementById('decrement')!.addEventListener('click', () => {
    state.count--
})
document.getElementById('reset')!.addEventListener('click', () => {
    state.count = 0
})

input.value = "0"

La variable state représente l'état de notre application et l'on souhaite que le champs afficher la bonne valeur, sans avoir à ajouter la logique de mis à jour du DOM à chaque fonction qui vient modifier count.

const state = new Proxy({
    count: 0
}, {
    set(target, props, value, receiver) {
        if (props === 'count') {
            input.value = value
        }
        return Reflect.set(...props)
    },
})

On décore notre état à l'aide d'un Proxy pour venir intercepter les écriture dans la propriété et on peut mettre à jour le DOM en fonction.

Date immutable

Cet exemple ne fera pas l'unanimité mais l'objectif est ici de créer une version immutable des Date en JavaScript tout en gardant l'objet Date de base. Ce cas d'utilisation sort un peu des règles des Proxy car on vient modifier le comportement de l'objet de base.

const immutDate = (date) => {
  return new Proxy(date, {
      get(target, prop) {
          // On fait en sorte que setXXX() renvoie une nouvelle date
          if (typeof prop === 'string' && prop.startsWith('set')) {
              return (...args) => {
                  const clone = new Date(date);
                  Reflect.get(clone, prop).apply(clone, args);
                  return immutDate(clone);
              };
          }
          // On intercepte les méthode addXXXX() pour créer des méthodes magiques (addMonth, addDate...)
          // addXXXX(n) = setXXXX(getXXXX() + n)
          if (typeof prop === 'string' && prop.startsWith('add')) {
              return (n) => {
                  const clone = new Date(date);
                  const method = prop.replace('add', 'set')
                  Reflect.get(clone, prop.replace('add', 'set')).apply(clone, [
                      Reflect.get(clone, prop.replace('add', 'get')).apply(clone) + n
                  ]);
                  return immutDate(clone);
              };
          }
          const value = Reflect.get(...arguments);
          // On rebind "this" pour cibler la date original
          if (typeof value === 'function') {
              return value.bind(target);
          }
          return value;
      }
  });
};

const date = immutDate(new Date(2024, 0, 1, 0, 0, 0))
const formatter = new Intl.DateTimeFormat('fr-FR', {dateStyle: 'medium'})
console.log(formatter.format(date.addDate(2).addFullYear(3).setMonth(1))) // 3 fév. 2027
console.log(formatter.format(date)) // 1 janv. 2024
Publié
Technologies utilisées
Auteur :
Grafikart
Partager