Čeština

Komplexní průvodce pro globální vývojáře k zvládnutí JavaScript Proxy API. Naučte se zachytávat a upravovat operace s objekty pomocí praktických příkladů, případů použití a tipů pro výkon.

JavaScript Proxy API: Hloubkový ponor do modifikace chování objektů

V neustále se vyvíjejícím prostředí moderního JavaScriptu vývojáři neustále hledají výkonnější a elegantnější způsoby správy a interakce s daty. Zatímco funkce jako třídy, moduly a async/await způsobily revoluci ve způsobu, jakým píšeme kód, existuje výkonná funkce metaprogramování zavedená v ECMAScript 2015 (ES6), která často zůstává nedostatečně využívaná: Proxy API.

Metaprogramování může znít zastrašujícím způsobem, ale je to jednoduše koncept psaní kódu, který pracuje s jiným kódem. Proxy API je primární nástroj JavaScriptu pro toto, který vám umožňuje vytvořit 'proxy' pro jiný objekt, který může zachytit a předefinovat základní operace pro daný objekt. Je to jako umístit přizpůsobitelného vrátného před objekt, což vám dává úplnou kontrolu nad tím, jak se k němu přistupuje a jak je upravován.

Tento komplexní průvodce objasní Proxy API. Prozkoumáme jeho základní koncepty, rozebereme jeho různé schopnosti s praktickými příklady a prodiskutujeme pokročilé případy použití a úvahy o výkonu. Na konci pochopíte, proč jsou Proxies základním kamenem moderních frameworků a jak je můžete využít k psaní čistšího, výkonnějšího a udržitelnějšího kódu.

Pochopení základních konceptů: Target, Handler a Traps

Proxy API je postaveno na třech základních komponentách. Pochopení jejich rolí je klíčem k zvládnutí proxies.

Syntaxe pro vytvoření proxy je přímočará:

const proxy = new Proxy(target, handler);

Podívejme se na velmi jednoduchý příklad. Vytvoříme proxy, které jednoduše předává všechny operace do cílového objektu pomocí prázdného handleru.


// Původní objekt
const target = {
  message: "Hello, World!"
};

// Prázdný handler. Všechny operace budou předány do cíle.
const handler = {};

// Proxy objekt
const proxy = new Proxy(target, handler);

// Přístup k vlastnosti na proxy
console.log(proxy.message); // Výstup: Hello, World!

// Operace byla předána do cíle
console.log(target.message); // Výstup: Hello, World!

// Úprava vlastnosti prostřednictvím proxy
proxy.anotherMessage = "Hello, Proxy!";

console.log(proxy.anotherMessage); // Výstup: Hello, Proxy!
console.log(target.anotherMessage); // Výstup: Hello, Proxy!

V tomto příkladu se proxy chová přesně jako původní objekt. Skutečná síla přichází, když začneme definovat traps v handleru.

Anatomie Proxy: Prozkoumání běžných Traps

Objekt handleru může obsahovat až 13 různých traps, z nichž každý odpovídá základní interní metodě objektů JavaScriptu. Prozkoumejme nejběžnější a nejužitečnější.

Traps pro přístup k vlastnostem

1. `get(target, property, receiver)`

Toto je pravděpodobně nejpoužívanější trap. Spouští se, když je čtena vlastnost proxy.

Příklad: Výchozí hodnoty pro neexistující vlastnosti.


const user = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30
};

