Svenska

En omfattande guide för globala utvecklare om att bemästra JavaScript Proxy API. Lär dig att fånga upp och anpassa objektoperationer med praktiska exempel, användningsfall och prestandatips.

JavaScript Proxy API: En Djupdykning i Modifiering av Objektbeteende

I det föränderliga landskapet av modern JavaScript söker utvecklare ständigt efter mer kraftfulla och eleganta sätt att hantera och interagera med data. Medan funktioner som klasser, moduler och async/await har revolutionerat hur vi skriver kod, finns det en kraftfull metaprogrammeringsfunktion introducerad i ECMAScript 2015 (ES6) som ofta förblir outnyttjad: Proxy API:et.

Metaprogrammering kan låta skrämmande, men det är helt enkelt konceptet att skriva kod som opererar på annan kod. Proxy API:et är JavaScripts primära verktyg för detta, vilket gör att du kan skapa en 'proxy' för ett annat objekt, som kan fånga upp och omdefiniera grundläggande operationer för det objektet. Det är som att placera en anpassningsbar grindvakt framför ett objekt, vilket ger dig fullständig kontroll över hur det nås och modifieras.

Denna omfattande guide kommer att avmystifiera Proxy API:et. Vi kommer att utforska dess kärnkoncept, bryta ner dess olika möjligheter med praktiska exempel och diskutera avancerade användningsfall och prestandaöverväganden. I slutet kommer du att förstå varför Proxies är en hörnsten i moderna ramverk och hur du kan utnyttja dem för att skriva renare, kraftfullare och mer underhållbar kod.

Förstå kärnkoncepten: Target, Handler och Traps

Proxy API:et är uppbyggt kring tre grundläggande komponenter. Att förstå deras roller är nyckeln till att bemästra proxies.

Syntaxen för att skapa en proxy är enkel:

const proxy = new Proxy(target, handler);

Låt oss titta på ett mycket grundläggande exempel. Vi kommer att skapa en proxy som helt enkelt skickar alla operationer vidare till target-objektet genom att använda en tom handler.


// Det ursprungliga objektet
const target = {
  message: "Hej världen!"
};

// En tom handler. Alla operationer kommer att vidarebefordras till target.
const handler = {};

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

// Åtkomst till en egenskap på proxyn
console.log(proxy.message); // Utdata: Hej världen!

// Operationen vidarebefordrades till target
console.log(target.message); // Utdata: Hej världen!

// Modifierar en egenskap via proxyn
proxy.anotherMessage = "Hej, Proxy!";

console.log(proxy.anotherMessage); // Utdata: Hej, Proxy!
console.log(target.anotherMessage); // Utdata: Hej, Proxy!

I detta exempel beter sig proxyn exakt som det ursprungliga objektet. Den verkliga kraften kommer när vi börjar definiera traps i handlern.

Anatomin av en Proxy: Utforska Vanliga Traps

Handler-objektet kan innehålla upp till 13 olika traps, som var och en motsvarar en grundläggande intern metod för JavaScript-objekt. Låt oss utforska de vanligaste och mest användbara.

Egenskapsåtkomst-Traps

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

Detta är förmodligen den mest använda trapen. Den utlöses när en egenskap på proxyn läses.

Exempel: Standardvärden för icke-existerande egenskaper.


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

const userHandler = {
  get(target, property) {
    // Om egenskapen finns på target, returnera den.
    // Annars, returnera ett standardmeddelande.
    return property in target ? target[property] : `Egenskapen '${property}' existerar inte.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Utdata: John
console.log(userProxy.age);       // Utdata: 30
console.log(userProxy.country);   // Utdata: Egenskapen 'country' existerar inte.

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

set-trapen anropas när en egenskap på proxyn tilldelas ett värde. Den är perfekt för validering, loggning eller att skapa skrivskyddade objekt.

Exempel: Datavalidering.


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('Ålder måste vara ett heltal.');
      }
      if (value <= 0) {
        throw new RangeError('Ålder måste vara ett positivt tal.');
      }
    }

    // Om valideringen lyckas, ange värdet på target-objektet.
    target[property] = value;

    // Indikera framgång.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // Detta är giltigt
console.log(personProxy.age); // Utdata: 30

try {
  personProxy.age = 'trettio'; // Kastar TypeError
} catch (e) {
  console.error(e.message); // Utdata: Ålder måste vara ett heltal.
}

try {
  personProxy.age = -5; // Kastar RangeError
} catch (e) {
  console.error(e.message); // Utdata: Ålder måste vara ett positivt tal.
}

3. `has(target, property)`

Denna trap fångar upp in-operatorn. Den låter dig kontrollera vilka egenskaper som verkar finnas på ett objekt.

Exempel: Dölja 'privata' egenskaper.

I JavaScript är en vanlig konvention att prefixa privata egenskaper med ett understreck (_). Vi kan använda has-trapen för att dölja dessa från in-operatorn.


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

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Låtsas att det inte existerar
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Utdata: true
console.log('_apiKey' in dataProxy);   // Utdata: false (även om det är på target)
console.log('id' in dataProxy);        // Utdata: true

Obs: Detta påverkar endast in-operatorn. Direkt åtkomst som dataProxy._apiKey skulle fortfarande fungera om du inte också implementerar en motsvarande get-trap.

4. `deleteProperty(target, property)`

Denna trap körs när en egenskap raderas med delete-operatorn. Den är användbar för att förhindra radering av viktiga egenskaper.

Trapen måste returnera true för en lyckad radering eller false för en misslyckad.

Exempel: Förhindra radering av egenskaper.


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

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Försök att radera skyddad egenskap: '${property}'. Åtgärden nekades.`);
      return false;
    }
    return true; // Egenskapen existerade ändå inte
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Konsol-utdata: Försök att radera skyddad egenskap: 'port'. Åtgärden nekades.

