Прокси и отражение

JavaScript
Текст длиной почти 10 000 символов, не распыляйте~~

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

Прокси используется во многих библиотеках и некоторых браузерных фреймворках. В этой главе мы увидим много практических приложений.

грамматика:

let proxy = new Proxy(target, handler)
  • target-- объект для переноса, который может быть чем угодно, включая функции.
  • handler- Конфигурация прокси: объект с "хуками" ("ловушками", методами, перехватывающими операции). Напримерgetкрючок для чтенияtargetАтрибуты,setкрючок написатьtargetсвойства и т. д.

правильноproxyоперации, если вhandlerЕсли в нем есть соответствующий хук, он сработает и у прокси есть шанс его обработать, в противном случае он будет напрямую обрабатывать цель.

Для начала создадим прокси без всяких хуков:

let target = {};
let proxy = new Proxy(target, {}); // 空的handler对象

proxy.test = 5; // 写入 Proxy 对象 (1)
alert(target.test); // 返回 5,test属性出现在了 target 上!

alert(proxy.test); // 还是 5,我们也可以从 proxy 对象读取它 (2)

for(let key in proxy) alert(key); // 返回 test,迭代也正常工作! (3)

Поскольку крюков нет, все парыproxyоперации направляются непосредственноtarget.

  1. операция записиproxy.test=будет писать значениеtarget.
  2. операция чтенияproxy.testбудет отtargetВернуть соответствующее значение.
  3. повторятьproxyбудет отtargetВернуть соответствующее значение.

Как видим, без всяких крючков,proxyЯвляетсяtargetпрозрачная упаковка.


Proxyпредставляет собой особый вид «странного объекта». У него нет собственных свойств. еслиhandlerпусто, операция прозрачно перенаправляется наtarget.

Чтобы активировать больше функций, давайте добавим хуки.

Что мы можем перехватить с их помощью?

Для большинства операций над объектами в спецификации JavaScript существует так называемый «внутренний метод», который описывает, как работает самый нижний уровень. Например[[Get]], внутренний метод чтения свойств,[[Set]], внутренние методы записи свойств и т. д. Эти методы используются только в спецификации, мы не можем вызывать их напрямую по имени метода.

Прокси-хуки будут перехватывать вызовы этих методов. они вспецификация проксии перечислены в таблице ниже.

Для каждого внутреннего метода в этой таблице есть хук: можно использовать для добавления вnew ProxyвремяhandlerИмя метода для перехвата операции в параметре:

внутренний метод Метод обработчика когда запускать
[[Get]] get читать свойство
[[Set]] set написать свойство
[[HasProperty]] has inоператор
[[Delete]] deleteProperty deleteдействовать
[[Call]] apply Прокси-объект вызывается как функция
[[Construct]] construct newдействовать
[[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf
[[IsExtensible]] isExtensible Object.isExtensible
[[PreventExtensions]] preventExtensions Object.preventExtensions
[[DefineOwnProperty]] defineProperty Object.defineProperty, Object.defineProperties
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object/keys/values/entries
Invariants

JavaScript применяет определенные инварианты — когда операции должны выполняться внутренними методами и ловушками.

Большинство из них предназначены для возвращаемых значений:

  • [[Set]]Должен возвращаться, если значение было успешно записаноtrue, иначе возвратfalse.
  • [[Delete]]Если значение было успешно удалено, оно должно вернутьсяtrue, иначе возвратfalse.
  • ...и так далее, мы увидим больше в примерах ниже.

Есть некоторые другие инварианты, такие как:

  • [[GetPrototypeOf]], применяемый к прокси-объекту, должен возвращать тот же[[GetPrototypeOf]]То же значение применяется к проксируемому объекту. Другими словами, чтение прототипа прокси-объекта всегда должно возвращать прототип прокси-объекта.

Хуки могут перехватывать эти операции, но эти правила необходимо соблюдать.

Инварианты обеспечивают правильное и согласованное поведение языковых функций. Полный список инвариантов находится наТехнические характеристики. Если вы не будете делать странных вещей, вы не будете их нарушать.

Давайте посмотрим, как это работает на практическом примере.

Значение по умолчанию с хуком "get"

Наиболее распространенные хуки предназначены для чтения/записи свойств.

Чтобы перехватить операции чтения,handlerдолжен иметьget(target, property, receiver)метод.

Этот метод запускается при чтении свойства, и его параметры следующие:

  • target-- целевой объект, который передается в качестве первого аргументаnew Proxy,
  • property— имя целевого атрибута,
  • receiver-- если целевое свойство является свойством доступа геттера, тоreceiverЗдесь находится атрибут чтенияthisобъект. Обычно этоproxyСам объект (или, если мы наследуем от прокси, объект, который наследуется от этого прокси). Сейчас нам не нужен этот параметр, поэтому мы объясним его подробно позже.

давайте использоватьgetРеализует значение объекта по умолчанию.

Мы создадим массив, который возвращает 0 для элементов массива, которые не существуют.

Обычно, когда люди пытаются получить несуществующий элемент массива, они получаютundefined, но мы бы обернули обычный массив в прокси, чтобы поймать операцию чтения и вернуться без такого свойства0:

let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; // 默认值
    }
  }
});

alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (没有这样的元素)

Как мы видим, используйтеgetКрючки очень легкие.

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

Представьте, что у нас есть словарь с фразами и их переводами:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined

Теперь, если нет фразы, отdictionaryчтение вернетсяundefined. Однако на практике возврат непереведенной фразы обычно происходит быстрее, чемundefinedБудь хорошим. Так что давайте в этом случае вернем непереведенную фразу вместоundefined.

Для этого мы обернемdictionaryВведите прокси, который перехватывает операции чтения:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

dictionary = new Proxy(dictionary, {
  get(target, phrase) { // 拦截读取属性操作
    if (phrase in target) { //如果字典包含该短语
      return target[phrase]; // 返回译文
    } else {
      // 否则返回未翻译的短语
      return phrase;
    }
  }
});

// 在字典中查找任意短语!
// 最坏的情况也只是它们没有被翻译。
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy

Обратите внимание, как прокси перезаписывает переменную:

dictionary = new Proxy(dictionary, ...);

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

Подтвердить с помощью хука "set"

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

При написании свойствsetКрючковые триггеры.

set(target, property, value, receiver):

  • target-- целевой объект, который передается в качестве первого аргументаnew Proxy,
  • property— имя целевого атрибута,
  • value- значение, которое будет установлено для целевого свойства,
  • receiver-- а такжеgetХуки похожи, только связаны с аксессорами сеттера.

Если операция записи прошла успешно,setкрючок должен вернутьсяtrue, иначе возвратfalse(вызыватьTypeError).

Давайте используем его для проверки нового значения:

let numbers = [];

numbers = new Proxy(numbers, { // (*)
  set(target, prop, val) { // 拦截写入操作
    if (typeof val == 'number') {
      target[prop] = val;
      return true;
    } else {
      return false;
    }
  }
});

numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2

numbers.push("test"); // TypeError (proxy 的 `set` 操作返回 false)

alert("This line is never reached (error in the line above)");

Примечание. Встроенные методы Array по-прежнему работают! использование значенияpushметод добавления в массив. При добавлении значенийlengthАтрибуты автоматически увеличиваются. Наш прокси-объект Proxy ничего не ломает.

Нам не нужно переписывать такие вещи, какpushа такжеunshiftМетоды массива типа добавления элементов, в них можно добавлять проверки, т.к. внутренне они используют перехваченный прокси[[Set]]работать.

Поэтому код лаконичный и понятный.

не забудь вернутьсяtrue

Как упоминалось выше, сохраняйте инварианты.

дляsetоперация, она должна вернуться при успешной записиtrue.

Если мы забудем это сделать или вернем ложное значение, действие сработает.TypeError.

Итерация с «ownKeys» и «getOwnPropertyDescriptor»

