Czy można zaimplementować dynamiczne metody pobierające / ustawiające w JavaScript?

138

Zdaję sobie sprawę, jak tworzyć metody pobierające i ustawiające dla właściwości, których nazwy już znamy, robiąc coś takiego:

// A trivial example:
function MyObject(val){
    this.count = 0;
    this.value = val;
}
MyObject.prototype = {
    get value(){
        return this.count < 2 ? "Go away" : this._value;
    },
    set value(val){
        this._value = val + (++this.count);
    }
};
var a = new MyObject('foo');

alert(a.value); // --> "Go away"
a.value = 'bar';
alert(a.value); // --> "bar2"

Teraz moje pytanie brzmi: czy jest możliwe zdefiniowanie czegoś w rodzaju metod pobierających i ustawiających typu catch-all, takich jak te? To znaczy, utwórz metody pobierające i ustawiające dla dowolnej nazwy właściwości, która nie jest jeszcze zdefiniowana.

Ta koncepcja jest możliwa w PHP przy użyciu metod magicznych __get()i __set()( informacje na ich temat można znaleźć w dokumentacji PHP ), więc naprawdę pytam, czy istnieje odpowiednik w języku JavaScript?

Nie trzeba dodawać, że najlepiej byłoby rozwiązanie, które jest kompatybilne z różnymi przeglądarkami.

daiscog
źródło
Udało mi się to zrobić, zobacz moją odpowiedź tutaj .

Odpowiedzi:

225

Aktualizacja z 2013 i 2015 r. (Poniżej przedstawiono oryginalną odpowiedź z 2011 r.) :

Zmieniło się to od specyfikacji ES2015 (znanej również jako „ES6”): JavaScript ma teraz serwery proxy . Serwery proxy umożliwiają tworzenie obiektów, które są prawdziwymi proxy dla innych obiektów (fasady na). Oto prosty przykład, który zamienia wszystkie wartości właściwości, które są ciągami, na wszystkie kapsle podczas pobierania:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let original = {
    "foo": "bar"
};
let proxy = new Proxy(original, {
    get(target, name, receiver) {
        let rv = Reflect.get(target, name, receiver);
        if (typeof rv === "string") {
            rv = rv.toUpperCase();
        }
        return rv;
      }
});
console.log(`original.foo = ${original.foo}`); // "original.foo = bar"
console.log(`proxy.foo = ${proxy.foo}`);       // "proxy.foo = BAR"

Operacje, których nie zastępujesz, mają swoje domyślne zachowanie. W powyższym, wszystko, co nadpisujemy, to get, ale istnieje cała lista operacji, do których można się podłączyć.

Na getliście argumentów funkcji obsługi:

  • targetto obiekt, który ma zostać przekazany ( originalw naszym przypadku).
  • name jest (oczywiście) nazwą pobieranej właściwości, która zwykle jest ciągiem znaków, ale może być również symbolem.
  • receiverjest obiektem, który powinien być używany tak jak thisw funkcji pobierającej, jeśli właściwość jest akcesorem, a nie właściwością danych. W normalnym przypadku jest to proxy lub coś, co po nim dziedziczy, ale może to być wszystko, ponieważ pułapka może zostać uruchomiona przez Reflect.get.

Pozwala to na utworzenie obiektu z wybraną funkcją pobierania i ustawiania catch-all:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let obj = new Proxy({}, {
    get(target, name, receiver) {
        if (!Reflect.has(target, name)) {
            console.log("Getting non-existent property '" + name + "'");
            return undefined;
        }
        return Reflect.get(target, name, receiver);
    },
    set(target, name, value, receiver) {
        if (!Reflect.has(target, name)) {
            console.log(`Setting non-existent property '${name}', initial value: ${value}`);
        }
        return Reflect.set(target, name, value, receiver);
    }
});

console.log(`[before] obj.foo = ${obj.foo}`);
obj.foo = "bar";
console.log(`[after] obj.foo = ${obj.foo}`);

Wynik powyższego to:

Pobieranie nieistniejącej właściwości „foo”
[przed] obj.foo = undefined
Ustawienie nieistniejącej właściwości „foo”, wartość początkowa: bar
[po] obj.foo = bar

Zwróć uwagę, jak otrzymujemy komunikat „nieistniejący”, kiedy próbujemy odzyskać, foogdy jeszcze nie istnieje, i ponownie, kiedy go tworzymy, ale nie później.


Odpowiedź z 2011 r. (Patrz powyżej dla aktualizacji z 2013 i 2015 r.) :