console.log(configProxy.port); // Utdata: 8080 (Den raderades inte)

Objekt-Enumeration och Beskrivnings-Traps

5. `ownKeys(target)`

Denna trap utlöses av operationer som får listan över ett objekts egna egenskaper, såsom Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() och Reflect.ownKeys().

Exempel: Filtrering av nycklar.

Låt oss kombinera detta med vårt tidigare 'privata' egenskapsexempel för att helt dölja dem.


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) {
    // Förhindra även direkt åtkomst
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

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

Observera att vi använder Reflect här. Reflect-objektet tillhandahåller metoder för avlyssningsbara JavaScript-operationer, och dess metoder har samma namn och signaturer som proxy-traps. Det är en bästa praxis att använda Reflect för att vidarebefordra den ursprungliga operationen till target, vilket säkerställer att standardbeteendet upprätthålls korrekt.

Funktions- och Konstruktor-Traps

Proxies är inte begränsade till vanliga objekt. När target är en funktion kan du fånga upp anrop och konstruktioner.

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

Denna trap anropas när en proxy för en funktion körs. Den fångar upp funktionsanropet.

Exempel: Loggning av funktionsanrop och deras argument.


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

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Anropar funktionen '${target.name}' med argument: ${argumentsList}`);
    // Utför den ursprungliga funktionen med rätt kontext och argument
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`Funktionen '${target.name}' returnerade: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Konsol-utdata:
// Anropar funktionen 'sum' med argument: 5,10
// Funktionen 'sum' returnerade: 15

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

Denna trap fångar upp användningen av new-operatorn på en proxy för en klass eller funktion.

Exempel: Singleton-mönster-implementering.


class MyDatabaseConnection {
  constructor(url) {
    this.url = url;
    console.log(`Ansluter till ${this.url}...`);
  }
}

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Skapar ny instans.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Returnerar befintlig instans.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Konsol-utdata:
// Skapar ny instans.
// Ansluter till db://primary...
// Returnerar befintlig instans.

const conn2 = new ProxiedConnection('db://secondary'); // URL ignoreras
// Konsol-utdata:
// Returnerar befintlig instans.

console.log(conn1 === conn2); // Utdata: true
console.log(conn1.url); // Utdata: db://primary
console.log(conn2.url); // Utdata: db://primary

Praktiska Användningsfall och Avancerade Mönster

Nu när vi har täckt de enskilda trapsen, låt oss se hur de kan kombineras för att lösa problem i den verkliga världen.

1. API-Abstraktion och Datatransformering

API:er returnerar ofta data i ett format som inte matchar dina applikationskonventioner (t.ex. snake_case vs. camelCase). En proxy kan transparent hantera denna konvertering.


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

// Föreställ dig att detta är våra rådata från ett API
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // Kontrollera om camelCase-versionen existerar direkt
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Fallback till ursprungligt egenskapsnamn
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// Vi kan nu komma åt egenskaper med camelCase, även om de lagras som snake_case
console.log(userModel.userId);        // Utdata: 123
console.log(userModel.firstName);     // Utdata: Alice
console.log(userModel.accountStatus); // Utdata: active

2. Observerbara och Databindning (Kärnan i Moderna Ramverk)

Proxies är motorn bakom reaktivitetssystemen i moderna ramverk som Vue 3. När du ändrar en egenskap på ett proxierat statsobjekt kan set-trapen användas för att utlösa uppdateringar i UI:t eller andra delar av applikationen.

Här är ett mycket förenklat exempel:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Utlös callback vid ändring
      return result;
    }
  };
  return new Proxy(target, handler);
}

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