Object.keys,for..inЦиклы и большинство других методов перебора свойств объекта используют[[OwnPropertyKeys]]внутренний метод (поownKeysперехват хука), чтобы получить список свойств.

Эти методы различаются в деталях:

  • Object.getOwnPropertyNames(obj)Возвращает несимвольные ключи.
  • Object.getOwnPropertySymbols(obj)Символ ключа возврата.
  • Object.keys/values()возвращается сenumerableПара ключ-значение тега, не являющаяся символом (теги свойств подробно описаны в главе «Флаги атрибутов и дескрипторы свойств»).
  • for..inперебрать всеenumerableНесимвольные ключи для тегов и ключи для объектов-прототипов.

...но все начинается с этого списка.

В приведенном ниже примере мы используемownKeysперехват крюкаfor..inправильноuserобход, также используяObject.keysа такжеObject.valuesчтобы пропустить подчеркивание_Свойства в начале:

let user = {
  name: "John",
  age: 30,
  _password: "***"
};

user = new Proxy(user, {
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// "ownKeys" 过滤掉 _password
for(let key in user) alert(key); // name,然后是 age

// 对这些方法同样有效:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30

Пока еще работает.

Хотя, если мы вернем ключ, которого нет в объекте,Object.keysне перечисляет ключ:

let user = { };

user = new Proxy(user, {
  ownKeys(target) {
    return ['a', 'b', 'c'];
  }
});

alert( Object.keys(user) ); // <empty>

Почему? причина проста:Object.keysвозвращается только сenumerableатрибут тега. Для проверки метод вызывается для каждого свойства[[GetOwnProperty]]чтобы получить дескриптор атрибута. Здесь, поскольку атрибута нет, его дескриптор пустой, нетenumerableотметьте, поэтому он будет пропущен.

чтобыObject.keysВозвращая свойство, нам нужно либо добавить свойство, либоenumerableТег хранится в объекте, либо мы можем перехватывать обращения к нему[[GetOwnProperty]](крюкgetOwnPropertyDescriptorсделает это) и возвращает дескриптор enumerable: true.

Вот пример:

let user = { };

user = new Proxy(user, {
  ownKeys(target) { // 一旦被调用,就返回一个属性列表
    return ['a', 'b', 'c'];
  },

  getOwnPropertyDescriptor(target, prop) { // 被每个属性调用
    return {
      enumerable: true,
      configurable: true
      /* 其他属性,类似于 "value:..." */
    };
  }

});

alert( Object.keys(user) ); // a, b, c

Еще раз отметим: если свойство не существует в объекте, нам просто нужно перехватить[[GetOwnProperty]].

Защищенные свойства с помощью «deleteProperty» и других хуков

Существует общее соглашение, которое подчеркивает_Префиксные свойства и методы являются внутренними. Доступ к ним не должен осуществляться извне объекта.

Технически это возможно:

let user = {
  name: "John",
  _password: "secret"
};

alert(user._password); // secret

Давайте использовать прокси, чтобы предотвратить_Любой доступ к свойствам, начинающимся с .

Нам понадобятся следующие крючки:

  • getПри чтении таких свойств выдается ошибка,
  • setПри записи свойства выдается ошибка,
  • deletePropertyВыдается ошибка при удалении свойства,
  • ownKeysВ использованииfor..inи подобныеObject.keysметод исключения_свойства в начале.

код показывает, как показано ниже:

let user = {
  name: "John",
  _password: "***"
};

user = new Proxy(user, {
  get(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    }
    let value = target[prop];
    return (typeof value === 'function') ? value.bind(target) : value; // (*)
  },
  set(target, prop, val) { // 拦截写入操作
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      target[prop] = val;
      return true;
    }
  },
  deleteProperty(target, prop) { // 拦截属性删除
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      delete target[prop];
      return true;
    }
  },
  ownKeys(target) { // 拦截读取属性列表
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// “get” 不允许读取 _password
try {
  alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }

//  “set” 不允许写入 _password
try {
  user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }

// “deleteProperty” 不允许删除 _password 属性
try {
  delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }

// “ownKeys” 过滤排除 _password
for(let key in user) alert(key); // name

Пожалуйста, обратите внимание на строку(*)серединаgetВажные детали крючков:

get(target, prop) {
  // ...
  let value = target[prop];
  return (typeof value === 'function') ? value.bind(target) : value; // (*)
}

Зачем нужен вызов функцииvalue.bind(target)?

Причина в том, что методы объекта (например,user.checkPassword()) должен иметь доступ_password:

user = {
  // ...
  checkPassword(value) {
    //对象方法必须能读取 _password
    return value === this._password;
  }
}

правильноuser.checkPassword()Вызов прокси-объекта будет вызванuserтак какthis(Объект перед оператором точки становитсяthis), поэтому, когда он пытается получить доступthis._passwordВремяgetХук активируется (срабатывает при чтении любого свойства) и выдает ошибку.

Поэтому мы делаем(*)для привязки контекста метода объекта к исходному объекту,target. Затем их будущие вызовы будут использоватьtargetтак какthis, не запускает никаких хуков.

Это решение обычно работает, но оно не идеально, потому что один метод может передать непроксированный объект куда-то еще, и тогда мы застрянем: где исходный объект, а где проксированный объект?

Кроме того, объект может быть подтвержден несколько раз (несколько прокси могут добавлять разные «твики» на объект), и если мы передаем развернутый объект к способу, могут быть непреднамеренные последствия.

Поэтому такой прокси нигде не должен использоваться.

частная собственность класса

Современные движки Javascript изначально поддерживают частные свойства, которые начинаются с#как префикс. Это подробно описано в главе Частные и защищенные свойства и методы. Прокси не требуется.

Однако у таких свойств есть свои проблемы. В частности, они не передаются по наследству.

Крючки «в пределах досягаемости» и «имеет»

Давайте рассмотрим еще несколько примеров.

У нас есть объект диапазона:

let range = {
  start: 1,
  end: 10
};

мы хотим использоватьinоператора, чтобы проверить, находится ли номер вrangeв пределах диапазона.

Долженhasперехват крюкаinпередача.

has(target, property)

  • target-- целевой объект, переданный в качестве первого параметраnew Proxy
  • property-- имя атрибута

Примеры следующие

let range = {
  start: 1,
  end: 10
};

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end
  }
});

