Použití Closure Space k vytvoření skutečných soukromých členů v JavaScriptu

Tento článek je součástí série webových vývojářů od společnosti Microsoft. Děkujeme, že podporujete partnery, kteří umožňují existenci SitePoint.

Nedávno jsem vyvinul Angular Cloud Data Connector, který umožňuje vývojářům Angular používat cloudová data, konkrétně mobilní službu Azure, pomocí webových standardů, jako je indexovaná DB. Snažil jsem se pro vývojáře JavaScriptu vytvořit způsob, jak vložit soukromé členy do objektu. Moje technika pro tento konkrétní případ je použít to, čemu říkám „uzavřený prostor“. V tomto tutoriálu se s vámi chci podělit o to, jak to používat pro vaše vlastní projekty a jak to ovlivňuje výkon a paměť u hlavních prohlížečů.

Ale než se do toho pustím, dovolte mi podělit se o to, proč možná potřebujete soukromé členy, a také alternativní způsob, jak „simulovat“ soukromé členy.

Pokud chcete diskutovat o tomto článku, můžete mi napsat ping na Twitteru.

Proč používat soukromé členy

Když vytváříte objekt pomocí JavaScriptu, můžete definovat hodnotové členy. Pokud na nich chcete řídit přístup pro čtení/zápis, potřebujete přístupové objekty, které lze definovat takto:

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

Díky tomu máte plnou kontrolu nad operacemi čtení a zápisu. Problém je v tom, že _property člen je stále přístupný a lze jej přímo upravit.

To je přesně důvod, proč potřebujete robustnější způsob, jak definovat soukromé členy, ke kterým mají přístup pouze funkce objektu.

Použití uzavíracího prostoru

Řešením je využití uzavíracího prostoru. Tento paměťový prostor pro vás vytváří prohlížeč pokaždé, když má vnitřní funkce přístup k proměnným z rozsahu vnější funkce. Někdy to může být složité, ale pro naše téma je to perfektní řešení.

Změňme tedy předchozí kód, abychom tuto funkci používali:

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

V tomto příkladu createProperty funkce má currentValue proměnná get a set funkce mohou vidět. Tato proměnná bude uložena v uzavíracím prostoru get a set funkcí. Pouze tyto dvě funkce nyní mohou zobrazit a aktualizovat currentValue variabilní! Mise splněna!

Jedinou námitku, kterou zde máme, je, že zdrojová hodnota (myVar ) je stále přístupný. Zde tedy přichází další verze pro ještě robustnější ochranu:

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

Pomocí této metody je zničena i zdrojová hodnota. Takže mise plně splněna!

Úvahy o výkonu

Pojďme se nyní podívat na výkon.

Uzavřené prostory nebo dokonce vlastnosti jsou samozřejmě pomalejší a dražší než jen obyčejná proměnná. Proto se tento článek více zaměřuje na rozdíl mezi běžným způsobem a technikou uzavření prostoru.

Abych potvrdil, že přístup k uzavření prostoru není příliš drahý ve srovnání se standardním způsobem, napsal jsem tento malý benchmark:

<!DOCTYPE html>
<html xmlns='https://www.w3.org/1999/xhtml'>
<head>
  <title>Benchmark</title>
  <style>
    html {
      font-family: 'Helvetica Neue', Helvetica;
    }
  </style>
</head>
<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>

Vytvořím jeden milion objektů, všechny s vlastností member . Pak udělám tři testy:

  • Jeden milion náhodných přístupů k nemovitosti

  • Jeden milion náhodných přístupů k verzi „uzavíracího prostoru“

  • Jeden milion náhodných přístupů k běžné verzi get/set

Zde je tabulka a graf s podrobnostmi o výsledku:

Vidíme, že verze s uzavřeným prostorem je vždy rychlejší než běžná verze a v závislosti na prohlížeči to může být opravdu působivá optimalizace.

Výkon Chrome je horší, než jsem čekal. Může se vyskytnout chyba, takže pro jistotu jsem kontaktoval tým Google, abych zjistil, co se děje. Pokud si chcete vyzkoušet, jak to funguje v Project Spartan – novém prohlížeči společnosti Microsoft, který se bude dodávat jako výchozí s Windows 10 – můžete si jej stáhnout zde.

Použití uzavřeného prostoru nebo dokonce vlastnosti může být desetkrát pomalejší než přímý přístup ke členovi. Buďte proto varováni a používejte jej moudře.

Paměťová stopa

Musíme také zkontrolovat, zda tato technika nespotřebovává příliš mnoho paměti. Pro srovnání paměti jsem napsal tyto tři malé kousky kódu:

Referenční kód

var sampleSize = 1000000;
var entities = [];

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

Normální způsob

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

Verze uzavřeného prostoru

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 &amp;amp;lt; sampleSize; index++) {
  var entity = {};
  var currentValue = 'hello world (' + index + ')';
  createProperty(entity, 'property', currentValue);

  entities.push(entity);
}

Poté jsem spustil všechny tři ukázky kódu a spustil vestavěný profilovač paměti (příklad zde pomocí nástrojů F12):

Zde jsou výsledky, které jsem získal na svém počítači:

Ve srovnání s uzavřeným prostorem a běžným způsobem má pouze Chrome o něco lepší výsledky pro verzi s uzavřeným prostorem. IE11 a Firefox využívají o něco více paměti, ale prohlížeče jsou podobné – uživatelé si pravděpodobně nevšimnou rozdílu mezi moderními prohlížeči.

Více praktických zkušeností s JavaScriptem

Možná vás to překvapí, ale Microsoft má spoustu bezplatných lekcí o mnoha tématech JavaScriptu s otevřeným zdrojovým kódem a s příchodem Project Spartan jsme na misi vytvořit mnohem více. Podívejte se na můj vlastní:

  • Úvod do WebGL 3D a HTML5 a Babylon.JS

  • Vytvoření jednostránkové aplikace s ASP.NET a AngularJS

  • Špičková grafika v HTML

Nebo výuková série našeho týmu:

  • Praktické tipy pro výkon, jak zrychlit HTML/JavaScript (sedmidílná série od responzivního designu přes neformální hry až po optimalizaci výkonu)

  • Moderní webová platforma JumpStart (základy HTML, CSS a JS)

  • Vývoj univerzální aplikace pro Windows pomocí HTML a JavaScript JumpStart (k vytvoření aplikace použijte JS, který jste již vytvořili)

A některé bezplatné nástroje:Visual Studio Community, Azure Trial a nástroje pro testování napříč prohlížeči pro Mac, Linux nebo Windows.

Závěr

Jak vidíte, vlastnosti uzavřeného prostoru mohou být skvělým způsobem, jak vytvořit skutečně soukromá data. Možná se budete muset vypořádat s malým zvýšením spotřeby paměti, ale z mého pohledu je to docela rozumné (a za tuto cenu můžete dosáhnout velkého zlepšení výkonu oproti běžnému používání).

A mimochodem, pokud si to chcete vyzkoušet na vlastní kůži, najdete všechny použité kódy zde. Zde je dobrý návod na Azure Mobile Services.

Tento článek je součástí série webových vývojářů od společnosti Microsoft. Jsme nadšeni, že se s vámi můžeme podělit o Project Spartan a jeho nové vykreslovací jádro. Získejte zdarma virtuální stroje nebo otestujte vzdáleně na svém zařízení Mac, iOS, Android nebo Windows na modern.IE.