const userHandler = {
  get(target, property) {
    // Pokud vlastnost existuje na targetu, vraťte ji.
    // Jinak vraťte výchozí zprávu.
    return property in target ? target[property] : `Vlastnost '${property}' neexistuje.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Výstup: John
console.log(userProxy.age);       // Výstup: 30
console.log(userProxy.country);   // Výstup: Vlastnost 'country' neexistuje.

2. `set(target, property, value, receiver)`

Trap set je volán, když je vlastnosti proxy přiřazena hodnota. Je ideální pro validaci, protokolování nebo vytváření objektů jen pro čtení.

Příklad: Validace dat.


const person = {
  name: 'Jane Doe',
  age: 25
};

const validationHandler = {
  set(target, property, value) {
    if (property === 'age') {
      if (typeof value !== 'number' || !Number.isInteger(value)) {
        throw new TypeError('Věk musí být celé číslo.');
      }
      if (value <= 0) {
        throw new RangeError('Věk musí být kladné číslo.');
      }
    }

    // Pokud validace projde, nastavte hodnotu na target objektu.
    target[property] = value;

    // Označte úspěch.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // Toto je platné
console.log(personProxy.age); // Výstup: 30

try {
  personProxy.age = 'thirty'; // Vyvolá TypeError
} catch (e) {
  console.error(e.message); // Výstup: Věk musí být celé číslo.
}

try {
  personProxy.age = -5; // Vyvolá RangeError
} catch (e) {
  console.error(e.message); // Výstup: Věk musí být kladné číslo.
}

3. `has(target, property)`

Tento trap zachycuje operátor in. Umožňuje vám kontrolovat, které vlastnosti se zdají existovat na objektu.

Příklad: Skrytí 'soukromých' vlastností.

V JavaScriptu je běžnou konvencí předpona soukromých vlastností podtržítkem (_). Můžeme použít trap has k jejich skrytí před operátorem in.


const secretData = {
  _apiKey: 'xyz123abc',
  publicKey: 'pub456def',
  id: 1
};

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Předstírejte, že neexistuje
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Výstup: true
console.log('_apiKey' in dataProxy);   // Výstup: false (i když je na targetu)
console.log('id' in dataProxy);        // Výstup: true

Poznámka: Toto ovlivňuje pouze operátor in. Přímý přístup jako dataProxy._apiKey by stále fungoval, pokud také neimplementujete odpovídající trap get.

4. `deleteProperty(target, property)`

Tento trap je spuštěn, když je vlastnost smazána pomocí operátoru delete. Je užitečný pro zabránění smazání důležitých vlastností.

Trap musí vrátit true pro úspěšné smazání nebo false pro neúspěšné.

Příklad: Zabránění smazání vlastností.


const immutableConfig = {
  databaseUrl: 'prod.db.server',
  port: 8080
};

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Pokus o smazání chráněné vlastnosti: '${property}'. Operace zamítnuta.`);
      return false;
    }
    return true; // Vlastnost stejně neexistovala
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Výstup konzole: Pokus o smazání chráněné vlastnosti: 'port'. Operace zamítnuta.

console.log(configProxy.port); // Výstup: 8080 (Nebyla smazána)

Traps pro výčet a popis objektů

5. `ownKeys(target)`

Tento trap je spuštěn operacemi, které získají seznam vlastních vlastností objektu, jako jsou Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() a Reflect.ownKeys().

Příklad: Filtrování klíčů.

Pojďme to zkombinovat s naším předchozím příkladem 'soukromé' vlastnosti, abychom je plně skryli.


const secretData = {
  _apiKey: 'xyz123abc',
  publicKey: 'pub456def',
  id: 1
};

