Obiekt Proxy jest używany w celu definiowania specyficznego zachowania dla podstawowych operacji (n.p. wyszukiwanie atrybutu, przypisanie, wyliczanie, wywołanie funkcji, etc).

Terminologia

handler
Zastępczy obiekt zawierający pułapki (traps).
traps
Metody zapewniające dostęp do atrybutów. Pojęcie to jest analogiczne do pułapek w systemie operacyjnym.
target
Obiekt wirtualizowany przez proxy. Często jest używany aby magazyować dane obiektu proxy. Niezmienniki (wartości które pozostają niezmienione) dotyczące nierozszerzalności obiektu lub niekonfigurowalnnych atrybutów są weryfikowane w oparciu o target.

Składnia

var p = new Proxy(target, handler);

Parametry

target
Docelowy obiekt (może być obiektem dowolnego typu, włącznie z wbudowanymi tablicami, funkcjami, a nawet innyi obiektami proxy) przeznaczony do opakowania przez Proxy.
handler
Obiekt obsługujący którego atrybuty są funkcjami definiującymi zachowanie proxy podczas wykonania na nim operacji.

Metody

Proxy.revocable()
Tworzy odwracalny obiekt Proxy.

Metody obiektu obsługującego

Obiekt obsługujący jest obiektem zastępczym zawierającym pułapki dla obieku Proxy.

Wszystkie pułapki są opcjonalne. Jeśli pułapka nie została zdefiniowana, domyślnym zachowaniem jest przekazanie operacji do obiektu docelowego.

handler.getPrototypeOf()
Pułapka na Object.getPrototypeOf.
handler.setPrototypeOf()
Pułapka na Object.setPrototypeOf.
handler.isExtensible()
Pułapka na Object.isExtensible.
handler.preventExtensions()
Pułapka na Object.preventExtensions.
handler.getOwnPropertyDescriptor()
Pułapka na Object.getOwnPropertyDescriptor.
handler.defineProperty()
Pułapka na Object.defineProperty.
handler.has()
Pułapka na operator in.
handler.get()
Pułapka na pobieranie wartości atrybutu.
handler.set()
Pułapka na ustawianie wartości atrybutu.
handler.deleteProperty()
Pułapka na operator delete.
handler.ownKeys()
Pułapka na Object.getOwnPropertyNames i Object.getOwnPropertySymbols.
handler.apply()
Pułapka na wywołanie funkcji.
handler.construct()
Pułapka na operator new.

Niektóre niestandardowe pułapki są przestarzałe i zostały usunięte.

Przykłady

Podstawowy przykład

W tym prostym przykładzie liczba 37 jest zwracana jako domyślna wartość kiedy nazwa atrybutu nie istnieje w obiekcie. W tym celu użyty jest handler get.

var handler = {
    get: function(target, name) {
        return name in target ?
            target[name] :
            37;
    }
};

var p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37

Puste proxy przekazujące

W tym przykładzie używamy wbudowanego obiektu JavaScript do którego proxy przekaże wszystkie zaaplikowane na nim operacje.

var target = {};
var p = new Proxy(target, {});

p.a = 37; // operacja przekazana do obiektu target

console.log(target.a); // 37. Operacja została prawidłowo przekazana

Zwróć uwagę, że ten przykład działa dla obiektów JavaScript jednak nie sprawdzi się w przypadku obiektów przeglądarki takich jak elementy DOM. Sprawdź jedno rozwiązanie.

Walidacja

Używając Proxy, łatwo możesz zwalidować wartości przekazywane do obiektu. Poniższy przykład używa metody obsługującej set.

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // domyślnym zachowaniem jest zapisanie wartości
    obj[prop] = value;

    // oznacza pomyślne wykonanie
    return true;
  }
};

let person = new Proxy({}, validator);

person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // rzuca wyjątek
person.age = 300; // rzuca wyjątek

Rozszerzanie konstruktora

Funkcja proxy może w łatwy sposób rozszerzyć konstruktor innym konstruktorem. W tym przykładzie użyto funkcje obsługujące construct oraz apply.

function extend(sup, base) {
  var descriptor = Object.getOwnPropertyDescriptor(
    base.prototype, 'constructor'
  );
  base.prototype = Object.create(sup.prototype);
  var handler = {
    construct: function(target, args) {
      var obj = Object.create(base.prototype);
      this.apply(target, obj, args);
      return obj;
    },
    apply: function(target, that, args) {
      sup.apply(that, args);
      base.apply(that, args);
    }
  };
  var proxy = new Proxy(base, handler);
  descriptor.value = proxy;
  Object.defineProperty(base.prototype, 'constructor', descriptor);
  return proxy;
}

