Kategorien
JavaScript & jQuery Programmierung

JavaScript: Mit Closure Space private Attribute erstellen

Vor einiger Zeit habe ich einen Angular Cloud Data Connector entwickelt, der es Angular-Entwicklern erlaubt, mithilfe von Webstandards, wie indexierten DB, Cloud-Daten zu nutzen, und zwar konkret Azure Mobile Service. Ich wollte Java-Entwicklern die Möglichkeit geben, private Attribute (private members) in ein Objekt einzubetten. Die Technik, die ich für diesen speziellen Fall benutze, nenne ich “closure space”. In diesem Tutorial zeige ich, wie man das für verschiedene Projekte nutzen kann und inwiefern Performance und Speicher der gängigen Browser davon beeinflusst werden.

Aber bevor wir richtig anfangen, werde ich noch kurz erklären, wozu man private Attribute braucht und auf welche Weise man sie auch “simulieren” kann.

Wozu brauchen wir private Attribute?

Wenn wir ein Objekt mit JavaScript erstellen, können wir Wertattribute (value members) festlegen. Um den Lese-/Schreibzugriff darauf zu steuern, müssen wir Zugriffsberechtigte wie folgt definieren:

var entity = {};

entity._property = "hello world";
Object.defineProperty(entity, "property", {
    get: function () { return this._property; },
    set: function (value) {
        this._property = value;
    },
    enumerable: true,
    configurable: true
});

Auf diese Weise haben wir die volle Kontrolle über den Lese-/Schreibzugriff. Problematisch ist jedoch, dass das _property Attribut noch zugänglich ist und direkt verändert werden kann.

Genau deshalb brauchen wir einen solideren Weg, um private Attribute festzulegen, die nur mithilfe der Funktionen des Objekts zugänglich sind.

Die Nutzung von Closure Space

Die Lösung heißt: Closure Space. Dieser Speicherraum wird vom Browser bereitgestellt, und zwar jedes Mal, wenn eine innere Funktion Zugang zu Variablen aus dem Bereich einer äußeren Funktion besitzt. Das ist manchmal eine ziemlich knifflige Angelegenheit, aber für unsere Zwecke ist die Lösung perfekt.

Um diese Funktion zu nutzen, ändern wir also den vorherigen Code in diesen hier:

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {};

var myVar = "hello world";
createProperty(entity, "property", myVar);

In diesem Beispiel hat die Funktion createProperty die Variable currentValue, welche von Get/Set-Funktionen erkannt wird. Diese Variable wird im Closure Space der Get- und Set-Funktionen gespeichert. Ausschließlich diese beiden Funktionen können nun die Variable currentValue sehen und aktualisieren! Mission erfüllt!

Fast zumindest. Der einzige Vorbehalt: Der Quellenwert (myVar) ist nach wie vor zugänglich. Deshalb zeige ich hier eine Version mit noch besserem Schutz:

var createProperty = function (obj, prop) {
    var currentValue = obj[prop];
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {
    property: "hello world"
};

createProperty(entity, "property");

Mit dieser Methode wird sogar der Quellenwert unsichtbar. Nun haben wir die Aufgabe aber wirklich erfüllt!

Ein Blick auf die Performance

Jetzt schauen wir mal wie sich das Ganze auf die Performance auswirkt.

Ganz klar, Closure Space oder sogar Eigenschaften sind langsamer und aufwändiger als reine Variablen. Deshalb will ich mich in diesem Artikel auf den Unterschied zwischen dem üblichen Weg und der Closure Space-Technik konzentrieren.

Um nachzuweisen, dass die Nutzung von Closure Space verhältnismäßig effizienter ist als der herkömmliche Weg, habe ich diesen kleinen Benchmark-Code geschrieben:

<!DOCTYPE html>
  <html xmlns="http://www.w3.org/1999/xhtml">
  <head>
  <title></title>
  </head>
  <style>
  html {
  font-family: "Helvetica Neue", Helvetica;
  }
  </style>
  <body>
  <div id="results">Computing...</div>
  <script>
  var results = document.getElementById("results");
  var sampleSize = 1000000;
  var opCounts = 1000000;
  
 var entities = [];

 setTimeout(function () {
  // Creating entities
  for (var index = 0; index < sampleSize; index++) {
  entities.push({
  property: "hello world (" + index + ")"
  });
  }

 // Random reads
  var start = new Date().getTime();
  for (index = 0; index < opCounts; index++) {
  var position = Math.floor(Math.random() * entities.length);
  var temp = entities[position].property;
  }
  var end = new Date().getTime();

 results.innerHTML = "<strong>Results:</strong><br>Using member access: <strong>" + (end - start) + "</strong> ms";
  }, 0);

 setTimeout(function () {
  // Closure space =======================================
  var createProperty = function (obj, prop, currentValue) {
  Object.defineProperty(obj, prop, {
  get: function () { return currentValue; },
  set: function (value) {
  currentValue = value;
  },
  enumerable: true,
  configurable: true
  });
  }
  // Adding property and using closure space to save private value
  for (var index = 0; index < sampleSize; index++) {
  var entity = entities[index];

 var currentValue = entity.property;
  createProperty(entity, "property", currentValue);
  }

 // Random reads
  var start = new Date().getTime();
  for (index = 0; index < opCounts; index++) {
  var position = Math.floor(Math.random() * entities.length);
  var temp = entities[position].property;
  }
  var end = new Date().getTime();

 results.innerHTML += "<br>Using closure space: <strong>" + (end - start) + "</strong> ms";
  }, 0);

 setTimeout(function () {
  // Using local member =======================================
  // Adding property and using local member to save private value
  for (var index = 0; index < sampleSize; index++) {
  var entity = entities[index];

 entity._property = entity.property;
  Object.defineProperty(entity, "property", {
  get: function () { return this._property; },
  set: function (value) {
  this._property = value;
  },
  enumerable: true,
  configurable: true
  });
  }

 // Random reads
  var start = new Date().getTime();
  for (index = 0; index < opCounts; index++) {
  var position = Math.floor(Math.random() * entities.length);
  var temp = entities[position].property;
  }
  var end = new Date().getTime();

 results.innerHTML += "<br>Using local member: <strong>" + (end - start) + "</strong> ms";
  }, 0);

 </script>
  </body>
  </html>

Ich erstelle eine Million Objekte, jeweils mit einem Eigenschaftsattribut (property member). Dann lasse ich drei Tests laufen:

  • Führe 1 Million Direktzugriffe auf die Eigenschaft aus
  • Führe 1 Million Direktzugriffe auf die “Closure Space” Version aus
  • Führe 1 Million Direktzugriffe auf die reguläre Get/Set Version aus

Hier eine Tabelle und eine Grafik von den Ergebnissen:


Deutlich zu sehen ist, dass die Closure Space-Version in allen Fällen schneller arbeitet als die reguläre Version. Abhängig vom Browser kann die Optimierung sehr eindrucksvoll ausfallen.

Die Performance von Chrome war weniger gut, als ich sie erwartet hatte. Um einen Bug auszuschließen, fragte ich daher bei Google nach, was die Ursache dafür sein könnte. Nebenbei bemerkt: Wer ausprobieren will, wie dieser Test für Microsoft Edge ausgeht – der neue Browser von Microsoft, der in Windows 10 enthalten ist – der kann ihn hier herunterladen.

Wie auch immer, wenn wir genau hinschauen, sehen wir, dass die Nutzung von Closure Space oder sogar einer Eigenschaft zehnmal langsamer funktioniert, als der direkte Zugriff auf ein Attribut. Das nur als Warnung, das Ganze mit Bedacht anzuwenden.

Speicherbedarf

Wir sollten auch prüfen, ob diese Technik nicht zu viel Speicherkapazität bindet. Um also auch dafür einen Benchmark-Test durchzuführen, habe ich diese drei Stückchen Code geschrieben:

Referenz Code

var sampleSize = 1000000;

var entities = [];

// Creating entities
for (var index = 0; index < sampleSize; index++) {
    entities.push({
        property: "hello world (" + index + ")"
    });
}

Der übliche Weg

var sampleSize = 1000000;

var entities = [];

// Adding property and using local member to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    entity._property = "hello world (" + index + ")";
    Object.defineProperty(entity, "property", {
        get: function () { return this._property; },
        set: function (value) {
            this._property = value;
        },
        enumerable: true,
        configurable: true
    });

    entities.push(entity);
}

Closure Space-Version

var sampleSize = 1000000;

var entities = [];

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

// Adding property and using closure space to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    var currentValue = "hello world (" + index + ")";
    createProperty(entity, "property", currentValue);

    entities.push(entity);

Dann ließ ich alle drei Codes durchlaufen und startete den integrierten Memory Profiler (als Beispiel nutzte ich hier die F12 Tools):

Auf meinem Computer erhielt ich daraufhin folgende Ergebnisse:

Im Vergleich von Closure Space und dem üblichen Weg hatte nur Chrome leicht bessere Werte für die Closure Space-Version. IE11 und Firefox brauchten ein bisschen mehr Speicher, aber generell waren alle Browser relativ vergleichbar. Als User wird man da kaum einen Unterschied bemerken.

Mehr Tipps, Tricks und Hilfen rund um JavaScript

Microsoft hält eine ganze Reihe kostenloser Lehrangebote zu zahlreichen Open Source JavaScript-Themen bereit – und seit der Veröffentlichung von Microsoft Edge tun wir in diesem Bereich noch sehr viel mehr. Auch von mir gibt es Tutorials:

Außerdem haben wir noch die Schulungsreihe unseres Teams:

Praktisch sind zudem die folgenden kostenlosen Tools: Visual Studio Community, Azure Testversion und Cross Browser Testing Tools für Mac, Linux oder Windows.

Fazit

Wie wir gesehen haben, können wir mit Closure Space sehr gut wirklich private Daten erstellen. Das kostet uns ein bisschen Speicherplatz, aber meiner Meinung nach ist das vertretbar (und bringt uns schließlich gleichzeitig eine bessere Performance, als wenn wir den üblichen Weg nutzen würden).

Wer Fragen zu diesem Artikel hat, kann mich gern auf Twitter kontaktieren: @deltakosh

Und wer es gleich selbst versuchen will, kann den gesamten verwendeten Code hier finden. Es gibt außerdem eine gute Kurzeinweisung in die Azure Mobile Services, und zwar hier.

Dieser Artikel ist Teil der Web-Dev Tech-Series von Microsoft. Wir freuen uns Microsoft Edge (früher Project Spartan genannt) und seine neue Rendering Engine mit euch zu teilen. Kostenlose Virtual Machines oder Remote Testings für Mac, iOS, Android oder Windows gibt es hier: @ dev.modern.IE.

(dpe)

Von David Catuhe

David Catuhe ist Principal Program Manager bei Microsoft mit einem Fokus auf Web-Entwicklung. Er ist der Autor des babylon.js Frameworks, mit dem man 3D Spiele mit HTML5 and WebGL erstellen kann.

Lesen Sie seinen Blog auf MSDN oder folgen Sie ihm auf Twitter: @deltakosh

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.