Галоўная / Артыкулы / Розумеўце JavaScript Proxy: патэрны Traps, Reflect і Reactive

Розумеўце JavaScript Proxy: патэрны Traps, Reflect і Reactive

Дазвольце дазнацься, як об’екты Proxy і Reflect у JavaScript перехопляюць доступ да атрыбутаў, што дапамагае рэалізаваць верыфікацыю, вярчыныя атрыбуты і реактыўныя фрэймворкі.

1096 слоў

Кожны раз, калі вы пісваеце obj.property або прызначаеце ў яго значэнне за дапамою obj.property = value, вы выкарыстоўваеце аперацыі, якія JavaScript выканаўляе таямніча, без жадных вбудованых спосабоў для стежэння за тым, што відбываецца пад час ўжытку. Об’ект Proxy цэлкам усуне гэтая прыпуск. Ён дазволяе вам абгортаваць цэльвы об’ект і перехопляць гэтыя фундаментальныя аперацыі – чытанне, запіс, выдаленне – прычыму яны не набудуць чыннасці. Гэты механізм перехоплення ёст канкрэтны мотор, які стоіць за багатымі рэштамі, якія выглядаюць як „чары“ у сучасных бібліотеках і інструментах JavaScript.

Абгортавач, які можа перехопляць все

Proxy знаходзіцца межы вашага коду і об’екта, які ён абгортае, а набор функцый-пасток вяршыць, што на самай працэ выканаецца для кожнага типу аперацыі, якая выканана над цим об’ектам.

const config = { retries: 3, timeout: 5000 };

const guardedConfig = new Proxy(config, {
  set(target, key, value) {
    if (key === "retries" && (typeof value !== "number" || value < 0)) {
      throw new TypeError("retries must be a non-negative number");
    }
    target[key] = value;
    return true;
  },
});

guardedConfig.retries = 5;   // works fine
guardedConfig.retries = -1;  // throws TypeError, caught before it ever reaches the object

Нічы не выдзеляеся ў напісанні guardedConfig.retries = 5 паўтарою з звычным прызначэнням значэння атрыбута. Гэта самэ ўсё і є метайя дызайну: перехопленне заступніка застаецца непазначальным у месцы вызову. Можна дадаць перакананне, логаванне чы іншыя парадуктывны эфекты да таго, што выглядае як звычны доступ да атрыбута, не вымагаючы жадных змян у кодзе, які фактычна выкарыстоўвае об’ект.

Механізм, які стоіць за реактыўнымі фрэймворкамі

function reactive(target, onChange) {
  return new Proxy(target, {
    get(obj, key) {
      return obj[key];
    },
    set(obj, key, value) {
      const changed = obj[key] !== value;
      obj[key] = value;
      if (changed) onChange(key, value);
      return true;
    },
  });
}

const state = reactive({ count: 0 }, (key, value) => {
  console.log(`${key} changed to ${value}, re-rendering...`);
});

state.count = 1; // "count changed to 1, re-rendering..." — no explicit call needed

Запіс state.count = 1 выглядае як абсалютна звычайная прызначэння, таму што сынтаксічна ён такой і є. Тое, што надае ёй значэння, — це «пастка» set, яка ператварае гэтую звычайную лінію ў запуск падзеі, на якую можа адказаць рэшта системы. Гэта справжня база, на якой паслужаюць реактыўныя фрэймворкі. Не існуе якога-то спецыяльнага типу дадзеных «observable», які трэба включаць у кожным месца. Це звычайныя об’екты, загорнутыя ў Proxy, які тыхо фіксуе кожную запіс у іх.

Вяртуальныя атрыбуты: значэнні, якія існуюць толькі калі вы прасіце

«Пастка» get можа без проблем вернуць значэнне, якое на самай справе ніколи не было збераганае на базовым об’екте, тое значна вы можете адкрываць вырахаваныя чы рэгруэраваныя значэнні так, нібы яны былі звычайнымі атрыбутамі.

const product = { name: "Desk Lamp", priceCents: 3499 };

const productView = new Proxy(product, {
  get(target, key) {
    if (key === "priceDollars") {
      return (target.priceCents / 100).toFixed(2);
    }
    return target[key];
  },
});

console.log(productView.priceDollars); // "34.99" — computed on read, not stored anywhere