alert(5 in range); // true
alert(50 in range); // false

Довольно синтаксический сахар, не так ли? И это очень просто реализовать.

Функция-обертка: «применить»

Мы также можем обернуть прокси вокруг функции.

apply(target, thisArg, args)Хуки позволяют вызывать прокси как функции:

  • targetявляется целевым объектом (функции являются объектами в JavaScript)
  • thisArgдаthisзначение
  • argsэто список параметров

Например, давайте вспомнимdelay(f, ms)декоратор, это то, что у нас есть вВыкройка декоратора, звоните/заявказавершена в одной главе.

В этой главе мы не реализовали его с помощью прокси. передачаdelay(f, ms)Возвращает функцию, которая будетmsпереадресовывать все звонки наf.

Вот предыдущая реализация на основе функций:

function delay(f, ms) {
  // 返回一个超时后调用 f 函数的包装器
  return function() { // (*)
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

// 这次包装后,sayHi 在3秒后被调用
sayHi = delay(sayHi, 3000);

sayHi("John"); // Hello, John! (3秒后)

Как мы уже видели, в большинстве случаев это работает. функция-оболочка(*)Выполнить вызов после тайм-аута.

Но функция-оболочка не пересылает операции чтения/записи свойств или что-то еще. После переноса свойства исходной функции недоступны, напримерname,lengthи другие:

function delay(f, ms) {
  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

alert(sayHi.length); // 1 (函数的 length 是其声明中的参数个数)

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 0 (在包装器声明中,参数个数为0)

ProxyГораздо мощнее, потому что он перенаправляет все на целевой объект.

давайте использоватьProxyвместо функции-оболочки:

function delay(f, ms) {
  return new Proxy(f, {
    apply(target, thisArg, args) {
      setTimeout(() => target.apply(thisArg, args), ms);
    }
  });
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 1 (*) proxy 转发“获取 length” 操作到目标对象

sayHi("John"); // Hello, John! (3秒后)

Результат тот же, но теперь не только вызов, но и все операции на прокси переадресовываются на исходную функцию. Так сказать Hi.length в(*)Результат (*) возвращается правильно после переноса строк.

У нас есть более «богатая» обертка.

Существуют и другие хуки: полный список находится в начале этой главы. Схема их использования аналогична приведенной выше.

Reflect

Reflectвстроенный объект, упрощающий созданиеProxy.

Предыдущие внутренние методы, такие как[[Get]],[[Set]]и т. д. являются просто спецификациями и не могут быть вызваны напрямую.

ReflectОбъекты позволяют вызывать эти внутренние методы. Его методы являются минимальными оболочками для внутренних методов.

ЭтоReflectПример того же и вызова:

действовать Reflectпередача внутренний метод
obj[prop] Reflect.get(obj, prop) [[Get]]
obj[prop] = value Reflect.set(obj, prop, value) [[Set]]
delete obj[prop] Reflect.deleteProperty(obj, prop) [[Delete]]
new F(value) Reflect.construct(F, value) [[Construct]]

Например:

let user = {};

Reflect.set(user, 'name', 'John');

alert(user.name); // John

особенно,Reflectпозволяет нам использовать функцию (Reflect.construct,Reflect.deleteProperty, ...) выполнить операцию (new,delete, ...). Это интересная особенность, но здесь есть еще один важный момент.

для каждого может бытьProxyзахваченные внутренние методы,ReflectУ каждого есть соответствующий метод Reflect с тем же именем и параметрами, что иProxyКрючки одинаковые.

Поэтому мы можем использоватьReflectчтобы перенаправить операцию исходному объекту.

В этом примере крючокgetа такжеsetПрозрачно (как будто ни одного из них не существует) пересылать операции чтения/записи объекту с сообщением:

let user = {
  name: "John",
};

user = new Proxy(user, {
  get(target, prop, receiver) {
    alert(`GET ${prop}`);
    return Reflect.get(target, prop, receiver); // (1)
  },
  set(target, prop, val, receiver) {
    alert(`SET ${prop}=${val}`);
    return Reflect.set(target, prop, val, receiver); // (2)
  }
});

let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"

здесь:

  • Reflect.getпрочитать свойство объекта
  • Reflect.setЗаписать свойства объекта, успешно вернутьсяtrue, иначе возвратfalse

При этом все просто: если хук хочет перенаправить вызов на объект, он просто вызывает с теми же параметрамиReflect.<method>Будет достаточно.

В большинстве случаев мы не можем использоватьReflectсделать то же самое, например, используяReflect.get(target, prop, receiver)Свойство read можно заменить наtarget[prop]. Хотя есть и тонкие отличия.

прокси геттер

Давайте посмотрим на примере, почемуReflect.getлучше. мы также увидим, почемуget/setимеет четвертый параметрreceiver, который мы раньше не использовали.

у нас есть с_nameсвойство и объект с геттеромuser.

Вот прокси:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop];
  }
});

