Loading...

Proxy жана Reflect

Proxy жана Reflect

 

Объект Proxy башка объекттин тегерегине "оройт" жана аны менен окуу/жазуу касиеттери жана башкалар сыяктуу ар кандай аракеттерди кармай алат (жана, кааласа, өз алдынча иштете алат). Кийинкиде биз "прокси" деп аталган объекттерге кайрылабыз.

Проксилер көптөгөн библиотекаларда  жана кээ бир браузер алкактарында колдонулат. Бул бөлүмдө биз чыныгы дүйнөлүк көйгөйлөрдү чечүүдө проксилерди көп колдонуу учурларын көрөбүз.

Синтаксис:

@A@let proxy = new Proxy(target, handler);@A@
  • target- бул прокси жасашыңыз керек болгон объект, ал бардык нерсе, анын ичинде функциялар болушу мүмкүн.
  • handler– прокси конфигурациясы: “тузактары” бар объект: ар кандай операцияларды кармап турган ыкмалар, мисалы, getкасиетти окуу үчүн капкан, касиетти жазуу үчүн targetкапкан ж.б.у.с.settarget

Операциялар учурунда proxy, эгерде handlerтиешелүү “капкан” бар болсо, анда ал иштейт жана прокси аны өз алдынча иштетүү мүмкүнчүлүгүнө ээ, антпесе операция баштапкы объектте аткарылат target.

Алгачкы мисал катары, эч кандай тузаксыз прокси түзөлү:

@A@let target = {};
let proxy = new Proxy(target, {}); // пустой handler

proxy.test = 5; // записываем в прокси (1)
alert(target.test); // 5, свойство появилось в target!

alert(proxy.test); // 5, мы также можем прочитать его из прокси (2)

for(let key in proxy) alert(key); // test, итерация работает (3)@A@

Илгичтер жок болгондуктан, бардык операциялар proxyбаштапкы объектке колдонулбайт target.

  1. Менчик жазуусу proxy.test=маанини коет target.
  2. Мүлктү окуу proxy.testтөмөнкүдөн маанини кайтарат target.
  3. Итерациялоо proxyтөмөнкүдөн маанилерди кайтарат target.

Көрүнүп тургандай, капкансыз proxy- бул тунук орогуч target

Proxyөзгөчө, "экзотикалык" объект болуп саналат, анын өзүнүн касиеттери жок. Бош болсо, handlerал жөн гана бардык операцияларды target.

Анын башка функцияларын иштетүү үчүн, келгиле, тузактарды кошолу.

Биз алар менен так эмнени кармай алабыз?

Көпчүлүк объекти аракеттери үчүн JavaScript спецификациясында аны кантип аткаруу керектиги эң төмөнкү деңгээлде сүрөттөлгөн "ички ыкма" бар. Мисалы, [[Get]]- касиетти окуунун ички ыкмасы, [[Set]]- касиетти жазуу үчүн ж.б.у.с. Бул ыкмалар спецификацияда гана колдонулат, биз аларга түздөн-түз аты менен кайрыла албайбыз.

Тузактар ​​жөн гана бул ички ыкмаларга чалууларды кармап турушат. Кармалууга мүмкүн болгон ыкмалардын толук тизмеси Прокси спецификациясында жана ошондой эле төмөнкү таблицада келтирилген .

handlerБул таблицадагы ар бир ички ыкма үчүн илгич көрсөтүлөт, башкача айтканда, бул операцияны токтотуу үчүн түзүп жатканда параметрге кошо турган ыкманын аталышы new Proxy:

