Ist es möglich, dynamische Getter / Setter in JavaScript zu implementieren?

131

Ich weiß, wie man Getter und Setter für Eigenschaften erstellt, deren Namen man bereits kennt, indem man so etwas macht:

// 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"

Meine Frage ist nun, ob es möglich ist, solche Catch-All-Getter und Setter wie diese zu definieren. Erstellen Sie also Getter und Setter für jeden Eigenschaftsnamen, der noch nicht definiert ist.

Das Konzept ist in PHP mit den Methoden __get()und __set()magic möglich ( Informationen hierzu finden Sie in der PHP-Dokumentation ). Ich frage mich also wirklich, ob es ein JavaScript gibt, das diesen Methoden entspricht.

Natürlich möchte ich im Idealfall eine browserübergreifende Lösung.

Daiscog
quelle
Ich habe es geschafft, siehe meine Antwort hier, wie.

Antworten:

215

Update 2013 und 2015 (siehe unten für die ursprüngliche Antwort von 2011) :

Dies wurde ab der ES2015-Spezifikation (auch bekannt als "ES6") geändert: JavaScript verfügt jetzt über Proxys . Mit Proxies können Sie Objekte erstellen, die echte Proxys für (Fassaden auf) anderen Objekten sind. Hier ist ein einfaches Beispiel, das alle Eigenschaftswerte, die Zeichenfolgen sind, beim Abrufen in alle Obergrenzen umwandelt:

"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"

Vorgänge, die Sie nicht überschreiben, haben ihr Standardverhalten. Oben überschreiben wir nur get, aber es gibt eine ganze Liste von Operationen, an die Sie sich anschließen können.

In der getArgumentliste der Handlerfunktion:

  • targetist das Objekt, das vertreten wird ( originalin unserem Fall).
  • name ist (natürlich) der Name der Eigenschaft, die abgerufen wird. Dies ist normalerweise eine Zeichenfolge, kann aber auch ein Symbol sein.
  • receiverist das Objekt, das wie thisin der Getter-Funktion verwendet werden soll, wenn die Eigenschaft eher ein Accessor als eine Dateneigenschaft ist. Im Normalfall ist dies der Proxy oder etwas, das von ihm erbt, aber es kann alles sein, da die Falle durch ausgelöst werden kann Reflect.get.

Auf diese Weise können Sie ein Objekt mit der gewünschten Catch-All-Getter- und Setter-Funktion erstellen:

"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}`);

Die Ausgabe der oben genannten ist:

Nicht existierendes Eigentum 'foo' bekommen
[vor] obj.foo = undefiniert
Festlegen der nicht vorhandenen Eigenschaft 'foo', Anfangswert: bar
[nach] obj.foo = bar

Beachten Sie, wie wir die Nachricht "nicht vorhanden" erhalten, wenn wir versuchen, sie abzurufen, foowenn sie noch nicht vorhanden ist, und erneut, wenn wir sie erstellen, jedoch nicht danach.


Antwort von 2011 (siehe oben für Updates 2013 und 2015) :

Nein, JavaScript verfügt nicht über eine Catch-All-Eigenschaftsfunktion. Die von Ihnen verwendete Accessor-Syntax wird in Abschnitt 11.1.5 der Spezifikation behandelt und bietet keinen Platzhalter oder ähnliches.

Sie könnten natürlich eine Funktion implementieren, um dies zu tun, aber ich vermute, Sie möchten wahrscheinlich nicht f = obj.prop("foo");eher als f = obj.foo;und obj.prop("foo", value);statt verwenden obj.foo = value;(was notwendig wäre, damit die Funktion unbekannte Eigenschaften verarbeitet).

FWIW, die Getter-Funktion (ich habe mich nicht mit der Setter-Logik beschäftigt) würde ungefähr so ​​aussehen:

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...
};

Aber ich kann mir auch nicht vorstellen, dass Sie das wirklich wollen, weil es die Art und Weise ändert, wie Sie das Objekt verwenden.

TJ Crowder
quelle
1
Es gibt eine Alternative zu Proxy: Object.defineProperty(). Ich habe die Details in meine neue Antwort eingefügt .
Andrew
@ Andrew - Ich fürchte, Sie haben die Frage falsch verstanden. Siehe meinen Kommentar zu Ihrer Antwort.
TJ Crowder
4

Das Folgende könnte ein origineller Ansatz für dieses Problem sein:

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;
  }
}

Um es zu verwenden, sollten die Eigenschaften als Zeichenfolgen übergeben werden. Hier ist ein Beispiel, wie es funktioniert:

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

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

Bearbeiten: Ein verbesserter, objektorientierterer Ansatz, der auf dem basiert, was ich vorgeschlagen habe, ist der folgende:

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'));

Sie können es hier sehen .

clami219
quelle
Das funktioniert nicht. Sie können keinen Getter definieren, ohne den Namen der Eigenschaft anzugeben.
John Kurlak
@ JohnKurlak Überprüfen Sie diese jsFiddle: jsfiddle.net/oga7ne4x Es funktioniert. Sie müssen nur die Eigenschaftsnamen als Zeichenfolgen übergeben.
Clami219
3
Ah, danke für die Klarstellung. Ich dachte, Sie wollten die Sprachfunktion get () / set () verwenden und nicht Ihr eigenes get () / set () schreiben. Ich mag diese Lösung immer noch nicht, weil sie das ursprüngliche Problem nicht wirklich löst.
John Kurlak
@ JohnKurlak Nun, ich habe geschrieben, es ist ein origineller Ansatz. Es bietet eine andere Möglichkeit, das Problem zu lösen, auch wenn es das Problem nicht löst, wenn Sie einen vorhandenen Code haben, der einen traditionelleren Ansatz verwendet. Aber es ist gut, wenn Sie von vorne anfangen. Sicherlich keine Abwertung wert ...
clami219
@ JohnKurlak Sehen Sie, ob es jetzt besser aussieht! :)
clami219
0