Nie, JavaScript nie ma właściwości typu catch-all. Składnia akcesora, której używasz, jest omówiona w sekcji 11.1.5 specyfikacji i nie oferuje żadnych symboli wieloznacznych ani czegoś podobnego.

Możesz oczywiście zaimplementować funkcję, aby to zrobić, ale przypuszczam, że prawdopodobnie nie chcesz używać f = obj.prop("foo");zamiast f = obj.foo;i obj.prop("foo", value);zamiast obj.foo = value;(co byłoby konieczne, aby funkcja obsługiwała nieznane właściwości).

FWIW, funkcja getter (nie zawracałem sobie głowy logiką ustawiającą) wyglądałaby mniej więcej tak:

MyObject.prototype.prop = function(propName) {
    if (propName in this) {
        // This object or its prototype already has this property,
        // return the existing value.
        return this[propName];
    }

    // ...Catch-all, deal with undefined property here...
};

Ale znowu nie mogę sobie wyobrazić, że naprawdę chciałbyś to zrobić, ponieważ zmienia to sposób korzystania z obiektu.

TJ Crowder
źródło
1
Jest to alternatywa dla Proxy: Object.defineProperty(). Szczegóły umieściłem w mojej nowej odpowiedzi .
Andrew,
@Andrew - Obawiam się, że źle odczytałeś pytanie, zobacz mój komentarz do Twojej odpowiedzi.
TJ Crowder
Uwaga: serwery proxy nie są natywnie dostępne w IE11 (od 2020 r. Maksimum to 5 lat więcej w przypadku niektórych firm, które go obsługują). Może istnieć polyfill + babel. Lub możesz wrócić do wersji ES5 za pomocą Object.defineProperty
TamusJRoyce
4

Oryginalnym podejściem do tego problemu może być:

var obj = {
  emptyValue: null,
  get: function(prop){
    if(typeof this[prop] == "undefined")
        return this.emptyValue;
    else
        return this[prop];
  },
  set: function(prop,value){
    this[prop] = value;
  }
}

Aby z niego skorzystać, właściwości należy przekazać jako ciągi. Oto przykład tego, jak to działa:

//To set a property
obj.set('myProperty','myValue');

//To get a property
var myVar = obj.get('myProperty');

Edycja: Ulepszone, bardziej zorientowane obiektowo podejście oparte na tym, co zaproponowałem, jest następujące:

function MyObject() {
    var emptyValue = null;
    var obj = {};
    this.get = function(prop){
        return (typeof obj[prop] == "undefined") ? emptyValue : obj[prop];
    };
    this.set = function(prop,value){
        obj[prop] = value;
    };
}

var newObj = new MyObject();
newObj.set('myProperty','MyValue');
alert(newObj.get('myProperty'));

Możesz zobaczyć, jak działa tutaj .

clami219
źródło
To nie działa. Nie można zdefiniować metody pobierającej bez określenia nazwy właściwości.
John Kurlak
@JohnKurlak Sprawdź to jsFiddle: jsfiddle.net/oga7ne4x To działa. Musisz tylko przekazać nazwy właściwości jako ciągi.
clami219
3
Ach, dzięki za wyjaśnienie. Myślałem, że próbujesz użyć funkcji języka get () / set (), zamiast pisać własne get () / set (). Jednak nadal nie podoba mi się to rozwiązanie, ponieważ tak naprawdę nie rozwiązuje pierwotnego problemu.
John Kurlak
@JohnKurlak Cóż, napisałem, że to oryginalne podejście. Zapewnia inny sposób rozwiązania problemu, nawet jeśli nie rozwiązuje problemu, w którym masz istniejący kod, który wykorzystuje bardziej tradycyjne podejście. Ale dobrze, jeśli zaczynasz od zera. Z pewnością nie zasługuje na złą opinię ...
clami219
@JohnKurlak Sprawdź, czy teraz wygląda lepiej! :)
clami219
-2

Przedmowa:

Odpowiedź TJ Crowdera wspomina o Proxy, które będzie potrzebne dla metody pobierającej / ustawiającej typu catch-all dla właściwości, które nie istnieją, o co prosił OP. W zależności od tego, jakie zachowanie jest faktycznie pożądane w przypadku dynamicznych metod pobierających / ustawiających, a Proxymoże nie być konieczne; lub, potencjalnie, możesz chcieć użyć kombinacji a Proxyz tym, co pokażę poniżej.

(PS ProxyNiedawno eksperymentowałem z Firefoksem w systemie Linux i stwierdziłem, że jest on bardzo wydajny, ale także nieco zagmatwany / trudny w obsłudze i poprawny. Co ważniejsze, odkryłem również, że jest dość powolny (przynajmniej w w odniesieniu do tego, jak obecnie wygląda zoptymalizowany JavaScript) - mówię w dziedzinie dziesięciokrotności wolniej).