alert(userProxy.name); // Guest

ДолженgetХук здесь «прозрачен», он возвращает исходное свойство и больше ничего не делает. Для нашего примера этого достаточно.

Кажется, все в порядке. Но усложним пример.

другой объектadminотuserПосле наследования мы можем наблюдать неправильное поведение:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop]; // (*) target = user
  }
});

let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

// Expected: Admin
alert(admin.name); // 输出:Guest (?!?)

читатьadmin.nameдолжен вернуться"Admin", вместо"Guest"!

Что случилось? Может мы что-то не так делаем с наследованием?

Но если убрать прокси то все работает как положено.

Проблема собственно в прокси, в(*)Ряд.

  1. когда мы читаемadmin.name,из-заadminСам объект не имеет соответствующего свойства, поиск пойдет по его прототипу.

  2. ПрототипuserProxy.

  3. читать с проксиnameимущество,getХук срабатывает и возвращается из исходного объектаtarget[prop]свойства, в(*)Ряд

    при звонкеtarget[prop]когда, еслиpropгеттер, который будетthis=targetзапустить его код в контексте. Таким образом, результат исходного объектаtargetизthis._nameто есть изuser.

Для разрешения этой ситуации нам нужноgetТретий параметр хукаreceiver. Он гарантирует правильностьthisк геттеру. В нашем случае этоadmin.