const keyHidingHandler = {
  has(target, property) {
    return !property.startsWith('_') && property in target;
  },
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(key => !key.startsWith('_'));
  },
  get(target, property, receiver) {
    // Zabraňte také přímému přístupu
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

console.log(Object.keys(fullProxy)); // Výstup: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // Výstup: true
console.log('_apiKey' in fullProxy);   // Výstup: false
console.log(fullProxy._apiKey);      // Výstup: undefined

Všimněte si, že zde používáme Reflect. Objekt Reflect poskytuje metody pro zachytitelné operace JavaScriptu a jeho metody mají stejné názvy a podpisy jako proxy traps. Je osvědčeným postupem používat Reflect k předání původní operace do cíle, což zajišťuje správné zachování výchozího chování.

Traps pro funkce a konstruktory

Proxies se neomezují pouze na prosté objekty. Když je cílem funkce, můžete zachytit volání a konstrukce.

6. `apply(target, thisArg, argumentsList)`

Tento trap je volán, když je spuštěno proxy funkce. Zachycuje volání funkce.

Příklad: Protokolování volání funkcí a jejich argumentů.


function sum(a, b) {
  return a + b;
}

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Volání funkce '${target.name}' s argumenty: ${argumentsList}`);
    // Spusťte původní funkci se správným kontextem a argumenty
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`Funkce '${target.name}' vrátila: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Výstup konzole:
// Volání funkce 'sum' s argumenty: 5,10
// Funkce 'sum' vrátila: 15

7. `construct(target, argumentsList, newTarget)`

Tento trap zachycuje použití operátoru new na proxy třídy nebo funkce.

Příklad: Implementace vzoru Singleton.


class MyDatabaseConnection {
  constructor(url) {
    this.url = url;
    console.log(`Připojování k ${this.url}...`);
  }
}

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Vytváření nové instance.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Vracení existující instance.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Výstup konzole:
// Vytváření nové instance.
// Připojování k db://primary...
// Vracení existující instance.

const conn2 = new ProxiedConnection('db://secondary'); // URL bude ignorována
// Výstup konzole:
// Vracení existující instance.

console.log(conn1 === conn2); // Výstup: true
console.log(conn1.url); // Výstup: db://primary
console.log(conn2.url); // Výstup: db://primary

Praktické případy použití a pokročilé vzory

Nyní, když jsme probrali jednotlivé traps, podívejme se, jak je lze kombinovat k řešení problémů reálného světa.

1. Abstrakce API a transformace dat

API často vracejí data ve formátu, který neodpovídá konvencím vaší aplikace (např. snake_case vs. camelCase). Proxy může transparentně zpracovat tuto konverzi.


function snakeToCamel(s) {
  return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}

// Představte si, že toto jsou naše surová data z API
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // Zkontrolujte, zda verze camelCase existuje přímo
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Vraťte se k původnímu názvu vlastnosti
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// Nyní můžeme přistupovat k vlastnostem pomocí camelCase, i když jsou uloženy jako snake_case
console.log(userModel.userId);        // Výstup: 123
console.log(userModel.firstName);     // Výstup: Alice
console.log(userModel.accountStatus); // Výstup: active

2. Observables a Data Binding (Jádro moderních frameworků)

Proxies jsou motorem reaktivních systémů v moderních frameworkech, jako je Vue 3. Když změníte vlastnost na proxied stavovém objektu, trap set lze použít ke spuštění aktualizací v uživatelském rozhraní nebo jiných částech aplikace.

Zde je vysoce zjednodušený příklad:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Spusťte zpětné volání při změně
      return result;
    }
  };
  return new Proxy(target, handler);
}

const state = {
  count: 0,
  message: 'Hello'
};

