JavaScript und Schutz vor unerwünschtem Zugriff auf Objekte

Der genaue Hintergrund meiner Fragestellung führt jetzt zu weit, aber letztlich bin ich auf diesen Artikel gestoßen. Nehmen wir einmal an, dass ich eine JavaScript Klasse aus einer Bibliothek in meinem Programm unverändert verwenden möchte. Im Beispiel könnte das etwa so aussehen:

var SomeClass = function () {
    function SomeClass(value) {
        this.value = value;
    };

    SomeClass.prototype.getValue = function () {
        return this.value;
    };

    SomeClass.prototype.setValue = function (newValue) {
        this.value = newValue;
    };

    return SomeClass;
}();

Das Programm selbst bietet eine Erweiterungsschnittstelle an, über die ein Anwender sich in bestimmte Abläufe einklinken kann. Allerdings soll er im Beispiel nur lesend auf den in einer Instanz gespeicherten Klasse zugreifen können (getValue), Änderungen (setValue, value) sind alleine meinem Programm erlaubt – gerade ein direkter Zugriff auf Eigenschaften kann natürlich einiges durcheinanderwerfen.

Der oben erwähnte Artikel erklärt sehr schön, welche Alternativen man hat, um tatsächlich das gewünschte Ergebnis zu erreichen. Am mächtigsten scheint der Einsatz von Closures (JavaScript Scopes) pro Objektinstanz zu sein. Allerdings erwähnt der Autor des Artikels dabei auch, dass es zu Geschwindigkeitseinbußen kommen wird – zudem der Artikel davon ausgeht, dass man die verwendete Bibliothek verändern kann. Hier einmal ein Alternativvorschlag respektive eine Ergänzung.

Im Endeffekt soll das Programm in der Lage sein, eine Kapselung (hier als Proxy bezeichnet) zu erzeugen, die auf keinem Weg einen Zugriff auf die Objektinstanz der Bibliothek zulässt. Die Erstellung der Kapselung könnte man sich so vorstellen – Details gleich:

// Einmalig ganz am Anfang
var someClassProxy = Context.registerProxyType(SomeClass, "getValue");

// Das wäre unser Bibliotheksobjekt, vielleicht auch nur einmalig erzeugt
var lib = new SomeClass(42);

// Hier brauchen wir irgendwo tief im Code die Kapselung
var proxy = Context.createProxy(test, someClassProxy);
try {
    extensionMethod(proxy);
} finally {
    proxy.destroy();
}

Ziel ist es, dass über die Kapselung nur die Methode getValue angeboten wird und zudem die Eigenschaften des JavaScript Objektes keine Rückschlüsse auf das Bibliotheksobjekt zulassen. Das wäre dann eine mögliche Lösung:

var Context = function () {
    function Context() {
    };

    var index = 0;
    var map = {};

    Context.createProxy = function (instance, factory) {
        if (instance === null)
            return null;
        if (instance === undefined)
            return undefined;

        var proxy = factory(index++);

        map[proxy.index] = instance;

        return proxy;
    };

    Context.registerProxyType = function () {
        var args = arguments;
        var type = args[0];

        var Proxy = function () {
            function Proxy(index) {
                this.index = index;
            };

            Proxy.prototype.destroy = function () {
                delete map[this.index];
            };

            for (var i = 1; i < args.length; i++) {
                var name = args[i];
                var fn = type.prototype[name];
                if (fn === undefined)
                    throw "no method " + name;

                Proxy.prototype[name] = function (fn) {
                    return function () {
                        var instance = map[this.index];
                        if (instance === undefined)
                            throw "proxy already disconnected";

                        return fn.apply(instance, arguments);
                    };
                }(fn);
            }

            return Proxy;
        }();

        return function (index) {
            return new Proxy(index);
        };
    };

    return Context;
}();

Mit der statischen registerProxyType Methode kann ich zu einer beliebigen Klasse einen Kapselungserzeuger (Factory) erstellen, der in der Kapselung nur die gewünschten Methoden offenlegt. In der Kapselung wird auch keine Referenz auf das Bibliotheksobjekt gespeichert, sondern nur eine laufende Nummer einer aktiven Kapselung - da könnte der Anwender in seiner Erweiterung dran herumspielen, erreicht dadurch aber maximal andere aktive Kapselungen. Durch das Closure (den Scope) ist vor allem map für den Anwender unerreichbar. createProxy erzeugt zu einem beliebigen Bibliotheksobjekt eine neue Kapselung - alternative Kapselungen ein und der selben Klasse für unterschiedliche Erweiterungsszenarien sind denkbar. Durch einen laufenden Index (ebenfalls im Closure) wird die Kapselung aktiviert, ein destroy Aufruf auf der Kapselung hebt die Aktivierung wieder auf - auch hier kann die Erweiterung pfuschen aber letztlich nur ihre eigene Aufgabe erschweren, niemals aber die Infrastruktur der Bibliotheksobjekte antasten.

Ok, das ist als Studie nur ein erster Gedanke. Sollte ich es denn wirklich einmal brauchen, kann man sicher noch vieles verfeinern - e.g. muss destroy wirklich eine Methode der Kapselung sein? Aber ich hoffe, dass auch in diesem groben Ansatz zumindest die Idee insbesondere als Erweiterung des eingangs erwähnten Artikels verständlich wird.

Happy Coding

Jochen