To tłumaczenie jest niekompletne. Pomóż przetłumaczyć ten artykuł z języka angielskiego.

 
 

Domknięcia to funkcje, których funkcje wewnętrzne odwołują się do niezależnych (wolnych) zmiennych. Innymi słowy, funkcje zdeklarowane wewnątrz domknięcia 'pamiętają' środowisko, w którym zostały utworzone.

Zasięg leksykalny

Rozważ poniższy przykład:

function init() {
  var name = "Mozilla"; // name jest zmienną lokalną utworzoną przez funkcję init
  function displayName() { // displayName() jest wewnętrzną funkcją, domknięciem
    alert(name); // używa zmiennej zdeklarowanej w funkcji nadrzędnej
  }
  displayName();    
}
init();

init() tworzy zmienną lokalną name oraz funkcję displayName(). displayName() jest funkcją lokalną która została zdefiniowana wewnątrz funkcji init() i jest dostępna tylko wewnątrz tej funkcji. displayName() nie ma własnych zmiennych lokalnych. Jednakże, ponieważ wewnętrzne funkcje mają dostęp do zmiennych zdefiniowanych w funkcjach zewnętrznych, displayName() ma dostęp do zmiennej name zdeklarowanej w funkcji nadrzędnej, init().

Uruchom kod i zauważ że alert() zawarty w funkcji displayName() wyświetlił wartość ze zmiennej name, która jest zdeklarowana w funkcji nadrzędnej. Jest to przykład zasięgu leksykalnego, który opisuje jak parser rozwiązuje zmienne kiedy funkcje są zagnieżdżone. Słowo "leksykalny" odnosi się do faktu że zasięg leksykalny używa lokalizacji zdefiniowania zmiennej w kodzie źródłowym aby określić gdzie ta zmienna jest dostępna. Zagnieżdżone funkcje mają dostęp do zmiennych zdeklarowanych w ich zewnętrznym zasięgu.

Domknięcie

Teraz rozważmy następujący przykład:

function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

Jeżeli uruchomisz ten kod przekonasz się że ma takie samo działanie jak poprzedni przykład z funkcją init(): tym razem wartość tekstowa "Mozilla" zostanie wyświetlona w alercie. To, co jest inne, - i interesujące - to to, że wewnętrzna funkcja displayName() została zwrócona z nadrzędnej funkcji przed jej wykonaniem.

Na pierwszy rzut oka może się wydawać nieintuicyjne, że kod nadal pracuje. W niektórych językach programowania zmienne lokalne znajdujące się w funkcji istnieją tylko przez czas trwania tej funkcji. W momencie gdy makeFunc() zostanie wykonana możesz oczekiwa,ć że zmienna nie będzie już dostępna. Jednakże, ponieważ w naszym przypadku kod nadal pracuje, jak widać nie dotyczy to języka JavaScript.

Spowodowane jest to tym, że omawiane funkcje przybierają w Javascript formę domknięć. Domknięcie jest kombinacją funkcji i leksykalnego środowiska w którym ta funkcja została zdeklarowana. To środowisko zawiera każdą zmienną lokalną która była w zasięgu w momencie kiedy domknięcie zostało stworzone. W tym przypadku, myFunc jest referencją do instancji funkcji displayName stworzonej w momencie działania makeFunc. Instancja displayName zarządza referencją do jej leksykalnego środowiska, w którym istnieje zmienna. Dlatego, kiedy myFunc jest uruchomiona, zmienna pozostaje dostępna do użycia i "Mozilla" może być przekazane do alert.

Poniżej znajduje się znacznie bardziej interesujący przykład — funkcja makeAdder:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

W tym przykładzie, zdefiniowaliśmy funkcję makeAdder(x), która pobiera argument, x, i zwraca nową funkcję.  Zwrócona funkcja pobiera argument, y, i zwraca sume x i y.

W zasadzie makeAdder jest fabryką funkcji — wytwarza funkcje, które mogą dodawać pewną wartość do ich argumentu. W powyższym przykładzie używamy naszej fabryki funkcji do stworzenia dwóch nowych funkcji — jedna, która dodaje 5 do jej argumentu i druga, która dodaje 10.

add5 i add10 są domknięciami. Dzielą ten sam kod zawarty w funkcji makeAdder, ale przechowują różne leksykalne środowisko. W leksykalnym środowisku add5x wynosi 5, natomiast w leksykalnym środowisku add10x jest równe 10.

Praktyczne domknięcia