Vorwort:

In der Antwort von TJ Crowder wird a erwähnt Proxy, das für einen Catch-All-Getter / Setter für Eigenschaften benötigt wird, die nicht existieren, wie vom OP gefordert. Abhängig davon, welches Verhalten bei dynamischen Gettern / Setzern tatsächlich gewünscht wird, ist ein Proxymöglicherweise jedoch nicht erforderlich. oder möglicherweise möchten Sie eine Kombination von a Proxymit dem verwenden, was ich Ihnen unten zeigen werde.

(PS Ich habe Proxyin letzter Zeit gründlich mit Firefox unter Linux experimentiert und festgestellt, dass es sehr leistungsfähig ist, aber auch etwas verwirrend / schwierig zu bearbeiten und richtig zu machen. Noch wichtiger ist, dass es auch ziemlich langsam ist (zumindest in) Beziehung dazu, wie optimiert JavaScript heutzutage ist) - Ich spreche im Bereich von Deka-Vielfachen langsamer.)


Um dynamisch erstellte Getter und Setter spezifisch zu implementieren, können Sie Object.defineProperty()oder verwendenObject.defineProperties() . Das geht auch ganz schnell.

Das Wesentliche ist, dass Sie einen Getter und / oder Setter für ein Objekt wie folgt definieren können:

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.

Hier sind einige Dinge zu beachten:

  • Sie können die valueEigenschaft im Eigenschaftsdeskriptor (oben nicht gezeigt) nicht gleichzeitig mit getund / oder verwendenset . aus den Dokumenten:

    In Objekten vorhandene Eigenschaftsbeschreibungen gibt es in zwei Hauptvarianten: Datenbeschreibungen und Zugriffsbeschreibungen. Ein Datendeskriptor ist eine Eigenschaft mit einem Wert, der beschreibbar sein kann oder nicht. Ein Accessor-Deskriptor ist eine Eigenschaft, die durch ein Getter-Setter-Funktionspaar beschrieben wird. Ein Deskriptor muss eine dieser beiden Varianten sein. es kann nicht beides sein.

  • Daher werden Sie feststellen, dass ich eine valEigenschaft außerhalb des Object.defineProperty()Aufruf- / Eigenschaftsdeskriptors erstellt habe. Dies ist Standardverhalten.
  • Gemäß der Fehler hier , nicht setzen writableauf trueim Eigenschaftendeskriptor , wenn Sie verwenden getoder set.
  • Möglicherweise möchten Sie die Einstellung in Betracht ziehen configurableund dies enumerablehängt jedoch davon ab, wonach Sie suchen. aus den Dokumenten:

    konfigurierbar

    • true genau dann, wenn der Typ dieses Eigenschaftsdeskriptors geändert werden kann und wenn die Eigenschaft aus dem entsprechenden Objekt gelöscht werden kann.

    • Der Standardwert ist false.


    aufzählbar

    • true genau dann, wenn diese Eigenschaft bei der Aufzählung der Eigenschaften des entsprechenden Objekts angezeigt wird.

    • Der Standardwert ist false.


In diesem Sinne können diese auch von Interesse sein:

  • Object.getOwnPropertyNames(obj): ruft alle Eigenschaften eines Objekts ab, auch nicht aufzählbare (AFAIK ist dies der einzige Weg, dies zu tun!).
  • Object.getOwnPropertyDescriptor(obj, prop): Ruft den Eigenschaftsdeskriptor eines Objekts ab, das Objekt, an das Object.defineProperty()oben übergeben wurde.
  • obj.propertyIsEnumerable(prop);: Rufen Sie für eine einzelne Eigenschaft einer bestimmten Objektinstanz diese Funktion für die Objektinstanz auf, um festzustellen, ob die bestimmte Eigenschaft aufzählbar ist oder nicht.
Andrew
quelle
2
Ich fürchte, Sie haben die Frage falsch verstanden. Das OP bat speziell um Fang alle wie PHPs __getund__set . definePropertybehandelt diesen Fall nicht. Aus der Frage: "Erstellen Sie Getter und Setter für jeden Eigenschaftsnamen, der noch nicht definiert ist." (ihre Betonung). definePropertydefiniert Eigenschaften im Voraus. Die einzige Möglichkeit, das zu tun, wonach das OP gefragt hat, ist ein Proxy.
TJ Crowder
@TJCrowder Ich verstehe. Sie sind technisch korrekt, obwohl die Frage nicht sehr klar war. Ich habe meine Antwort entsprechend angepasst. Einige möchten möglicherweise auch eine Kombination unserer Antworten (ich persönlich).
Andrew
@ Andrew, als ich diese Frage 2011 stellte, war der Anwendungsfall, den ich im Sinn hatte, eine Bibliothek, die ein Objekt zurückgeben kann, für das der Benutzer aufrufen kann obj.whateverProperty, sodass die Bibliothek dies mit einem generischen Getter abfangen und den Eigenschaftsnamen the erhalten kann Benutzer versucht zuzugreifen. Daher die Forderung nach "Allround-Getter und Setter".
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)

das funktioniert bei mir

Bruno
quelle
13
So zu Function()benutzen ist wie zu benutzen eval. Setzen Sie einfach direkt die Funktionen als Parameter von defineProperty. Oder, wenn Sie aus irgendeinem Grund darauf bestehen, dynamisch zu erstellen getund setdann eine Funktion höherer Ordnung zu verwenden, die die Funktion erstellt und zurückgibt, wievar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l