Как передать контекст как геттер? Для рутинных функций мы можем использоватьcall/apply, но это геттер, он не "вызывается", просто к нему обращаются.

Reflect.getЭто может быть сделано. Если мы его используем, все работает нормально.

Вот исправленный вариант:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) { // receiver = admin
    return Reflect.get(target, prop, receiver); // (*)
  }
});


let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

alert(admin.name); // Admin

Сейчасreceiver, сохраняя правильнуюthisцитаты (т.admin) ссылка, которая будет в(*)использовать в очередиReflect.getпередается геттеру.

Мы можем переписать хук, чтобы он был короче:

get(target, prop, receiver) {
  return Reflect.get(...arguments);
}

ReflectВызовы именуются точно так же, как хуки, и принимают те же параметры. Они специально разработаны таким образом.

следовательно,return Reflect...Для переадресации действий предусмотрена безопасная процедура напоминания, чтобы мы не забыли ничего, связанного с этим.

Ограничения прокси

Прокси предоставляют уникальный способ изменить или настроить поведение существующих объектов на самом низком уровне. Однако это не идеально. Есть ограничения.

Встроенные объекты: внутренние слоты

Многие встроенные объекты, такие какMap, Set, Date, Promiseи т. д. все используют так называемые «внутренние слоты».

Они аналогичны свойствам, но только для внутреннего использования, только для целей спецификации. Например,MapХраните предмет в[[MapData]]середина. встроенные методы для доступа к ним напрямую, без прохождения[[Get]]/[[Set]]внутренний метод. такProxyне может быть перехвачен.

Почему вас это должно волновать? Они внутренние!

Вот в чем проблема. После проксирования встроенного объекта, такого как этот, прокси-объект не имеет этих внутренних слотов, поэтому встроенный метод не работает.

Например:

let map = new Map();

let proxy = new Proxy(map, {});

proxy.set('test', 1); // Error

Внутренне, аMapхранить все данные в[[MapData]]во внутреннем слоте. Прокси-объекты не имеют такого слота.встроенный методMap.prototype.setметод пытается получить доступ к внутреннему свойствуthis.[[MapData]], но из-заthis=proxyсуществуетproxyне могу найти его в , он может только потерпеть неудачу.

К счастью, есть обходной путь:

let map = new Map();

let proxy = new Proxy(map, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value;
  }
});

proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)

Теперь это работает нормально, потому чтоgetХуки будут работать со свойствами (например,map.set) привязан к целевому объекту (map)сам.

В отличие от предыдущего примера,proxy.set(...)внутреннийthisЗначение неproxy, но исходный объектmap. Следовательно, когдаsetВнутренняя реализация хука пытается получить доступthis.[[MapData]]Когда внутри слота, это удается.

Arrayнет внутренних слотов

Одно заметное исключение: встроенныйArrayВнутренние слоты не используются. Это по историческим причинам, потому что он появился очень давно.

Поэтому при прокси-массиве такой проблемы нет.

частное поле

То же самое происходит и с приватными полями класса.

Например,getName()доступ к методу частный#nameсвойство и перерыв после прокси:

class User {
  #name = "Guest";

  getName() {
    return this.#name;
  }
}

let user = new User();

user = new Proxy(user, {});

alert(user.getName()); // Error

Причина в том, что приватные поля реализованы с использованием внутренних слотов. JavaScript обращается к ним без использования[[Get]]/[[Set]].

