Акт 1: «Ядро» — Часть 11 (Прокси)

Это продолжение серии о JavaScript, начатой ​​здесь.

Иногда необходимо перехватить определенные операции над полями объекта. Для этого JavaScript предоставляет реализацию шаблона прокси, то есть фактический объект обертывается прокси, и только последний передается потребителям. Этот рассказ призван объяснить основные особенности этих прокси.

Прокси и вариант его использования:

Прокси — это элегантный инструмент для наблюдения за изменениями, внесенными в объект.

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

Вот пример перехвата get-операций всех полей:

const obj= {
  a: 1,
  b: 2
}
const objProx = new Proxy(obj, {
  get(target, prop, receiver){
    console.log(
     `accessing the prop: ${prop} with value {target[prop]}`
    );
    // accessing the prop: a with value 1
    return target[prop];
  }
});
const a = objProx.a;
console.log(a); // 1

Создается новый прокси-объект, обертывающий obj. Конструктор получает обработчик, который перехватывает все обращения к свойствам этого объекта.

То же самое можно сделать с модификациями объекта, перехватив set:

const obj= {
  a: 1,
  b: 2
}
const objProx = new Proxy(obj, {
    set (target, prop, value) {
    console.log(`setting prop: ${prop} to value: ${value}`);
    // setting prop: b to value: 3
    target[prop] = value;
  }
});
objProx.b = 3;
console.log(obj.b); // 3

Это самые важные «ловушки», которые можно перехватить. Есть еще много других, например deleteProperty , enumerate (при использовании for-in ), ownKeys (при использовании Object.keys ), defineProperty (при использовании Object.defineProperty ),…

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

const obj = {
  a: 1,
  b: {
    c: 2
  }
}
const objProx = new Proxy(obj, {
  set(target, prop, value) {
    console.log(`somebody sets prop: ${prop} to value: ${value}`);
    target[prop] = value;
  }
});
objProx.b.c = 3;
console.log(obj.b.c); // 3

Видите ли, никакой регистрации не произошло, так как значение b не изменилось. Если вы хотите иметь прокси, который также распознает глубокие изменения, вы можете написать такой код:

function createDeepProxy(o, onChange, path = '') {
  const internalProxies = Object.entries(o)
    .filter(([_, value]) => typeof value === 'object')
    .reduce(
      (prev, [prop, value]) =>
        ({ ...prev, [prop]: createDeepProxy(
            value, onChange,`${path}/${prop}`) }),
      {}
    );
  const prox = new Proxy(o, {
    set(target, prop, value) {
      if (target[prop] !== value) {
        onChange(path, prop, value);
      }
      target[prop] = value;
    },
    get(target, key) {
      return (key in internalProxies) ? 
              internalProxies[key] : target[key];
    }
  });
  return prox;
}

Эта функция создает рекурсивно прокси. То есть всякий раз, когда он обнаруживает свойство, не имеющее примитивного значения, он изменяет get для этого свойства настолько, что возвращает другой прокси. Метод можно применить следующим образом:

const obj = {
  a: 1,
  b: {
    c: 2,
    d: [1, 2, 3]
  }
}
const objProx = createDeepProxy(
  obj,
  (path, prop, val) => console.log(
      `change at ${path} for prop: ${prop}, new value: ${val}`
   )
);
objProx.b.c = 2; // no change
objProx.b.c = 3; // 'change at /b for prop: c, new value: 3'
objProx.a = 4; // 'change at  for prop: a, new value: 4'
objProx.b.d[1] = 7; // 'change at /b/d for prop: 1, new value: 7'

Это хорошее упражнение для понимания всех деталей реализации этого метода. Реализация далека от завершения и охватывает только простые объекты. Также обратите внимание, если некоторые значения исходного объекта равны null или undefined, определение typeof не работает. Более того, set-перехват работает над типами объектов только один раз. Например, в приведенном выше примере, если вы устанавливали

a.b = {c: 7}

затем, хотя это изменение перехватывается соответствующим обработчиком прокси, новое значение a.b больше не является прокси. Это можно исправить, назначив в обработчике новый экземпляр прокси, созданный createDeepProxy(value).

На этом пока все, и не забывайте оставлять комментарии по вопросам или ошибкам, которые вы обнаружите. Давайте посмотрим еще раз в следующей истории, если хотите.

Спасибо за чтение!