Замикання (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, яка є локальною змінною функції, яка вже виконалась.
В наступній статті я розкажу про найпоширеніші області застосування замикань, а також про граблі, на які можна наткнутися, застосовуючи цей механізм.
Коментарі (4)
RSS згорнути / розгорнутиз особистого досвіду співбесідування особливо проблемним місцем є симуляція OOP на JavaScript, наприклад межі тощо що може prototype і т.д.
zenyk
Тут вже проскакував опис ООП: йшлося тільки про створення об’єктів, але про паттерни наслідування не згадувалось. Якщо буде час, то напишу, бо сам стикався з десятком тих паттернів :)
volopav
і
Кінцевий приклад, як на мене, невдалий — ілюструє більше принцип рекурсивних функцій, аніж сабж.
Roland
А от кома після document.getElementById(id) не є опечаткою, то бо змінні можна оголошувати через кому після var.
Скажу так, кінцевий приклад ілюструє рекурсію також. Багато прикладів, думаю що більш вдалих, буде в окремій статті.
volopav
Тільки зареєстровані й авторизовані користувачі можуть залишати коментарі.