Ички ыкма Тузак Эмне себеп болот
[[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
Инварианттар

JavaScript ички ыкмаларды жана илгичтерди ишке ашырууга кээ бир шарттарды - инварианттарды жүктөйт.

Алардын көбү кайтарым баалуулуктарга тиешелүү:

  • Мааниси ийгиликтүү жазылган болсо, метод [[Set]]кайтып келиши керек , болбосо .truefalse
  • Мааниси ийгиликтүү алынып салынса, метод [[Delete]]кайтып келиши керек , болбосо .truefalse
  • …ж.б., биз төмөндөгү мисалдардан көбүрөөк көрөбүз.

Башка инварианттар бар, мисалы:

  • [[GetPrototypeOf]]Проксиге колдонулган ыкма [[GetPrototypeOf]]баштапкы объектке колдонулган ыкма менен бирдей маанини кайтарышы керек. Башка сөз менен айтканда, прототиптин прототибин окуу ар дайым баштапкы объектинин прототибин кайтарып бериши керек.

Илгичтер бул ыкмаларга чалууларды кармап алат, бирок көрсөтүлгөн шарттарга жооп бериши керек.

Инварианттар тилдик түзүлүштөрдүн жана методдордун туура жана ырааттуу жүрүм-турумун кепилдейт. Инварианттардын толук тизмесин спецификациядан тапса болот , бирок сиз чындап кызыктай бир нерсе кылмайынча, ал шарттарды бузбайсыз.

Эми мунун баары реалдуу мисалдар менен кантип иштээрин карап көрөлү.

"Алуу" илгич менен демейки маани

Эң көп колдонулган тузактар ​​окуу/жазуу касиеттери.

Окуу операциясын токтотуу үчүн, handlerболушу керек get(target, property, receiver).

Ал аргументтер менен объекттин касиетин окууга аракет кылып жатканда күйөт:

  • targetконструкторго биринчи аргумент катары берилген баштапкы объект new Proxy,
  • property- мүлктүн аталышы,
  • receiver- эгерде объекттин касиети алуучу болсо, анда - бул аны чакырганда receiverколдонула турган объект . thisБул көбүнчө прокси объектинин өзү (же андан мураска алган объект). Бизге азыр бул аргументтин кереги жок, бул тууралуу кийинчерээк кененирээк токтолобуз.

Объекттин касиеттери үчүн "демейки маанилерди" ишке ашыруу үчүн илгичти колдонолу get.

Мисалы, сандык массив түзөлү, андан жок элемент окулганда 0, .

Адатта, массивден окууда жок касиет кайтарылат undefined, бирок биз кадимки массивди массивден касиетти окуу операциясын токтоткон проксиге ороп коёбуз жана 0мындай элемент жок болсо кайтарат:

@A@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 (нет такого элемента)@A@

Көрүнүп тургандай, муну илгич менен жасоо абдан оңой get.

ProxyБиз демейки маанилерди кайтаруу үчүн каалаган логиканы ишке ашыруу үчүн колдоно алабыз .

Бизде англис тилиндеги фразалар жана алардын испан тилине котормосу бар сөздүк объектиси бар экенин элестетип көрөлү:

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

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

Эми, эгер сөз dictionaryайкашы жок болсо, ал окуп жатканда кайтып келет undefined. Бирок иш жүзүндө фразаларды котормостон калтыруу колдонууга караганда жакшыраак undefined. Ошондуктан, котормо жок болгон учурда англис тилиндеги оригиналдуу сөз айкашынын ордуна кайтарылгандай кылалы undefined.

Буга жетишүү үчүн, келгиле, аны dictionaryокуу операциясын токтоткон проксиге ороп көрөлү:

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

dictionary = new Proxy(dictionary, {
  get(target, phrase) { // перехватываем чтение свойства в dictionary
    if (phrase in target) { // если перевод для фразы есть в словаре
      return target[phrase]; // возвращаем его
    } else {
      // иначе возвращаем непереведённую фразу
      return phrase;
    }
  }
});

// Запросим перевод произвольного выражения в словаре!
// В худшем случае оно не будет переведено
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (нет перевода)@A@
Анын ордуна прокси бардык жерде колдонулушу керекtarget

Эскертүү: прокси өзгөрмөнүн үстүнөн жазат:

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

Прокси бардык жерде баштапкы объектти алмаштырышы керек. Проксиден кийин эч ким баштапкы объектке кайрылбашы керек. Болбосо, баш аламандыкка кабылуу абдан оңой.

"Орнотуу" капкан менен текшерүү

Биз сандар үчүн гана массив түзгүбүз келет дейли. Эгер ага башка түрдөгү маани кошулса, анда бул катага алып келиши керек.

Кыймыл setмүлк жазылганда ишке кирет.

set(target, property, value, receiver):

  • targetконструкторго биринчи аргумент катары берилген баштапкы объект new Proxy,
  • property- мүлктүн аталышы,
  • value- мүлктүн наркы,
  • receiver– илгичке окшош get, бул аргумент, эгерде касиет орнотуучу болсо гана мааниге ээ.

Жазуу ийгиликтүү болсо, илгич setкайтып келиши керек , антпесе (ката пайда болот ).truefalseTypeError

Келгиле, аны жаңы баалуулуктарды сыноо үчүн колдонолу:

@A@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("Длина: " + numbers.length); // 2

numbers.push("тест"); // TypeError (ловушка set на прокси вернула false)

alert("Интерпретатор никогда не доходит до этой строки (из-за ошибки в строке выше)");@A@

Камтылган массив функциясы дагы эле иштей тургандыгын белгилеңиз! Маанилер тарабынан кошулат push. Бул мүлктү lengthжогорулатат . Биздин прокси эч нерсени бузбайт.

pushАл жерде типти текшерүүнү кошуу үчүн массив ыкмаларын жана башкаларды жокко чыгаруунун кереги жок , анткени алар прокси тарабынан кармалып турган unshiftоперацияны ичтен колдонушат .[[Set]]

Ошентип, код таза жана ачык бойдон калууда.

Кайтууну унутпаңызtrue

Жогоруда айтылгандай, инварианттар сакталышы керек.

мулк ийгиликтүү жазылган болсо, setкайтуу үчүн кайырмак ишке ашыруу үчүн .true

Муну унутуп коюу же кандайдыр бир жалган маанини кайтаруу катага алып келет TypeError.

"ownKeys" жана "getOwnPropertyDescriptor" менен кайталоо

Object.keys, цикл for..inжана объекттин касиеттер тизмесинде иштеген башка ыкмалардын көбү аларды алуу үчүн ички ыкманы [[OwnPropertyKeys]]( илмек ) колдонушат.ownKeys

Бул ыкмалар майда-чүйдөсүнө чейин айырмаланат:

  • Object.getOwnPropertyNames(obj)символдук эмес ачкычтарды кайтарат.
  • Object.getOwnPropertySymbols(obj)тамга баскычтарын кайтарат.
  • Object.keys/values()желек менен символдон башка ачкычтарды/баалуулуктарды кайтарат (мүлктүн желектери жөнүндө Flags жана касиеттин дескрипторлоруenumerable бөлүмүндө кененирээк окуңуз ).
  • for..inenumerableжелек менен символдук эмес баскычтарды , ошондой эле прототип баскычтарын кайталайт .

...Бирок алардын баары ушул тизмеден башталат.

Төмөндөгү мисалда биз объектти ownKeysайлантуу үчүн илгичти колдонобуз for..in, ошондой эле астын сызык менен башталган касиеттерди Object.keysөткөрүп жиберебиз :Object.values_

@A@let user = {
  name: "Вася",
  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) ); // Вася,30@A@

Көрүнүп тургандай, ал иштейт.

Бирок, биз объектте жок ачкычты кайтарууга аракет кылсак, анда Object.keysал кайтарылбайт:

@A@let user = { };

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

alert( Object.keys(user) ); // <пусто>@A@

Неге? Себеби жөнөкөй: Object.keysал касиеттери менен гана кайтарат enumerable. Бул желектин бар же жок экенин аныктоо үчүн, ал ар бир касиет үчүн ички ыкманы чакырат , ал өзүнүн дескрипторун[[GetOwnProperty]] алат . Бирок бул учурда, касиет жок, анын дескриптору бош, желек жок, ошондуктан өткөрүп жиберилет.enumerable

Мүлктү кайтаруу үчүн Object.keysже мүлк объектиде физикалык түрдө жана желек менен болушу керек enumerable, же чалууларга тоскоол болушу керек [[GetOwnProperty]](бул тузак тарабынан жасалат getOwnPropertyDescriptor), ал жерде дескрипторду менен кайтарылышы керек enumerable: true.

Бул кандайча иштейт:

@A@let user = { };

user = new Proxy(user, {
  ownKeys(target) { // вызывается 1 раз для получения списка свойств
    return ['a', 'b', 'c'];
  },

  getOwnPropertyDescriptor(target, prop) { // вызывается для каждого свойства
    return {
      enumerable: true,
      configurable: true
      /* ...другие флаги, возможно, "value: ..." */
    };
  }

});

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

Дагы бир жолу, тутканы алуу объектинин өзүндө мүлк жок болсо гана кармалышы керек экенин белгилей кетүү керек.

"deleteProperty" капкан менен корголгон касиеттери жана башкалар

_Аталышы астынкы сызык менен башталган касиеттер жана ыкмалар ички катары каралышы керек деген жалпы кабыл алынган конвенция бар . Аларга объекттин сыртынан кирүүгө болбойт.

Бирок, техникалык жактан дагы эле мүмкүн:

@A@let user = {
  name: "Вася",
  _password: "secret"
};

alert(user._password); // secret@A@

_менен башталган касиеттерди сырттан кирүүдөн коргоо үчүн прокси колдонолу .

Бизге төмөнкү тузактар ​​керек болот:

  • get– мындай касиетти окууда ката кетирүү үчүн,
  • set– жазуу учурунда ката кетирүү үчүн,
  • deleteProperty– жок кылууда ката пайда болуу үчүн,
  • ownKeysfor..in– типтеги мындай касиеттерди жана ыкмаларын алып салуу үчүн Object.keys.

Бул жерде тиешелүү код:

@A@let user = {
  name: "Вася",
  _password: "***"
};

user = new Proxy(user, {
  get(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error("Отказано в доступе");
    } else {
      let value = target[prop];
      return (typeof value === 'function') ? value.bind(target) : value; // (*)
    }
  },
  set(target, prop, val) { // перехватываем запись свойства
    if (prop.startsWith('_')) {
      throw new Error("Отказано в доступе");
    } else {
      target[prop] = val;
      return true;
    }
  },
  deleteProperty(target, prop) { // перехватываем удаление свойства
    if (prop.startsWith('_')) {
      throw new Error("Отказано в доступе");
    } else {
      delete target[prop];
      return true;
    }
  },
  ownKeys(target) { // перехватываем попытку итерации
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// "get" не позволяет прочитать _password
try {
  alert(user._password); // Error: Отказано в доступе
} catch(e) { alert(e.message); }

// "set" не позволяет записать _password
try {
  user._password = "test"; // Error: Отказано в доступе
} catch(e) { alert(e.message); }

// "deleteProperty" не позволяет удалить _password
try {
  delete user._password; // Error: Отказано в доступе
} 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;
  }
}@A@

Чакыруу user.checkPassword()проксидик объектти объект userкатары кабыл алат this(чекитке айланганга чейинки объект this), андыктан мындай чалуу ге жасалганда this._password, илгич getкүчүнө кирет (ал касиеттин каалаган окуусунда күйөт) жана ката ыргытылат.

Ошондуктан, биз контекстти объекттин методдору менен байланыштырабыз target(*). Андан кийин алардын кийинки чалуулары эч кандай тузаксыз targetкатары колдонулат .this

Бул чечим, адатта, иштейт, бирок идеалдуу эмес, анткени ыкма баштапкы объектти башка жерден өткөрүп жибериши мүмкүн жана башаламандык болушу мүмкүн: баштапкы объект кайда, ал эми прокси кайда.

Мындан тышкары, объект бир нече жолу прокси болушу мүмкүн (ар кандай функцияларды кошуу үчүн), эгер сиз оригиналды ыкмага өткөрүп берсеңиз, анда күтүлбөгөн жагдайлар болушу мүмкүн.

Андыктан мындай проксиди бардык жерде колдонбошуңуз керек.

Класстардагы жеке менчик

Заманбап JavaScript котормочулары класстардагы жеке касиеттерди колдойт. Мындай касиеттердин аттары символдон башталышы керек #Алар жеке жана корголгон методдор жана касиеттер бөлүмүндө кеңири баяндалган . Аларга мындай проксилердин кереги жок.

Бирок, жеке менчиктин кемчиликтери бар. Тактап айтканда, алар тукум кууган эмес.

"Бар" капкан менен "аралыгы бар"

Келгиле, дагы мисалдарды карап көрөлү.

rangeБизде диапазонду сүрөттөгөн объект бар дейли :

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

inКайсы бир сандын белгиленген диапазондо экенин текшерүү үчүн операторду колдонгубуз келет .

Тузак hasчалууларды кармап турат in.

has(target, property)

  • targetконструкторго биринчи аргумент катары берилген баштапкы объект new Proxy,
  • property- мүлктүн аталышы

Бул жерде демо болуп саналат:

@A@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@A@

Мыкты көрүнөт, туурабы? Жана ишке ашыруу үчүн абдан жеңил.

Ороо функциялары: "колдонуу"

Биз проксилерди жана функцияларды ороп алабыз.

apply(target, thisArg, args)Прокси функция катары чакырылганда илгич иштетилет:

  • targetбаштапкы объект (биз эсибизде болгондой, функция JavaScript тилиндеги объект),
  • thisArgконтекст болуп саналат this.
  • args- аргументтердин тизмеси.

delay(f, ms)Мисалы, биз Декораторлор жана Чалууларды багыттоо бөлүмүндө жараткан декораторду эстейли , чалуу/колдонуу .

Анан прокси түзбөй эле жасадык. Чакыруу чалууларды миллисекунддан кийин delay(f, ms)өткөргөн функцияны кайтарды .fms

Бул жерде функциянын негизинде мурунку ишке ашыруу болуп саналат:

@A@function delay(f, ms) {
  // возвращает обёртку, которая вызывает функцию f через таймаут
  return function() { // (*)
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(user) {
  alert(`Привет, ${user}!`);
}

// после обёртки вызовы sayHi будут срабатывать с задержкой в 3 секунды
sayHi = delay(sayHi, 3000);

sayHi("Вася"); // Привет, Вася! (через 3 секунды)@A@

Биз көрүп тургандай, бул жалпысынан иштейт. Ороочу функциясы inline (*)белгиленген кечигүү менен керектүү функцияны чакырат.

Бирок биздин орогуч функциябыз касиетти окуу/жазуу операцияларын жана башкаларды кайра багыттабайт. nameОролгондон кийин, , , жана башкалар сыяктуу баштапкы функциянын касиеттерине жетүү мүмкүнчүлүгү lengthжоголот.

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

function sayHi(user) {
  alert(`Привет, ${user}!`);
}

alert(sayHi.length); // 1 (в функции length - это число аргументов в её объявлении)

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 0 (в объявлении функции-обёртки ноль аргументов)@A@

Проксилер бул мааниде алда канча күчтүү, анткени алар бардыгын баштапкы объектке багытташат.

Келгиле, орогуч функциясынын ордуна прокси колдонолу:

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

function sayHi(user) {
  alert(`Привет, ${user}!`);
}

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 1 (*) прокси перенаправляет чтение свойства length на исходную функцию

sayHi("Вася"); // Привет, Вася! (через 3 секунды)@A@

Натыйжа бирдей, бирок азыр чалуулар гана эмес, проксидеги башка операциялар дагы баштапкы функцияга багытталат. Ошентип, касиетти окуу операциясы проксиден кийин sayHi.lengthкатардагы туура маанини кайтарат (*).

Биз эң жакшы ороп алдык.

Башка каталар бар, бул бөлүмдүн башында толук тизмеси. Сиз аларды жогоруда айтылгандай эле колдоно аласыз.

чагылдыруу

Reflectпроксилерди түзүүнү жөнөкөйлөтүүчү орнотулган объект болуп саналат.

[[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]]

Мисалы:

@A@let user = {};

Reflect.set(user, 'name', 'Вася');

alert(user.name); // Вася@A@

Атап айтканда, Reflectал операторлорду ( newdelete…) функциялар ( Reflect.constructReflect.deleteProperty, …) катары чакырууга мүмкүндүк берет. Бул кызыктуу мүмкүнчүлүк, бирок бул жерде бизди башка нерсе кызыктырууда.

Байланышкан ар бир ички ыкма үчүн илгич менен бирдей аталышка жана ошол эле аргументтерге ээ болгон Proxyтиешелүү ыкма бар .ReflectProxy

ReflectОшондуктан, биз операцияны баштапкы объектке багыттоо үчүн колдоно алабыз .

Бул мисалда илгичтер да, тунук getsetалар жок сыяктуу) да билдирүүнү көрсөтүү менен объектке окууну жана жазууну кайра багыттайт:

@A@let user = {
  name: "Вася",
};

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; // выводит "GET name"
user.name = "Петя"; // выводит "SET name=Петя"@A@

Бул жерде:

  1. Reflect.getобъекттин касиетин окуйт.
  2. Reflect.setмүлктү жазат жана trueийгиликке кайтып келет, болбосо false.

Башкача айтканда, бардыгы абдан жөнөкөй - эгер капкан чалууну объектке багыттоону кааласа, анда аны Reflect.<метод>ошол эле аргументтер менен чакыруу жетиштүү.

Көпчүлүк учурларда, биз муну жок кылсак болот Reflect, мисалы, касиетти окуу Reflect.get(target, prop, receiver)менен алмаштырса болот target[prop]. Бирок кээ бир нюанстарды өткөрүп жиберүү оңой.

Прокси алуу

Келгиле, жакшыраак экенин көрсөткөн конкреттүү мисалды карап көрөлү жана ошол эле учурда үчүнчү аргумент Reflect.getэмне үчүн керек экенин аныктайбыз , биз аны мурда колдонгон эмеспиз.get/setreceiver

userБизде мүлкү бар объект _nameжана ал үчүн алуучу бар дейли .

Айланада прокси жасайлы user:

@A@let user = {
  _name: "Гость",
  get name() {
    return this._name;
  }
};

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

alert(userProxy.name); // Гость@A@

Бул жерде илгич get"тунук", ал баштапкы объекттин касиетин кайтарып берет жана башка эч нерсе кылбайт. Биздин мисал үчүн бул жетиштүү.

Баары өз ордунда болуп жаткандай сезилет. Бирок мисалды бир аз татаалдаштырып көрөлү.

Эгерде биз прокси объекттен мурас алсак useradminанда анын туура эмес иш алып бараарын көрөбүз:

@A@let user = {
  _name: "Гость",
  get name() {
    return this._name;
  }
};

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

let admin = {
  __proto__: userProxy,
  _name: "Админ"
};

// Ожидается: Админ
alert(admin.name); // выводится Гость (?!?)@A@

Менчик чалуу admin.nameсапты кайтарышы керек "Админ", бирок ал чыгат "Гость"!

Эмне болду? Мүмкүн биз мурас боюнча туура эмес иш кылып жаткандырбыз?

Бирок проксиди алып салсаңыз, анда баары күтүлгөндөй иштейт.

Негизи проблема проксиде, (*).

  1. Окуп жатканда admin.name, объектте adminэч кандай касиет жок болгондуктан name, ал прототипте каралат.

  2. Прототип прокси болуп саналат userProxy.

  3. Прокси касиетинен окуганда, nameилгич күйүп , аны саптагыдай getбаштапкы объекттен кайтарат .target[prop](*)

    target[prop]If is чакыруу propалуучу өзүнүн кодун контекстте иштетет this=target. Демек, натыйжа this._nameбаштапкы объекттен target, башкача айтканда user.

Мындай жагдайларды оңдоо үчүн receiverкапкандын үчүнчү аргументи керек getthisАл алуучуга өтүү үчүн туура контекстке шилтемени сактайт . Бул учурда ал admin.

Алуучуга контекстти кантип өткөрүү керек? Кадимки функция үчүн биз колдонсок болот call/apply, бирок бул алуучу, ал чакырылбайт, жөн гана маанини оку.

Ал кыла алат Reflect.get. Эгер сиз аны колдонсоңуз, баары туура иштейт.

Бул жерде оңдолгон версия:

@A@let user = {
  _name: "Гость",
  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: "Админ"
};

alert(admin.name); // Админ@A@

Эми receiverжарактууга шилтемени камтыган this(башкача айтканда, ) сапта adminаркылуу алуучуга өткөрүлүп берилет .Reflect.get(*)

Сиз капканды кайра жаза аласыз жана кыскараак:

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

Бул ыкмалар Reflectтиешелүү илгичтер менен бирдей аталышка ээ жана бирдей аргументтерди алышат. Бул JavaScript спецификациясын иштеп чыгуу учурунда иштелип чыккан.

Ошентип return Reflect..., ал бизге операцияны баштапкы объектке багыттоонун жөнөкөй жана коопсуз жолун берет жана ошол эле учурда бул иш менен байланышкан мүмкүн болуучу каталардан куткарат.

Прокси чектөөлөр

Проксилер эң төмөнкү деңгээлдеги объекттердин жүрүм-турумун ыңгайлаштыруу үчүн уникалдуу курал болуп саналат. Бирок алар идеалдуу эмес, кээ бир чектөөлөр бар.

Киргизилген объекттер: Ички уячалар

MapSetDate, жана башкалар сыяктуу көптөгөн орнотулган объекттер Promise"ички уячалар" деп аталгандарды колдонушат.

Бул касиеттерге окшош, бирок спецификациянын өзүндө ички колдонуу үчүн гана. Мисалы, Mapэлементтерди ички уячага сактайт [[MapData]]. Камтылган ыкмалар аркылуу эмес, түздөн-түз слотторго кирүү [[Get]]/[[Set]]. Ошентип, прокси аларды кармай албайт.

Бул эмне үчүн маанилүү? Алар дагы эле ичинде!

Бир нюанс бар. Эгерде орнотулган объект прокси болсо, анда проксиде бул "ички уячалар" болбойт, андыктан мындай проксиде орнотулган ыкманы чакыруу аракети катага алып келет.

Мисал:

@A@let map = new Map();

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

proxy.set('test', 1); // будет ошибка@A@

Ички түрдөгү объект Mapбардык маалыматтарды ички уячага сактайт [[MapData]]. Проксиде мындай уяча жок. Камтылган ыкмаMap.prototype.set өзүнүн ички касиетине кирүүгө аракет кылат this.[[MapData]], бирок this=proxyаны таба албагандыктан, ал ишке ашпай калат.

Бактыга жараша, муну оңдоонун бир жолу бар

@A@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 (работает!)@A@

Эми ал иштеди, анткени getал map.setоригиналдуу функциянын касиеттерин байланыштырат map. Ошентип, ыкманы ишке ашыруу setички уячага кирүүгө аракет кылганда this.[[MapData]], баары жакшы болот.

Объект Arrayички уячаларды колдонбойт

Маанилүү өзгөчөлүк - бул орнотулган объект Array: ал ички уячаларды колдонбойт. Бул тарыхый жактан болгон, анткени массивдер тилге абдан көп убакыт мурун кошулган.

Башкача айтканда, жогоруда сүрөттөлгөн көйгөй массивдерди прокси менен камсыздоодо пайда болбойт.

Жеке талаалар

Окшош нерсе жеке класс талаалары менен болот.

Мисалы, ыкма getName()жеке талаага жетет #name, проксиден кийин ал иштебей калат:

@A@class User {
  #name = "Гость";

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

let user = new User();

user = new Proxy(user, {});

alert(user.getName()); // Ошибка

Себеби бир эле: жеке талаалар ички уячаларды колдонуу менен ишке ашырылат. [[Get]]/[[Set]]JavaScript аларга кирүүдө колдонбойт .

Чакырууда getName()маани thisпрокси болуп саналат user, анын жеке талаалары бар ички уячасы жок.

Чечим, мурунку жагдайдагыдай, контекстти методго байланыштыруу:

 
 
class User {
  #name = "Гость";

  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()); // Гость@A@

Бирок, бул чечимдин бир катар кемчиликтери бар, алар буга чейин айтылган: баштапкы объект методго өткөрүлүп берилет, аны башка жерден өткөрүүгө болот жана бул бүтүндөй прокси функционалдуулугун бузушу мүмкүн.

Прокси != баштапкы объект

Прокси жана проксиден турган объект эки башка объект. Бул табигый нерсе, туурабы?

Эгерде биз түпнуска объектти ачкыч катары колдонсок жана аны прокси катары колдонсок, прокси табылбайт:

@A@let allUsers = new Set();

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

let user = new User("Вася");

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

user = new Proxy(user, {});

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

userКөрүнүп тургандай, проксиден кийин, топтомдун ичинен объектти табуу мүмкүн эмес allUsers, анткени прокси башка объект.

Проксилер теңдикти катуу текшерүүгө тоскоол болбойт===

Проксилер көптөгөн операторлорду кармоого жөндөмдүү, мисалы, new(казак construct), in(казак has), delete(казак deleteProperty) жана башкалар.

Бирок катуу теңчилик үчүн текшерүүнү токтотууга эч кандай жол жок. Объект өзүнө гана бирдей жана башка эч кандай баалуулуктарга ээ эмес.

Ошентип, катуу объекттин теңдигин текшерүүнү колдонгон бардык операциялар жана орнотулган класстар проксиди баштапкы объекттен айырмалайт. Бул учурда ачык-айкын алмаштыруу болбойт.

Өчүрүлгөн проксилер

Кайтарылып алынуучу прокси - бул атайын функцияны чакыруу менен өчүрүлө турган прокси.

Бизде кандайдыр бир ресурс бар дейли жана биз ага каалаган убакта кирүү мүмкүнчүлүгүн жабууну каалайбыз.

Көйгөйдү чечүү үчүн, биз тузактары жок, которула турган прокси колдоно алабыз. Мындай прокси бардык операцияларды прокси объектине өткөрүп берет жана биз аны каалаган убакта өчүрүү мүмкүнчүлүгүнө ээ болобуз.

Синтаксис:

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

Чакыруу объектти жана аны өчүрүүчү proxyфункцияны кайтарат.revoke

Бул жерде бир мисал:

 
 
let object = {
  data: "Важные данные"
};

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

// передаём прокси куда-нибудь вместо оригинального объекта...
alert(proxy.data); // Важные данные

// позже в коде
revoke();

// прокси больше не работает (отключён)
alert(proxy.data); // Ошибка@A@

Чакыруу revoke()проксиден түпнуска объектке болгон бардык ички шилтемелерди алып салат, андыктан алардын ортосунда эч кандай байланыш жок, эми баштапкы объект таштанды чогултулушу мүмкүн.

revokeПрокси объекти аркылуу WeakMapоңой табуу үчүн функцияны сактай алабыз :

@A@let revokes = new WeakMap();

let object = {
  data: "Важные данные"
};

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

revokes.set(proxy, revoke);

// ..позже в коде..
revoke = revokes.get(proxy);
revoke();

alert(proxy.data); // Ошибка (прокси отключён)@A@

Бул ыкманын артыкчылыгы - бул функцияны сүйрөп кетүүнүн кереги жок revokerevokesКерек болсо прокси объектинен алабыз .

WeakMapБиз анын ордуна Mapташтанды чогултууга бөгөт коюу үчүн колдондук . Эгерде прокси объектиге жеткиликсиз болуп калса (башкача айтканда, ал мындан ары шилтеме кылынбаса), анда ал WeakMapташтанды жыйноочуга аны эстутумдан алып салууга мүмкүндүк берет, ошондой эле revokeбул учурда керек эмес.

Шилтемелер

Бардыгы

Прокси - бул объекттин айланасындагы оролгон, ал "демейки боюнча" андагы операцияларды объектке багыттайт, бирок аларды кармап калуу мүмкүнчүлүгү бар.

Класстарды жана функцияларды кошо алганда, ар кандай объект прокси болушу мүмкүн.

Синтаксис:

@A@let proxy = new Proxy(target, {
  /* ловушки */
});@A@

…Андан кийин бардык жерде баштапкы объекттин ордуна прокси колдонуу кеңири таралган target. Проксидин өзүнүн касиеттери же ыкмалары жок. Тиешелүү илгич бар болсо, ал жөн гана операцияны токтотот, антпесе аны түз target.

Биз кармай алабыз:

  • Окуу ( get), жазуу ( set), жок кылуу ( deleteProperty) касиет (ал жок болсо да).
  • Функция чакыруу ( apply).
  • Оператор new(капкан construct).
  • Жана башка көптөгөн операциялар (толук тизме макаланын башында берилген, ошондой эле документтерде ) .

Бул бизге "виртуалдык" касиеттерди жана ыкмаларды түзүүгө, демейки маанилерди ишке ашырууга, байкоо жүргүзүүгө, жасалгалоочу функцияларга жана башкаларга мүмкүндүк берет.

Биз ошондой эле бир эле объектти ар кандай проксиге көп жолу ороп, ага функциянын ар кандай аспектилерин кошо алабыз.

Reflect API Проксиге кошумча катары түзүлгөн . Ар бир илмек үчүн ошол эле аргументтер менен Proxyыкма бар . ReflectЭгерде биз чалууну баштапкы объектке багыттоону кааласак, аны колдонушубуз керек.

Проксилердин кээ бир чектөөлөрү бар:

  • Киргизилген объекттер "ички уячалар" деп аталгандарды колдонушат, аларга кирүү проксиге кирбейт. Бирок, бул бөлүмдүн башында, бул чектөөдөн өтүүнүн бир жолу көрсөтүлгөн.
  • Жеке класстык талаалар жөнүндө да ушуну айтууга болот, анткени алар уячалардын негизинде ишке ашырылат. thisБашкача айтканда, прокси методдорго чалуулар аларга жетүү үчүн баштапкы объектке ээ болушу керек .
  • Объекттерди катуу теңчиликти текшерүүгө ===бөгөт коюуга болбойт.
  • Аткаруучулук: конкреттүү аткаруу котормочудан көз каранды, бирок жалпысынан эң жөнөкөй проксиди колдонуу менен менчикти алуу бир нече эсе көп убакытты талап кылат. Чынында, бул кээ бир "өзгөчө жүктөлгөн" объекттер үчүн гана маанилүү.

Tasks

 

Адатта, объекттен жок касиетти окуганда, кайтарат undefined.

Жок касиетти окууга аракет кылып жатканда ката жаратуучу прокси түзүңүз.

Бул программалык мүчүлүштүктөрдү эрте аныктоого жардам берет.

wrap(target)Объектти алган targetжана ага функциянын ошол аспектисин кошкон проксиди кайтарган функцияны жазыңыз .

Бул жерде ал кантип иштеши керек:

@A@let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
      /* ваш код */
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // Ошибка: такого свойства не существует@A@
чечим
 

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

Бул сыяктуу:

@A@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, {
  /* ваш код */
});

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

// вся остальная функциональность массивов должна остаться без изменений@A@
чечим
 

makeObservable(target)Проксиди кайтаруу менен объектти "байкоочу" кылган функцияны түзүңүз .

Бул жерде ал кантип иштеши керек:

@A@function makeObservable(target) {
  /* ваш код */
}

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

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

user.name = "John"; // выводит: SET name=John@A@

Башка сөз менен айтканда, кайтарылган makeObservableобъект оригиналдуу окшош, бирок ошондой эле ар кандай мулк өзгөртүү боюнча observe(handler)иштетүүгө мүмкүндүк берет ыкмасы бар handler.

handler(key, value)Кандайдыр бир касиет өзгөргөндө, ал мүлктүн аты жана наркы менен аталат .

PS Бул көйгөйдө, менчигиңизди жазуу менен гана чектелиңиз. Калган операцияларды да ушундай жол менен ишке ашырууга болот.