Aby konkretnie zaimplementować dynamicznie tworzone metody pobierające i ustawiające, możesz użyć Object.defineProperty()lub Object.defineProperties(). Jest to również dość szybkie.

Istota jest taka, że ​​możesz zdefiniować metodę pobierającą i / lub ustawiającą na obiekcie w ten sposób:

let obj = {};
let val = 0;
Object.defineProperty(obj, 'prop', { //<- This object is called a "property descriptor".
  //Alternatively, use: `get() {}`
  get: function() {
    return val;
  },
  //Alternatively, use: `set(newValue) {}`
  set: function(newValue) {
    val = newValue;
  }
});

//Calls the getter function.
console.log(obj.prop);
let copy = obj.prop;
//Etc.

//Calls the setter function.
obj.prop = 10;
++obj.prop;
//Etc.

Kilka rzeczy, na które należy zwrócić uwagę:

  • Nie można używać valuewłaściwości w deskryptorze właściwości ( nie pokazano powyżej) jednocześnie z geti / lub set; z dokumentów:

    Deskryptory właściwości występujące w obiektach występują w dwóch głównych odmianach: deskryptory danych i deskryptory akcesorów. Deskryptor danych jest właściwością, która ma wartość, która może lub nie może być zapisywalny. Deskryptor dostępowe jest właściwością opisanego parą gettera-nastawczym funkcji. Deskryptor musi być jednym z tych dwóch smaków; nie może być jednym i drugim.

  • W związku z tym zauważysz, że utworzyłem plik val własności zewnątrz w Object.defineProperty()deskryptorze połączenia / nieruchomości. To jest standardowe zachowanie.
  • Jak na błąd tutaj , nie ustawiajwritable się truew deskryptorze nieruchomości, jeśli używasz getlub set.
  • Może warto rozważyć ustawienie configurablea enumerablejednak, w zależności od tego, co jesteś po; z dokumentów:

    konfigurowalne

    • true wtedy i tylko wtedy, gdy typ tego deskryptora właściwości można zmienić i jeśli właściwość może zostać usunięta z odpowiedniego obiektu.

    • Domyślnie false.


    niezliczone

    • true wtedy i tylko wtedy, gdy ta właściwość pojawia się podczas wyliczania właściwości odpowiedniego obiektu.

    • Domyślnie false.


W tej notatce mogą one również być interesujące:

Andrzej
źródło
2
Obawiam się, że źle odczytałeś pytanie. OP specjalnie poprosił o złapanie wszystkich takich jak PHP __geti__set . definePropertynie obsługuje tej sprawy. Z pytania: „To znaczy, utwórz metody pobierające i ustawiające dla dowolnej nazwy właściwości, która nie jest jeszcze zdefiniowana”. (ich podkreślenie). definePropertyokreśla właściwości z wyprzedzeniem. Jedynym sposobem na zrobienie tego, o co prosił OP, jest proxy.
TJ Crowder
@TJCrowder Widzę. Masz rację techniczną, chociaż pytanie nie było zbyt jasne. Odpowiednio dostosowałem swoją odpowiedź. Ponadto niektórzy mogą chcieć kombinacji naszych odpowiedzi (ja osobiście tak robię).
Andrew,
@Andrew, kiedy zadawałem to pytanie w 2011 roku, przypadkiem użycia, o którym myślałem, była biblioteka, która może zwrócić obiekt, do którego użytkownik mógłby wywołać w obj.whateverPropertytaki sposób, że biblioteka może przechwycić to za pomocą ogólnego gettera i otrzymać nazwę właściwości użytkownik próbował uzyskać dostęp. Stąd wymóg dotyczący „przechwytujących wszystko i ustawiających”.
daiscog
-6
var x={}
var propName = 'value' 
var get = Function("return this['" + propName + "']")
var set = Function("newValue", "this['" + propName + "'] = newValue")
var handler = { 'get': get, 'set': set, enumerable: true, configurable: true }
Object.defineProperty(x, propName, handler)

to działa dla mnie

Bruno
źródło
13
Używanie w Function()ten sposób jest jak używanie eval. Po prostu wstaw bezpośrednio funkcje jako parametry defineProperty. Lub, jeśli z jakiegoś powodu upierają się do dynamicznego tworzenia geti set, a następnie użyć funkcji wyższego rzędu, który tworzy funkcję i zwraca go, jakvar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l