var Person = function(name) {
  this.name = name;
};

var Boy = extend(Person, function(name, age) {
  this.age = age;
});

Boy.prototype.sex = 'M';

var Peter = new Boy('Peter', 13);
console.log(Peter.sex);  // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age);  // 13

Manipulacja węzłami DOM

Czasami konieczne jest przełączenie atrybutu lub nazwy klasy dwóch innych elementów. Poniższy przykład pokazuje wykonanie funkcją obsługującą set.

let view = new Proxy({
  selected: null
},
{
  set: function(obj, prop, newval) {
    let oldval = obj[prop];

    if (prop === 'selected') {
      if (oldval) {
        oldval.setAttribute('aria-selected', 'false');
      }
      if (newval) {
        newval.setAttribute('aria-selected', 'true');
      }
    }

    // The default behavior to store the value
    obj[prop] = newval;

    // Indicate success
    return true;
  }
});

let i1 = view.selected = document.getElementById('item-1');
console.log(i1.getAttribute('aria-selected')); // 'true'

let i2 = view.selected = document.getElementById('item-2');
console.log(i1.getAttribute('aria-selected')); // 'false'
console.log(i2.getAttribute('aria-selected')); // 'true'

Korekcja wartości i dodatkowych atrybutów

Obiekt proxy products wylicza przekazaną wartość i konwertuje to tablicy w razie potrzeby. Obiekt dodatkowo obsługuje dodatkowy atrybut latestBrowser zarówno jako getter i setter.

let products = new Proxy({
  browsers: ['Internet Explorer', 'Netscape']
},
{
  get: function(obj, prop) {
    // An extra property
    if (prop === 'latestBrowser') {
      return obj.browsers[obj.browsers.length - 1];
    }

    // The default behavior to return the value
    return obj[prop];
  },
  set: function(obj, prop, value) {
    // An extra property
    if (prop === 'latestBrowser') {
      obj.browsers.push(value);
      return true;
    }

    // Convert the value if it is not an array
    if (typeof value === 'string') {
      value = [value];
    }

    // The default behavior to store the value
    obj[prop] = value;

    // Indicate success
    return true;
  }
});

console.log(products.browsers); // ['Internet Explorer', 'Netscape']
products.browsers = 'Firefox'; // przekazano string (przez pomyłkę)
console.log(products.browsers); // ['Firefox'] <- nie ma problemu, wartość jest typu array

products.latestBrowser = 'Chrome';
console.log(products.browsers); // ['Firefox', 'Chrome']
console.log(products.latestBrowser); // 'Chrome'

Wyszukiwanie elementu tablicy po jego właściwości

Poniższe proxy rozszerza tablicę o różne użyteczne funkcjonalności. Jak widać, można elastycznie "definiować" właściwości bez użycia Object.defineProperties. Ten przykład może być użyty aby znaleźć wiersz tabeli po jego komórce. W takim przypadku, celem będzie table.rows.

let products = new Proxy([
  { name: 'Firefox', type: 'browser' },
  { name: 'SeaMonkey', type: 'browser' },
  { name: 'Thunderbird', type: 'mailer' }
],
{
  get: function(obj, prop) {
    // domyślnym zachowaniem jest zwrócenie wartości; prop jest zwykle typu integer
    if (prop in obj) {
      return obj[prop];
    }

    // zwróć liczbę produktów; alias dla products.length
    if (prop === 'number') {
      return obj.length;
    }

    let result, types = {};

    for (let product of obj) {
      if (product.name === prop) {
        result = product;
      }
      if (types[product.type]) {
        types[product.type].push(product);
      } else {
        types[product.type] = [product];
      }
    }

    // znajdź product po nazwie
    if (result) {
      return result;
    }

    // znajdź produkty po typie
    if (prop in types) {
      return types[prop];
    }

    // zwróć typy produktów
    if (prop === 'types') {
      return Object.keys(types);
    }

    return undefined;
  }
});

console.log(products[0]); // { name: 'Firefox', type: 'browser' }
console.log(products['Firefox']); // { name: 'Firefox', type: 'browser' }
console.log(products['Chrome']); // undefined
console.log(products.browser); // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]
console.log(products.types); // ['browser', 'mailer']
console.log(products.number); // 3

Pełna lista przykładów pułapek

W celu stworzenia pełnej listy przykładów pułapek, w celach dydaktycznych, spróbujemy zastosować proxy nanie natywnym obiekcie który się szczególnie nadaje do tego typu operacji: globalny obiekt docCookies stworzony przez "mały framework" opublikowany na stronie document.cookie.