вызовgetName()ВремяthisЗначение проксируетсяuser, у него нет слота с закрытыми полями.

Опять же, решение для метода привязки возвращает его в нормальное состояние:

class User {
  #name = "Guest";

  getName() {
    return this.#name;
  }
}

let user = new User();

user = new Proxy(user, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value;
  }
});

alert(user.getName()); // Guest

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

Proxy != target

Прокси и исходный объект - разные объекты. Это естественно, верно?

Итак, если мы используем исходный объект в качестве ключа, а затем проксируем его, прокси не будет найден:

let allUsers = new Set();

class User {
  constructor(name) {
    this.name = name;
    allUsers.add(this);
  }
}

let user = new User("John");

alert(allUsers.has(user)); // true

user = new Proxy(user, {});

alert(allUsers.has(user)); // false

Как мы видим, после проксирования мы находимся вallUsersне найдено вuser, потому что прокси — это другой объект.

Прокси не может перехватывать строгие тесты на равенство===

Прокси может перехватывать многие операторы, такие как new (используяconstruct), в (используяhas), удалить (используяdeleteProperty)Ждать.

Но нет возможности перехватить строгие проверки на равенство для объектов. Объект строго равен самому себе и не имеет никакой другой ценности.

Поэтому все операции и встроенные классы, которые сравнивают объекты на равенство, различают цель и прокси. Здесь нет прозрачной замены.

Отменяемый прокси

Один

Отменяемый

Прокси — это прокси, который можно отключить.

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

Что мы можем сделать, так это обернуть его как отзываемый прокси без каких-либо хуков. Такой прокси будет перенаправлять действие на объект, который мы можем отключить в любой момент.

Синтаксис:

let {proxy, revoke} = Proxy.revocable(target, handler)

Вызов возвращаетproxyа такжеrevokeобъект функции, чтобы отключить его.

Вот пример:

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

// proxy 正常工作
alert(proxy.data); // Valuable data

// 之后某处调用
revoke();

// proxy 不再工作(已吊销)
alert(proxy.data); // Error

передачаrevoke()Все внутренние ссылки на целевые объекты удаляются из прокси, поэтому они больше не связаны. Затем целевой объект может быть удален сборщиком мусора.

Мы также можемrevokeсохранить вWeakMap, чтобы иметь возможность легко найти его через прокси-объект:

let revokes = new WeakMap();

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

revokes.set(proxy, revoke);

// ..later in our code..
revoke = revokes.get(proxy);
revoke();

alert(proxy.data); // Error(已吊销)

Преимущество этого подхода заключается в том, что нам не нужно нести с нами отзыва. Мы можем получить его с карты при необходимостиproxyчтобы получить это.

Здесь мы используемWeakMapвместоMap, так как это не мешает сборке мусора. Если прокси-объект становится «недоступным» (например, никакие переменные больше не ссылаются на него), тоWeakMapпозволяют сочетать его сrevokeОбъект стирается из памяти целиком, потому что он нам больше не нужен.

использованная литература

  • Технические характеристики:Proxy.
  • MDN: Proxy.

Суммировать

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

Он может обертывать любой тип объекта, включая классы и функции.

Синтаксис:

let proxy = new Proxy(target, {
  /* traps */
});

... тогда мы должны использовать вездеproxyвместоtarget. Прокси не имеют собственных свойств или методов. Если указан хук, он зафиксирует действие, в противном случае оно будет перенаправлено наtargetобъект.

Мы можем захватить:

  • читать(get), записывать (set),удалять(deleteProperty) свойства (даже несуществующие).
  • вызов функции (applyкрюк).
  • newработать(constructкрюк).
  • Многие другие операции (полный список в начале статьи иdocsсередина).

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

Мы также можем обернуть объект несколько раз в разные прокси и украсить его несколькими функциями.

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

