Czym są domknięcia w JavaScript?

JavaScript

readTime

6 min

Czym są domknięcia w JavaScript?

JavaScript to język, który oferuje niesamowite możliwości. Jedną z funkcji, która często budzi emocje wśród programistów, są domknięcia.

Dla początkujących może wydawać się skomplikowane ale w tym artykule wyjaśnię Ci, czym są domknięcia w JavaScript, jak działają i dlaczego są tak ważne.

Będziemy używać prostych przykładów, które pomogą Ci zrozumieć ten temat i zastosować go w Twoim kodzie.

Czym są domknięcia w JavaScript?

Na początek najważniejsze – czym są domknięcia w JavaScript?

Domknięcie (z ang. closure) to mechanizm, w którym funkcja wewnętrzna ma dostęp do zmiennych zdefiniowanych w funkcji zewnętrznej, nawet po zakończeniu jej wykonywania.

Daje to niezwykłą moc, bo możesz „domknąć” pewne dane w ramach funkcji i korzystać z nich, kiedy tego potrzebujesz.

Wyobraź sobie, że masz funkcję, która tworzy zestaw danych i zwraca inną funkcję, która może korzystać z tych danych.

Zmienna zewnętrzna zostaje „domknięta” i zachowana dla funkcji wewnętrznej, nawet po zakończeniu działania funkcji zewnętrznej.

Jak działa domknięcie w JavaScript?

Przykład z domknięciem

Spróbujmy zrozumieć to za pomocą prostego przykładu.
Zobaczmy, jak działa domknięcie:

javascript
function outerFunction() {
  let outerVariable = "Jestem na zewnątrz!";

  function innerFunction() {
    console.log(outerVariable); // 'Jestem na zewnątrz!'
  }

  return innerFunction;
}

const myClosure = outerFunction();
myClosure();

Mamy tutaj dwie funkcje – outerFunction i innerFunction. Kiedy wywołujemy outerFunction, zwraca ona wewnętrzną funkcję.

Wewnętrzna funkcja innerFunction wciąż ma dostęp do zmiennej outerVariable, nawet po zakończeniu działania outerFunction.

To właśnie jest domknięcie – funkcja zapamiętuje swoje otoczenie (czyli zmienne, które były dostępne w momencie jej tworzenia).

Lexical Scope – podstawa domknięć

Aby zrozumieć, jak działają domknięcia, musisz poznać pojęcie zakresu leksykalnego (ang. lexical scope).

W JavaScript zmienne mają zakres określony przez to, gdzie zostały zdefiniowane, a nie gdzie zostały wywołane.

To oznacza, że wewnętrzna funkcja ma dostęp do zmiennych z funkcji zewnętrznej, ponieważ została w niej zdefiniowana.

Przykład:

javascript
function sayHello() {
  let name = "Janek";

  if (true) {
    let name = "Adam";
    console.log(name); // "Adam"
  }

  console.log(name); // "Janek"
}

sayHello();

W tym przykładzie mamy dwie zmienne o nazwie name, ale każda z nich działa w swoim własnym zakresie leksykalnym.
Zmienna wewnątrz bloku if nie wpływa na zmienną name zdefiniowaną w zakresie głównym funkcji sayHello.

Różnica między var, let i const

W JavaScript mamy trzy słowa kluczowe do deklarowania zmiennych: var, let i const.

Zmienna zadeklarowana za pomocą var może być dostępna poza blokiem, w którym została zadeklarowana.

Natomiast zmienne zdefiniowane za pomocą let i const mają zakres blokowy, co oznacza, że są dostępne tylko wewnątrz tego bloku.

Przykład z var:

javascript
function testVar() {
  var licznik = 1;

  if (true) {
    var licznik = 2;
    console.log(licznik); // 2
  }

  console.log(licznik); // 2
}

testVar();

Przykład z let:

javascript
function testLet() {
  let licznik = 1;

  if (true) {
    let licznik = 2;
    console.log(licznik); // 2
  }

  console.log(licznik); // 1
}

testLet();

Jak widzisz, let i const zapewniają większą kontrolę nad zakresem leksykalnym, co czyni kod bardziej przewidywalnym i bezpiecznym.

Jak wykorzystać domknięć w praktyce?

Tworzenie liczników