У об’екте product няма жаданага поля priceDollars. Ён ствараецца на месцы, кожны раз, калі даўаецца да выкарыстоўвання, унутры функціи get. Гэта сутнасова разлічыцца ад геттера, які пішаць можна з get унутры класу або об’екта, таму што Proxy можа перасланяць выкарыстоўванне ключоў, якія ніколі не былі заявлены раней. Гэта корыстна, напрыклад, калі хочаце абгорнуць адпаведзь API дапамога дадатковых вырахоўваных поль без змены самага первіснага даных.

Чаму Reflect часта выкорыстоўваецца разам з Proxy

Є тонкасць, якая паставляе прыгожданне людям праз першы раз, калі яны пішаць функцію-перасланяльнік, якая должна перайсці да стандартнага поведзення. Выконанне гэтага простым спосабам лёгка прыводзіць да тонкіх парадоксаў.

const handler = {
  get(target, key) {
    console.log(`reading ${key}`);
    return target[key]; // works, but loses some edge-case correctness
  },
};

Для простых, звычных об’ектаў такі падход у большасці случаяў працюе, але безпечнейшы і правільнейшы спосаб праадрасавання операцыі да яе стандартнага поведзення — цэў праз Reflect. Ён адкрывае функцыю для кожнай прыгоды, якая точна відтворае операцыю, зберагчы правыя налаштаванні this і правільна справляючыся з болей складнымі ситуацыямі, такімі як спадчыны гетеры.

const handler = {
  get(target, key, receiver) {
    console.log(`reading ${key}`);
    return Reflect.get(target, key, receiver);
  },
};

Выклік Reflect.get(target, key, receiver) воспамінае абсалютна тое ж, што бы здарылася пад час звычайнага доступу да атрыбута, уключаючы крайнія случаи, якія стосуюцца протатэпаў і гетераў, якія залежнаць ад this, а таксама ситуацыі, калі target[key] можа таямна даць некоректны рэзультат. У практычнам коде Proxy прыметкай яўляецца тое, што практычна завжды выклікае вярнага Reflect-метода з усероўні кожнага «трапа», як толькі вы не намеравана зменяеце стандартную працэздатнась, адколі гэтае ўсуненне гарантуеяць, што ён будзе адпавядаць тому, што зазвычай здарылася пад час звычайнага доступу да атрыбута.

«Трапы», якія обмежваюць, а не расшыроўваюць

Трапы не толькі корыстныя для дадзення певнага поведэння об’екту, але таксама можу лёгкая ўсунуць яго. Трап has кантролюе тое, што вяртае аператар in, а трап deleteProperty можа цэлкам запрасіць выдаленне атрыбута.

const secureRecord = new Proxy(
  { id: 1, ssn: "123-45-6789" },
  {
    get(target, key) {
      if (key === "ssn") throw new Error("Direct access to ssn is not allowed");
      return Reflect.get(target, key);
    },
    deleteProperty() {
      throw new Error("Deleting fields is not allowed on this record");
    },
  }
);

console.log(secureRecord.id); // 1
console.log(secureRecord.ssn); // throws
delete secureRecord.id; // throws

Такі спосаб захавання сутнацыйно адрозніцаяся ад прыватнага поля класу: яго можна застосавіць да будзь-каго об’екта, без патрэбы, каб той об’ект спачатку быў запісаны як клас, і ён дазволяе задаць спецыфічныя правіла для кожнага атрыбута, якіх простая дырэктыва #private не можа выразіць.

Адна ідея, многі трапы

Кожная «пастка» на самай справе адказвае на тую ж самую запытку: што павінна адбыцца ў ту ж момент, калі код прабуе выконаць гэту ўседзённую операцыю над гэтым об’ектам? Чытанне значэння, запіс значэння, выдаленне значэння, перакананне, чы існуе ключ — гэтыя ўсе зазвычай є невидмымі, автаматычнымі дзеяннямі, а Proxy прыводзіць кожнае з іх у момент, які можна спазіраваць або перенаправляць. Системы рэакtyвнага стану, кэшавальныя прыемкі для верыфікацыі, шары кантролю доступу, вяртуальныя вычысляваныя поля — ніхто з гэтых не ўскладненыя трюкі, взятыя з разных джэрел. Усе яны пабудаваны на ідэнтычным аднойчынным механізме, проста нацэленыя на разную «пастку».

Супаўязаная літэратура