Прокси имеет некоторые ограничения:

  • Встроенные объекты имеют «внутренние слоты», и доступ к этим объектам не может быть проксирован. См. обходной путь выше.
  • То же самое касается полей закрытого класса, поскольку они реализуются внутри с помощью слотов. Таким образом, вызов прокси-метода должен иметь целевой объектthisчтобы получить к ним доступ.
  • Тест на равенство объектов===не может быть заблокирован.
  • Производительность: тесты зависят от движка, но обычно доступ к свойствам с простейшими прокси занимает в несколько раз больше времени. На практике это имеет значение только для некоторых «узких мест» объектов.

Несколько небольших примеров задач

Ошибка чтения несуществующего свойства

Как правило, попытка прочитать свойство, которое не существует, возвращаетundefined.

Создайте прокси, который выдает ошибку при попытке прочитать несуществующее свойство.

Это может помочь выявить ошибки программирования на ранней стадии.

написать согласиеtargetобъект и возвращает прокси, который добавляет функциональность этого аспектаwrap(target)функция.

Должны быть удовлетворены следующие результаты:

let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
      /* 你的代码 */
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // 错误:属性不存在
решение
let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      if (prop in target) {
        return Reflect.get(target, prop, receiver);
      } else {
        throw new ReferenceError(`Property doesn't exist: "${prop}"`)
      }
    }
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist

массив доступа с индексом -1

В некоторых языках программирования мы можем получить доступ к элементам массива, используя отрицательные индексы с конца.

так:

let array = [1, 2, 3];

array[-1]; // 3,最后一个元素
array[-2]; // 2,从末尾开始向前移动一步
array[-3]; // 1,从末尾开始向前移动两步

другими словами,array[-N]а такжеarray[array.length - N]такой же.

Создайте прокси для реализации этого поведения.

Это должно выглядеть так:

let array = [1, 2, 3];

array = new Proxy(array, {
  /* your code */
});

alert( array[-1] ); // 3
alert( array[-2] ); // 2

// 其他数组也应该适用于这个功能
решение
let array = [1, 2, 3];

array = new Proxy(array, {
  get(target, prop, receiver) {
    if (prop < 0) {
      // even if we access it like arr[1]
      // prop is a string, so need to convert it to number
      prop = +prop + target.length;
    }
    return Reflect.get(target, prop, receiver);
  }
});


alert(array[-1]); // 3
alert(array[-2]); // 2

Observable

Создайте объект, который «делает объект наблюдаемым», возвращая проксиmakeObservable(target)функция.

Это работает следующим образом:

function makeObservable(target) {
  /* your code */
}

let user = {};
user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John"; // alerts:设置 name 属性为 John

другими словами,makeObservableВозвращаемый объект аналогичен исходному объекту, но также имеетhandlerФункция настроена на метод, который вызывается при изменении любого свойства.observe(handler).

Всякий раз, когда свойство изменяется, оно вызывается с именем и значением свойства.handler(key, value).

P.S. В этом задании обратите внимание только на атрибут записи. Аналогичным образом могут быть реализованы и другие операции.

решение

Решение состоит из двух частей:

  1. несмотря ни на что.observe(handler)Когда он вызывается, нам всем нужно где-то запомнить обработчик, чтобы его можно было вызвать позже. Мы можем хранить обработчик непосредственно в объекте, используя Symbol в качестве ключа свойства.
  2. нам нужен ременьsetПрокси хука для вызова обработчика при возникновении каких-либо изменений.
let handlers = Symbol('handlers');

function makeObservable(target) {
  // 1. 初始化 handler 存储数组
  target[handlers] = [];

  // 存储 handler 函数到数组中以便于未来调用
  target.observe = function(handler) {
    this[handlers].push(handler);
  };

  // 2. 创建代理以处理更改
  return new Proxy(target, {
    set(target, property, value, receiver) {
      let success = Reflect.set(...arguments); // 转发写入操作到目标对象
      if (success) { // 如果设置属性的时候没有报错
        // 调用所有 handler
        target[handlers].forEach(handler => handler(property, value));
      }
      return success;
    }
  });
}

let user = {};

user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John";