Domknięcia są świetne do tworzenia liczników, które mogą domknąć zmienną licznikową i stopniowo ją zwiększać:

javascript
function createCounter() {
  let count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

Tutaj funkcja createCounter tworzy licznik, który domyka zmienną count.
Za każdym razem, gdy wywołasz counter(), wartość count zostanie zwiększona.

Domknięcia jako prywatne zmienne

Jednym z najciekawszych zastosowań domknięć jest tworzenie prywatnych zmiennych.
Możemy używać funkcji do domknięcia zmiennych, które nie są dostępne z zewnątrz:

javascript
function privateData() {
  let secret = "Moja tajemnica";

  return {
    getSecret: function () {
      return secret;
    },
    setSecret: function (newSecret) {
      secret = newSecret;
    },
  };
}

const myData = privateData();
console.log(myData.getSecret()); // 'Moja tajemnica'
myData.setSecret("Nowa tajemnica");
console.log(myData.getSecret()); // 'Nowa tajemnica'

Tutaj mamy funkcję privateData, która domyka zmienną secret, tworząc mechanizm prywatnych zmiennych z ograniczonym dostępem.

Pułapki związane z domknięciami

Chociaż domknięcia są świetne, mogą też prowadzić do problemów, zwłaszcza gdy używasz ich niewłaściwie.

Jednym z najczęstszych błędów jest zła obsługa zmiennych w pętli.

Przykład z var:

javascript
for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

// Output: 3, 3, 3

Zmienna i jest domknięta i po zakończeniu pętli ma wartość 3.

Dlatego wszystkie wywołania setTimeout logują tę wartość. Rozwiązanie?

Użyj let, który domknie zmienną dla każdego wywołania:

javascript
for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

// Output: 0, 1, 2

Wydajność i zarządzanie pamięcią

Domknięcia w JavaScript mogą czasem prowadzić do problemów z pamięcią, jeśli używasz ich w nieodpowiedni sposób.

Gdy tworzysz domknięcie, zmienne zewnętrzne są zachowywane w pamięci, dopóki są one potrzebne.
Jeśli masz zbyt wiele domknięć lub zbyt duże domknięcia, może to prowadzić do niepotrzebnego obciążenia pamięci.

Przykład:

javascript
function outerFunction() {
  var hugeArray = new Array(1000000).fill("Dane");

  function innerFunction() {
    console.log(hugeArray.length);
  }

  return innerFunction;
}

const myClosure = outerFunction();
myClosure();

Tutaj duża tablica hugeArray jest domknięta, co oznacza, że zostanie ona zachowana w pamięci, dopóki innerFunction będzie miała do niej dostęp.

Aby tego uniknąć, możesz domknąć tylko to, co jest naprawdę istotne, i usuwać referencje, gdy nie są już potrzebne.

Zastosowanie domknięć w node.js i express.js

Domknięcia są kluczowe również w środowiskach takich jak Node.js, gdzie często mamy do czynienia z asynchronicznymi operacjami.

Dzięki domknięciom możemy mieć pewność, że nasze funkcje asynchroniczne będą miały dostęp do odpowiednich zmiennych, nawet gdy zostaną wykonane po pewnym czasie.

Przykład z express.js:

javascript
const express = require("express");
const app = express();

function createLogger() {
  const start = Date.now();

  return function (req, res, next) {
    const duration = Date.now() - start;
    console.log(`Request time: ${duration}ms`);
    next();
  };
}

app.use(createLogger());

app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.listen(3000);

W tym przypadku funkcja createLogger domyka zmienną start, która zachowuje czas rozpoczęcia żądania.

Dzięki temu każda funkcja middleware ma dostęp do tej wartości.

Podsumowanie

Domknięcia w JavaScript pozwalają domknąć zmienne w funkcjach i mieć do nich dostęp w odpowiednim momencie.

Dzięki zakresowi leksykalnemu w JavaScript, możesz tworzyć bardziej złożone aplikacje, które działają przewidywalnie i efektywnie.

Pamiętaj jednak, aby używać domknięć rozważnie – nadmierne korzystanie może prowadzić do problemów z wydajnością.

authorImg

Witek Pruchnicki

Z pasją dzielę się wiedzą o programowaniu i nie tylko na różne sposoby