function render(prop, value) {
  console.log(`ÄNDRING UPPTÄCKT: Egenskapen '${prop}' sattes till '${value}'. Återger UI...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Konsol-utdata: ÄNDRING UPPTÄCKT: Egenskapen 'count' sattes till '1'. Återger UI...

observableState.message = 'Adjö';
// Konsol-utdata: ÄNDRING UPPTÄCKT: Egenskapen 'message' sattes till 'Adjö'. Återger UI...

3. Negativa Array-Index

Ett klassiskt och roligt exempel är att utöka infödda array-beteende för att stödja negativa index, där -1 hänvisar till det sista elementet, liknande språk som Python.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Konvertera negativt index till ett positivt från slutet
        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]);  // Utdata: a
console.log(proxiedArray[-1]); // Utdata: e
console.log(proxiedArray[-2]); // Utdata: d
console.log(proxiedArray.length); // Utdata: 5

Prestandaöverväganden och Bästa Praxis

Medan proxies är otroligt kraftfulla är de inte en magisk kula. Det är avgörande att förstå deras implikationer.

Prestanda-overhead

En proxy introducerar ett lager av indirektion. Varje operation på ett proxierat objekt måste passera genom handlern, vilket lägger till en liten mängd overhead jämfört med en direkt operation på ett vanligt objekt. För de flesta applikationer (som datavalidering eller reaktivitet på ramverksnivå) är denna overhead försumbar. Men i prestandakritisk kod, såsom en tät slinga som bearbetar miljontals objekt, kan detta bli en flaskhals. Benchmarka alltid om prestanda är ett primärt problem.

Proxy-Invarianter

En trap kan inte helt ljuga om target-objektets natur. JavaScript framtvingar en uppsättning regler som kallas 'invarianter' som proxy-traps måste lyda. Att bryta mot en invariant kommer att resultera i en TypeError.

Till exempel är en invariant för deleteProperty-trapen att den inte kan returnera true (vilket indikerar framgång) om motsvarande egenskap på target-objektet inte är konfigurerbar. Detta förhindrar att proxyn påstår sig ha raderat en egenskap som inte kan raderas.


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

const handler = {
  deleteProperty(target, prop) {
    // Detta kommer att bryta mot invarianten
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // Detta kommer att kasta ett fel
} catch (e) {
  console.error(e.message);
  // Utdata: 'deleteProperty' på proxy: returnerade true för icke-konfigurerbar egenskap 'unbreakable'
}

När man ska använda Proxies (och när man inte ska)

Revokabla Proxies

För scenarier där du kan behöva 'stänga av' en proxy (t.ex. av säkerhetsskäl eller minneshantering) tillhandahåller JavaScript Proxy.revocable(). Den returnerar ett objekt som innehåller både proxyn och en revoke-funktion.


const target = { data: 'känsligt' };
const handler = {};

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

console.log(proxy.data); // Utdata: känsligt

// Nu återkallar vi proxyns åtkomst
revoke();

try {
  console.log(proxy.data); // Detta kommer att kasta ett fel
} catch (e) {
  console.error(e.message);
  // Utdata: Kan inte utföra 'get' på en proxy som har återkallats
}

Proxies vs. Andra Metaprogrammeringstekniker

Före Proxies använde utvecklare andra metoder för att uppnå liknande mål. Det är användbart att förstå hur Proxies jämförs.

Object.defineProperty()

Object.defineProperty() modifierar ett objekt direkt genom att definiera getters och setters för specifika egenskaper. Proxies, å andra sidan, modifierar inte det ursprungliga objektet alls; de wrappar det.

Slutsats: Kraften i Virtualisering

JavaScript Proxy API är mer än bara en smart funktion; det är ett fundamentalt skifte i hur vi kan designa och interagera med objekt. Genom att tillåta oss att fånga upp och anpassa grundläggande operationer öppnar Proxies dörren till en värld av kraftfulla mönster: från sömlös datavalidering och transformering till de reaktiva systemen som driver moderna användargränssnitt.

Även om de kommer med en liten prestandakostnad och en uppsättning regler att följa, är deras förmåga att skapa rena, frikopplade och kraftfulla abstraktioner oöverträffad. Genom att virtualisera objekt kan du bygga system som är mer robusta, underhållbara och uttrycksfulla. Nästa gång du står inför en komplex utmaning som involverar datahantering, validering eller observerbarhet, överväg om en Proxy är rätt verktyg för jobbet. Det kan bara vara den mest eleganta lösningen i din verktygslåda.