Замикання в JavaScript

Замикання (closures) — доволі «слизька» для розуміння властивість JavaScript, але насправді принцип дії цього механізму простий.

Область видимості

В JavaScript окрема область видимості створюється тільки функціями, а не блоками обмеженими фігурними дужками:

function hello() {
    var a = 1;
    var b = 'hello';
    if(b === 'hello') {
        var c = b + ' world';
    }
    if(a) {
        var m = c + '!';
    }
    console.log(m);
}

hello(); // hello world!

Хоч m і c були оголошені всередині блоків, вони (змінні) видимі в межаш всієї функції. Також всередині цієї функції видимі всі змінні та інші функції, які були визначені в глобальній області:

var a = 'some value';
function getA() {
    return a;
}

console.log(getA()); //some value

Але при цьому локальні змінні та функції назовні не видимі:

function hereIsB() {
    var b = 'local variable';
}

hereIsB();
console.log(b); //b is undefined

Якщо визначити функцію всередині іншої функції, то внутрішня функція має доступ до локальних змінних зовнішньої функції:

var a = 1;
function outside() {
    var b = 'outside local';
    function inside() {
        var c = 'inside local';
        b += ' can be modified';
    }
    
    inside();
    console.log(a); 
    console.log(b); 
    console.log©; 
}

outside();
//1
//outside local can be modified
//c is undefined

Це ще називаєтсься лексичною областю видимості, бо задається вона на етапі оголошення функції, а не в момент її виконання. Щоб фунція inside() мала доступ до області видимості outside(), вона повинна бути оголошена всередині outside(), а не виконана там:

function outside() {
    var a = 'fake outside local';
    inside();
}

function inside() {
    console.log(a);
}

outside(); //a is undefined

inside() виконується всередині outside() і там же оголошена локальна змінна a, тому можна подумати, що a видима для inside(), але це не так, бо в момент оголошення inside() в її області видимості не було ніякої a.

В момент оголошення функції вона «запам’ятовує» середовище, в якому була визначена. Це середовище складається з власної (приватної) області та областей «батьківських» функцій (вгору по ієрархії). Це зовсім не означає, що функція пам’ятає кожну локальну змінну з ланцюжка. Навпаки: запам’ятовується тільки область, де можна шукати, а самі змінні можна динамічно змінювати, створювати нові, видялати існуючі, а функція підчас виконання звернеться до найсвіжішого значення змінної або видасть помилку, якщо такої змінної нема. Якщо в попередньому прикладі створити глобальну змінну a, то inside() її побачить, бо вона пам’ятає шлях до глобальної області. Також можна оголошувати функції, які містять виклики невизначених функцій. В нашому прикладі outside() повинна знати тільки свою область і все, що з’являється в цій області, миттєво стає доступним для outside()

function outside() {
    var a = 'fake outside local';
    inside();
}

outside(); //inside is not defined

function inside() {
   console.log(a);
}

outside(); //a is not defined

var a = 'hello';

outside(); //hello

delete inside;

outside(); //inside is not defined

function inside() {
    console.log(a + ' world!');
}

outside(); //hello world!


Розривання ланцюжка за допомогою замикання

Візьмемо таку ситуацію:

var a = 'some variable';
function F() {
   var b = 'another variable';

   function N() {
       var c = 'hello';
   }
}

В глобальній області є змінна a та функція F(), всередині якої визначена змінна b та функція N(). c є локальною змінно N().
З точки декларації a маємо доступ тільки до глобальної області. Від b — до глобальної області та області функції F(). Якщо ж бути всередині N(), то видимою є глобальна область, а також області функцій F() та N(). Від a до b доступу нема, бо b не є видимою за межами F(). Але від c до b та від N() до b доступ є.
Цікава річ — замикання — стається, коли N() виривається за межі F() і стає глобальною. Але оскільки N() була оголошена всередині F(), то вона «пам’ятає» область F() і має доступ до b. При цьому будь-яка інша глобальна функція доступу до b не має.

Є два шляхи, як можна «замкнутися» з глобальною областю: оголосити внутрішню функцію глобальною (оголосити без var), або зробити її результатом виконання батьківської функції.

function outside() {
    var b = 'I exist!';
    return function() {
        return b
    }
}

var inside = outside();
console.log(inside()); //I exist!

outside() повертає іншу функцію, яка має свою приватну область та доступ до області outside(), а отже і доступ до b. Оскільки outside() є глобальною функцією, то результат її виконання можна присвоїти глобальній змінній. Як результат, маємо глобальну функцію inside(), яка має доступ до приватної області функції outside().

Другий спосіб:

var inside;

function outside() {
    var b = 'I exist!';
    inside = function() {
        return b;
    }
}

console.log(inside()); //undefined
outside();
console.log(inside()); //I exist!


Замикання виникає тоді, коли функція зберігає посилання на область батьківської функції, навіть якщо батьківська функція виконалась і повернула значення. Таким чином утворюється замкнена область (капсула), яка невидима з глобальної області, але глобальні функції, які «вирвались» назовні, мають доступ до цієї капсули і можуть там створювати/видаляти/змінювати функції та змінні.

Давайте розглянемо приклад:

function fade(id) {
    var dom = document.getElementById(id),
        level = 1;
    function step () {
        var h = level.toString(16);
        dom.style.backgroundColor = 
            '#FFFF' + h + h;
        if (level < 15) {
            level += 1;
            setTimeout(step, 100);
        }
    }
    setTimeout(step, 100);
}

Якщо виконати fade() і передати їй id елемента як аргумент:

fade('popup');

то сама fade() виконається лише один раз, але елемент плавно змінить колір фону, бо внутрішня функція step() виконається 14 разів. При цьому в кожній ітерації буде оновлюватися значення level, яка є локальною змінною функції, яка вже виконалась.

В наступній статті я розкажу про найпоширеніші області застосування замикань, а також про граблі, на які можна наткнутися, застосовуючи цей механізм.
  • +7
  • 18 січня 2010, 19:50
  • volopav

Коментарі (4)

RSS згорнути / розгорнути
+
0
гарна стаття! чекаємо наступну :)

з особистого досвіду співбесідування особливо проблемним місцем є симуляція OOP на JavaScript, наприклад межі тощо що може prototype і т.д.
avatar

zenyk

  • 19 січня 2010, 00:53
+
+1
Дякую :)

Тут вже проскакував опис ООП: йшлося тільки про створення об’єктів, але про паттерни наслідування не згадувалось. Якщо буде час, то напишу, бо сам стикався з десятком тих паттернів :)
avatar

volopav

  • 19 січня 2010, 14:14
+
+1
Очепатки:
function inside() {
console.log(a + ' wolrd!');
}

outside(); //hello world!

і
document.getElementById(id),


Кінцевий приклад, як на мене, невдалий — ілюструє більше принцип рекурсивних функцій, аніж сабж.
avatar

Roland

  • 19 січня 2010, 19:58
+
0
Мені від тих ворлдів вже рябіло в очах, тому і пропустив :)
А от кома після document.getElementById(id) не є опечаткою, то бо змінні можна оголошувати через кому після var.

Скажу так, кінцевий приклад ілюструє рекурсію також. Багато прикладів, думаю що більш вдалих, буде в окремій статті.
avatar

volopav

  • 19 січня 2010, 20:39

Тільки зареєстровані й авторизовані користувачі можуть залишати коментарі.