/*
  var docCookies = ... pobranie obiektu "docCookies" tutaj:  
  https://developer.mozilla.org/en-US/docs/DOM/document.cookie#A_little_framework.3A_a_complete_cookies_reader.2Fwriter_with_full_unicode_support
*/

var docCookies = new Proxy(docCookies, {
  get: function (oTarget, sKey) {
    return oTarget[sKey] || oTarget.getItem(sKey) || undefined;
  },
  set: function (oTarget, sKey, vValue) {
    if (sKey in oTarget) { return false; }
    return oTarget.setItem(sKey, vValue);
  },
  deleteProperty: function (oTarget, sKey) {
    if (sKey in oTarget) { return false; }
    return oTarget.removeItem(sKey);
  },
  enumerate: function (oTarget, sKey) {
    return oTarget.keys();
  },
  ownKeys: function (oTarget, sKey) {
    return oTarget.keys();
  },
  has: function (oTarget, sKey) {
    return sKey in oTarget || oTarget.hasItem(sKey);
  },
  defineProperty: function (oTarget, sKey, oDesc) {
    if (oDesc && 'value' in oDesc) { oTarget.setItem(sKey, oDesc.value); }
    return oTarget;
  },
  getOwnPropertyDescriptor: function (oTarget, sKey) {
    var vValue = oTarget.getItem(sKey);
    return vValue ? {
      value: vValue,
      writable: true,
      enumerable: true,
      configurable: false
    } : undefined;
  },
});

/* Cookies test */

console.log(docCookies.my_cookie1 = 'First value');
console.log(docCookies.getItem('my_cookie1'));

docCookies.setItem('my_cookie1', 'Changed value');
console.log(docCookies.my_cookie1);

Specyfikacje

Specification Status Comment
ECMAScript 2015 (6th Edition, ECMA-262)
The definition of 'Proxy' in that specification.
Standard Initial definition.
ECMAScript 2016 (ECMA-262)
The definition of 'Proxy' in that specification.
Standard  
ECMAScript 2017 (ECMA-262)
The definition of 'Proxy' in that specification.
Standard  
ECMAScript Latest Draft (ECMA-262)
The definition of 'Proxy' in that specification.
Draft  

Kompatybilność przeglądarek

FeatureChromeEdgeFirefoxInternet ExplorerOperaSafari
Basic support491218 No3610
revocable Yes Yes34 No Yes10
handler.apply491218 No3610
handler.construct491218 No3610
handler.defineProperty491218 No3610
handler.deleteProperty491218 No3610
handler.enumerate No No37 — 47 No No No
handler.get491218 No3610
handler.getOwnPropertyDescriptor491218 No3610
handler.getPrototypeOf No No49 No No No
handler.has491218 No3610
handler.isExtensible ? ?31 No ? ?
handler.ownKeys4912181 No3610
handler.preventExtensions491222 No3610
handler.set491218 No3610
handler.setPrototypeOf ? ?49 No ? ?
FeatureAndroid webviewChrome for AndroidEdge mobileFirefox for AndroidOpera AndroidiOS SafariSamsung Internet
Basic support4949 Yes1836105.0
revocable Yes Yes Yes34 Yes10 Yes
handler.apply4949 Yes1836105.0
handler.construct4949 Yes1836105.0
handler.defineProperty4949 Yes1836105.0
handler.deleteProperty4949 Yes1836105.0
handler.enumerate No No No37 — 47 No No No
handler.get4949 Yes1836105.0
handler.getOwnPropertyDescriptor4949 Yes1836105.0
handler.getPrototypeOf No No No49 No No No
handler.has4949 Yes1836105.0
handler.isExtensible ? ? ?31 ? ? ?
handler.ownKeys4949 Yes18136105.0
handler.preventExtensions4949 Yes2236105.0
handler.set4949 Yes1836105.0
handler.setPrototypeOf ? ? ?49 ? ? ?

1. In Firefox 42, the implementation got updated to reflect the final ES2015 specification: The result is now checked if it is an array and if the array elements are either of type string or of type symbol. Enumerating duplicate own property names is not a failure anymore.

Zobacz również

Licensing note

Some content (text, examples) in this page has been copied or adapted from the ECMAScript wiki which content is licensed CC 2.0 BY-NC-SA.

Autorzy i etykiety dokumentu

Autorzy tej strony: 0ctothorp, andrzejkrecicki
Ostatnia aktualizacja: 0ctothorp,