Domknięcia są przydatne, ponieważ pozwalają Ci powiązać część danych (środowisko leksykalne) z funkcją, która operuje na tych danych. Jest to oczywista analogia do programowania obiektowego, gdzie obiekty pozwalają nam na powiązanie części danych (właściwości obiektu) z jedną lub dwiema metodami.

W rezultacie możesz użyć dokmnięć w sytuacjach, gdzie normalnie byś użył/a obiektu z wyłącznie jedną metodą.

Potencjalne sytuacje zastosowania powyższego zachowania są szczególnie popularne w sieci. Wiele kodu, który piszemy we front-endowym Javascripcie bazuje na zdarzeniach — definiujemy jakieś zachowanie, następnie dołączamy je do wydarzenia, które jest wywoływane przez użytkownika (kliknięciem myszki lub naciśnięciem klawisza klawiatury). Nasz kod jest najczęściej dołączony jako callback (wywołanie zwrotne): pojedyncza funkcja wykonywana jako odpowiedź na wydarzenie.

Przyjmijmy przykładowo, że chcemy dodać do strony przyciski, które zmieniają wielkość tekstu. Jednym ze sposobów na osiągnięcie tego jest określenie rozmiaru czcionki font-size elementu body w pikselach, następnie ustawienie rozmiaru innych elementów na stronie (takich jak nagłówki) używając jednostki względnej em:

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

Nasze interaktywne przyciski zmiany wielkości tekstu mogą zmienić wlaściwość font-size elementu body, a inne elementy strony dostosują się dzięki zastosowaniu jednostki względnej.

Poniżej realizacja w JavaScript:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

size12size14, oraz size16 są obecnie funkcjami, które zmienią rozmiar tekstu w body do odpowiednio 12, 14 oraz 16 pixeli. Możemy dołączyć je do przycisków (w tym przypadku linków) jak ponżej:

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>

Emulowanie prywatnych metod przy użyciu domknięć

Języki takie jak Java dostarczają możliwość zadeklarowania metody jako prywatna, co oznacza, że może ona zostać wywołana wylącznie przez inne metody w tej samej klasie.

JavaScript nie zapewnia do tego wbudowanej metody, jednakże jest możliwa emulacja prywatnych metod przy użyciu domknięć. Prywatne metody nie sa wyłącznie użyteczne z racji możliwości ograniczenia dostępu do kodu: dają również świetną możliwość zarządzania Twoją globalną przestrzenią nazw (namespace) uniemożliwiając nieistotnym metodom zaśmiecenie interfejsu publicznego Twojego kodu.

Poniższy kod ukazuje, w jaki sposób można użyć domknięć do zdefiniowania publicznych funkcji, które mają dostęp do prywatnych funkcji i zmiennych. Używanie dokmnięć w taki sposób znane jest jako module pattern:

var counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };   
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

W poprzednich przykładach każde domknięcie miało własne leksykalne środowisko. Jednakże w tym przypadku tworzymy pojedyncze środowisko leksykalne, współdzielone przez trzy funkcje: counter.incrementcounter.decrement oraz counter.value.

Owo współdzielone środowisko leksykalne tworzone jest w ciele funkcji anonimowej, która jest wykonana w momencie, gdy tylko zostanie zdefiniowana. Środowisko leksykalne zawiera dwa prywatne przedmioty: zmienną o nazwie privateCounter i funkcję o nazwie changeBy. Żaden z tych prywatnych przedmiotów nie może być wywołany bezpośrednio spoza funkcji anonimowej. Zamiast tego, muszą mieć być one wywołane poprzez trzy funkcje publiczne, które są zwracane z anonimowej klasy opakowującej (wrapper).

Te trzy funkcje publiczne to domknięcia, które współdzielą to samo środowisko. Dzięki JavaScriptowemu zakresowi leksykalnemu, każda z nich ma dostęp do zmiennej privateCounter oraz funkcji changeBy.

Zauważysz, że definiujemy anonimową funkcję, która tworzy licznik, a następnie od razu ją wywołujemy i przypisujemy wynik do zmiennej counter. Moglibyśmy przetrzymywać tę funkcję w oddzielnej zmiennej makeCounter i użyć jej do stworzenia kilku liczników.

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); /* Alerts 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* Alerts 2 */
counter1.decrement();
alert(counter1.value()); /* Alerts 1 */
alert(counter2.value()); /* Alerts 0 */

Zauważ, że każdy z dwóch liczników, counter1 oraz counter2, jest niezależny od drugiego. Każde domknięcie odnosi się do innej wersji zmiennej privateCounter przez własne domknięcie. Za każdym razem gdy któryś z liczników jest wywołany, jego środowisko leksykalne zmienia się przez zmianę wartości tej zmiennej; jednakże zmiany wartości zmiennej jednego domknięcia nie wpływają na wartość w innym domknięciu.