function render(prop, value) {
  console.log(`DETEKOVÁNA ZMĚNA: Vlastnost '${prop}' byla nastavena na '${value}'. Opětovné vykreslování UI...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Výstup konzole: DETEKOVÁNA ZMĚNA: Vlastnost 'count' byla nastavena na '1'. Opětovné vykreslování UI...

observableState.message = 'Goodbye';
// Výstup konzole: DETEKOVÁNA ZMĚNA: Vlastnost 'message' byla nastavena na 'Goodbye'. Opětovné vykreslování UI...

3. Záporné indexy polí

Klasickým a zábavným příkladem je rozšíření nativního chování polí pro podporu záporných indexů, kde -1 odkazuje na poslední prvek, podobně jako v jazycích jako Python.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Převeďte záporný index na kladný od konce
        property = String(target.length + index);
      }
      return Reflect.get(target, property);
    }
  };
  return new Proxy(arr, handler);
}

const originalArray = ['a', 'b', 'c', 'd', 'e'];
const proxiedArray = createNegativeArrayProxy(originalArray);

console.log(proxiedArray[0]);  // Výstup: a
console.log(proxiedArray[-1]); // Výstup: e
console.log(proxiedArray[-2]); // Výstup: d
console.log(proxiedArray.length); // Výstup: 5

Úvahy o výkonu a osvědčené postupy

I když jsou proxies neuvěřitelně výkonné, nejsou zázračnou kulkou. Je důležité porozumět jejich důsledkům.

Režie výkonu

Proxy zavádí vrstvu nepřímého přístupu. Každá operace na proxied objektu musí projít handlerem, což přidává malé množství režie ve srovnání s přímou operací na prostém objektu. U většiny aplikací (jako je validace dat nebo reaktivita na úrovni frameworku) je tato režie zanedbatelná. Nicméně v kódu kritickém pro výkon, jako je smyčka zpracovávající miliony položek, se to může stát úzkým hrdlem. Vždy provádějte benchmark, pokud je výkon primárním problémem.

Proxy Invariants

Trap nemůže zcela lhát o povaze cílového objektu. JavaScript vynucuje sadu pravidel nazývaných 'invariants', které proxy traps musí dodržovat. Porušení invariant způsobí TypeError.

Například invariant pro trap deleteProperty je, že nemůže vrátit true (označující úspěch), pokud odpovídající vlastnost na cílovém objektu není nekonfigurovatelná. To zabraňuje proxy tvrdit, že smazala vlastnost, kterou nelze smazat.


const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });

const handler = {
  deleteProperty(target, prop) {
    // Toto poruší invariant
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // Toto vyvolá chybu
} catch (e) {
  console.error(e.message);
  // Výstup: 'deleteProperty' na proxy: vrátil true pro nekonfigurovatelnou vlastnost 'unbreakable'
}

Kdy používat Proxies (a kdy ne)

Odvolatelné Proxies

Pro scénáře, kde možná budete muset 'vypnout' proxy (např. z bezpečnostních důvodů nebo správy paměti), JavaScript poskytuje Proxy.revocable(). Vrátí objekt obsahující jak proxy, tak funkci revoke.


const target = { data: 'sensitive' };
const handler = {};

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

console.log(proxy.data); // Výstup: sensitive

// Nyní odvoláme přístup proxy
revoke();

try {
  console.log(proxy.data); // Toto vyvolá chybu
} catch (e) {
  console.error(e.message);
  // Výstup: Nelze provést 'get' na proxy, které bylo odvoláno
}

Proxies vs. Jiné techniky metaprogramování

Před Proxies vývojáři používali jiné metody k dosažení podobných cílů. Je užitečné porozumět tomu, jak se Proxies srovnávají.

`Object.defineProperty()`

Object.defineProperty() upravuje objekt přímo definováním getters a setters pro konkrétní vlastnosti. Proxies na druhou stranu nemění původní objekt vůbec; obalují ho.

Závěr: Síla virtualizace

JavaScript Proxy API je více než jen chytrá funkce; je to zásadní posun ve způsobu, jakým můžeme navrhovat a interagovat s objekty. Tím, že nám umožňuje zachytit a přizpůsobit základní operace, Proxies otevírají dveře do světa výkonných vzorů: od bezproblémové validace a transformace dat po reaktivní systémy, které pohánějí moderní uživatelská rozhraní.

I když přicházejí s malými náklady na výkon a sadou pravidel, která je třeba dodržovat, jejich schopnost vytvářet čisté, oddělené a výkonné abstrakce je bezkonkurenční. Virtualizací objektů můžete budovat systémy, které jsou robustnější, udržitelnější a expresivnější. Až se příště setkáte se složitou výzvou týkající se správy dat, validace nebo pozorovatelnosti, zvažte, zda je Proxy tím správným nástrojem pro danou práci. Může to být nejelegantnější řešení ve vaší sadě nástrojů.

JavaScript Proxy API: Hloubkový ponor do modifikace chování objektů | MLOG