Używanie domknięć w ten sposób dostarcza wielu korzyści, które normalnie kojarzone sa z programowaniem obiektowym — w szczególności ukrywaniem oraz enkapsulacją danych.

Tworzenie domknięć w pętlach: popularne błędy

W czasach przed wprowadzeniem definicji let w standardzie ECMAScript 2015, popularnym problemem z domknieciami, bylo ich uzycie w pętlach.
Rozważmy poniższy kod:
 

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

Tablice  helpText definiuje trzy teksty  podpowiedzi, każda z nich jest powiazana z ID inputu z dokumentu.
Nastepnie w petli  for dodawana jest  obsluga zdazenia onfocus ktora ma pokazac tekst podpowiedzi przy wprowadzaniu tekstu  w pole.

Jezeli spradzisz dzialanie tego kodu, okaze sie ze nie dziala tak jak sie spodziewalismy. Zawsze zostanie wyswietlona podpowiedz dotyczaca ostatniego pola ('Your age (you must be over 16)')

Powodem takiego dzialania funkcji zwrotnej obslugujacej onfocus jest domknięcie;
Sklada sie ono z definicji funkcji i przechwyconego kontekstu zakresu z setupHelp. Trzy domkniecia stworzone sa w petli, ale dziela one to samo srodowisko leksykalne, ktore posiada zmienną, ktora jest aktualizowana (item.help). Trzy wartosci item.help są determinowane  w petli, wiec kiedy obsluga zdarzenia  onfocus zostanie wywolana, item.helpbedzie mialo wartosc ostatniej z ostatniej interracji pentli, ktora dawno zokonczyla swe dzialanie.

Rozwiazaniem tego problemu jest uzycie kolejnych domkniec, szczególnie fabryki funkcji opisanej wczesniej

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

To rozwiazanie dziala zgodznie z oczekiwaniami. 
This works as expected. W odroznieniu od wczesniejszego przykladu,   wszytkie funkcje zwrotne, posiadaja pojedyncze srodowisko leksykalne korespondujace z tablicą helpText, stworzone przez  makeHelpCallback 

Inna droga jest uzycie anonimowego domkniecia:

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
  }
}

setupHelp();

Jezeli nie chcesz uzywac domkniec , możesz użyć slowa kluczowego  let  ze standardu ES2015

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

Ten przyklad uzywa let zamiast var, wiec kazde domkniecie dowiazywane jest do blokowego zasiegu funkcji. 

Wątpliwości wydajnościowe

Niemądrze jest, aby niepotrzebnie tworzyć funkcje wewnątrz innych funkcji, jeżeli domnkięcia nie są wymagane w danej sytuacji, jako że odbije się to w negatywny sposób na wydajność skryptu, mierzoną poprzez czas wykonywania jak również i używaną pamięć.

Na przykład gdy tworzymy nowy obiekt/klasę, metody powinny być zwykle powiązane z prototypem obiketu zamiast definiowane w konstruktorze obiektu. Przyczyną jest to, że za każdym razem gdy konstruktor zostanie użyty, metody zostaną nadpisane (czyli przy każdym tworzeniu nowego obiektu).

Rozważmy następujący przykład:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

Ponieważ poprzedni przykład nie wykorzystuje zalet, które płyną z wykorzystania domknięć, możemy to przepisać w następujący sposób:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

Jednakże ponowne definiowanie prototypu nie jest rekomendowane. Poniższy przykład zamiast tego dodaje właściwości do istniejącego prototypu:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

Powyższy przykład może też zostać przepisany w bardziej czytelny sposób, z identycznym wynikiem:

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
}
(function() {
    this.getName = function() {
        return this.name;
    };
    this.getMessage = function() {
        return this.message;
    };
}).call(MyObject.prototype);

W poprzednich trzech przykładach odziedziczony prototyp może być współdzielony przez wszystkie obiektu i definicje metod nie muszą występować przy każdorazowym tworzeniu obiektu. Aby dowiedzieć się więcej, zobacz Szczegóły modelu obiektowego.

In the three previous examples, the inherited prototype can be shared by all objects and the method definitions need not occur at every object creation. See Details of the Object Model for more.

Autorzy i etykiety dokumentu

Autorzy tej strony: mat-bi, Jakubem, Bx., dodekx, lboratynski, ppuzio, kosanr1, ewape
Ostatnia aktualizacja: mat-bi,