Область видимості змінних, замикання
JavaScript – мова із сильним функціонально-орієнтованим ухилом. Він дає нам багато свободи. Функція може бути динамічно створена, скопійована в іншу змінну або передана як аргумент іншої функції і пізніше викликана зовсім іншого місця.
Ми знаємо, що функція може отримати доступ до змінних із зовнішнього оточення, ця можливість використовується дуже часто.
Але що станеться, коли зовнішні змінні зміняться? Чи функція отримає останнє значення або те, що існувало на момент створення функції?
І що станеться, коли функція переміститься в інше місце в коді і буде викликана звідти – чи вона отримає доступ до зовнішніх змінних свого нового розташування?
Різні мови поводяться по-різному в таких випадках, і в цьому розділі ми розглянемо поведінку JavaScript.
У JavaScript існує три способи оголосити змінну: let, const (сучасні), і var (пережиток минулого).
- У цій статті ми будемо використовувати змінні let у прикладах.
- Змінні, оголошені за допомогою const поводяться так само, так що ця стаття і про них.
- Старі змінні var мають кілька характерних відмінностей, вони будуть розглянуті у розділі Застаріле ключове слово "var".
Блоки коду
Якщо змінна оголошена всередині кодового блоку <. >, вона видно тільки всередині цього блоку.
< // Виконуємо деякі дії з локальною змінною, які не повинні бути видно зовні let message = "Hello"; // Змінна видно тільки в цьому блоці alert (message); // Hello > alert (message); // ReferenceError: message is not defined
За допомогою блоків <. >ми можемо ізолювати частину коду, що виконує своє власне завдання, зі змінними, що належать тільки їй:
Зауважте, що без окремих блоків виникне помилка, якщо ми використовуємо let з існуючим ім'ям змінної:
// показати повідомлення let message = "Hello"; alert(message); // показати інше повідомлення let message = "Goodbye"; // SyntaxError: Identifier 'message' має already been declared alert(message);
Для if, for, while і т.д. змінні, оголошені у блоці коду <. >, також видно лише всередині:
if (true) < let phrase = "Hello"; alert(phrase); // Hello > alert (phrase); // Помилка, немає такої змінної!
У цьому випадку після завершення роботи if нижній alert не побачить phrase , що й призведе до помилки.
І це чудово, оскільки це дозволяє нам створювати блочно-локальні змінні, які стосуються лише гілки if .
Те саме можна сказати і про цикли for і while :
for (let i = 0; i < 3; i++) < // змінна i видно тільки всередині for alert(i); // 0, потім 1, потім 2> alert (i); // Помилка, немає такої змінної!
Візуально let i = 0; знаходиться поза блоком коду <. >, проте тут у разі for є особливість: змінна, оголошена всередині (. ) , вважається частиною блоку.
Вкладені функції
Функція називається вкладеною, коли вона створюється всередині іншої функції.
Це дуже легко зробити в JavaScript.
Ми можемо використовувати це для впорядкування нашого коду, наприклад, як тут:
function sayHiBye(firstName, lastName) < // функція-помічник, яку ми використовуємо нижче function getFullName() < return firstName + " " + lastName; >alert( "Hello, " + getFullName() ); alert("Bye," + getFullName()); >
Тут вкладена функція getFullName() створена для зручності. Вона може отримати доступ до зовнішніх змінних і вивести повне ім'я. JavaScript вкладені функції використовуються дуже часто.
Що ще цікавіше, вкладена функція може бути повернена: або як властивість нового об'єкта (якщо зовнішня функція створює об'єкт з методами), або сама по собі. І потім може бути використана у будь-якому місці.Не важливо де, вона так само матиме доступ до тих же зовнішніх змінних.
Нижче, makeCounter створює функцію «лічильник», яка при кожному виклику повертає таке число:
function makeCounter() < let count = 0; return function() < return count++; // є доступом до зовнішньої змінної " count " >; > let counter = makeCounter(); alert (counter ()); / / 0 alert (counter ()); // 1 alert (counter ()); // 2
Незважаючи на простоту цього прикладу, трохи модифіковані його варіанти застосовуються на практиці, наприклад, у генераторі псевдовипадкових чисел та в багатьох інших випадках.
Як це працює? Якщо ми створимо кілька таких лічильників, чи вони будуть незалежними один від одного? Що відбувається зі змінними?
Розуміння таких речей корисне підвищення загального рівня володіння JavaScript і більш складних сценаріїв. Тож давайте трохи заглибимося.
Лексичне оточення
Глибокий технічний опис – попереду.
Як би мені не хотілося уникнути низькорівневих деталей мови, будь-яке уявлення про JavaScript без них буде недостатнім і неповним, тому приготуйтеся.
Для більшої наочності пояснення розбито кілька кроків.
Крок 1. Змінні
У JavaScript у кожної функції, що виконується, блоку коду <. >і скрипта є пов'язаний з ними внутрішній (прихований) об'єкт, званий лексичним оточенням LexicalEnvironment.
Об'єкт лексичного оточення складається із двох частин:
- Environment Record - Об'єкт, в якому як властивості зберігаються всі локальні змінні (а також деяка інша інформація, така як значення this).
- Посилання на зовнішнє лексичне оточення - тобто те, що відповідає коду зовні (зовні від поточних фігурних дужок).
"Змінна" - це просто властивість спеціального внутрішнього об'єкта: Environment Record. "Отримати або змінити змінну" означає "отримати або змінити властивість цього об'єкта".
Наприклад, у цьому простому коді лише одне лексичне оточення:
Це так зване глобальне лексичне оточення, пов'язане з усім скриптом.
На зображенні вище прямокутник означає Environment Record (сховище змінних), а стрілка означає посилання на зовнішнє оточення. У глобального лексичного оточення немає зовнішнього оточення, тому вона вказує на null .
У міру виконання коду лексичне оточення змінюється.
Прямокутники з правого боку демонструють, як глобальне лексичне оточення змінюється у процесі виконання коду:
- При запуску скрипта лексичне оточення попередньо заповнюється всіма оголошеними змінними.
- Спочатку вони перебувають у стані «Uninitialized». Це особливий внутрішній стан, яке означає, що двигун знає про змінну, але на неї не можна посилатися, доки вона не буде оголошена за допомогою let. Це майже те саме, якби змінна не існувала.
- З'являється визначення змінної let phrase. У неї ще немає значення, тому присвоюється undefined . З цього моменту ми можемо використати змінну.
- Змінної phrase надається значення.
- Змінна phrase змінює значення.
Поки що все виглядає просто, правда?
- Змінна – це властивість спеціального внутрішнього об'єкта, пов'язаного з поточним виконуваним блоком/функцією/скриптом.
- Робота зі змінними – це насправді робота із властивостями цього об'єкта.
"Лексичне оточення" - це об'єкт специфікації: він існує тільки "теоретично" у специфікації мови для опису того, як усе працює. Ми не можемо отримати цей об'єкт у нашому коді та маніпулювати їм безпосередньо.
JavaScript-движки також можуть оптимізувати його, відкидати змінні для економії пам'яті, що не використовуються, і виконувати інші внутрішні дії, але при цьому видима поведінка залишається такою, як описано.
Крок 2. Function Declaration
Функція – це значення, як і змінна.
Різниця полягає в тому, що Function Declaration миттєво повністю ініціалізується.
Коли створюється лексичне оточення, Function Declaration відразу стає функцією, готової до використання (на відміну let, який досі оголошення може бути використаний).
Саме тому ми можемо викликати функцію, оголошену як Function Declaration, до її оголошення.
Ось, наприклад, початковий стан глобального лексичного оточення при додаванні функції:
Звичайно, така поведінка стосується лише Function Declaration, а не Function Expression, в яких ми присвоюємо функцію змінної, наприклад let say = function(name) <. >.
Крок 3. Внутрішнє та зовнішнє лексичне оточення
Коли функція запускається, на початку її виклику автоматично створюється нове лексичне оточення для зберігання локальних змінних та параметрів виклику.
Наприклад, для say("John") це виглядає так (виконання знаходиться на рядку, позначеному стрілкою):
У процесі виклику функції ми маємо два лексичні оточення: внутрішнє (для викликаної функції) і зовнішнє (глобальне):
- Внутрішнє лексичне оточення відповідає поточному виконанню say. У ньому є одна змінна name , параметр функції. Ми викликаємо say("John"), так що значення змінної name дорівнює "John".
- Зовнішнє лексичне оточення – це світове лексичне оточення. У ньому знаходяться змінна phrase та сама функція.
У внутрішнього лексичного оточення є посилання на зовнішнє outer.
Коли код хоче отримати доступ до змінної – спочатку відбувається пошук у внутрішньому лексичному оточенні, потім у зовнішньому, потім у наступному тощо, до глобального.
Якщо змінну не було знайдено, це буде помилкою в строгому режимі (use strict).Без суворого режиму, для зворотної сумісності, присвоєння неіснуючої змінної створює нову глобальну змінну з такою ж назвою.
Давайте подивимося, як відбувається пошук у нашому прикладі:
- Для змінної name alert всередині say відразу ж знаходить її у внутрішньому лексичному оточенні.
- Коли alert хоче отримати доступ до phrase, він не знаходить її локально, тому змушений звернутися до зовнішнього лексичного оточення і знаходить phrase там.
Крок 4. Повернення функції
Повернімося наприклад з makeCounter :
function makeCounter() < let count = 0; return function() < return count++; >; > let counter = makeCounter();
На початку кожного виклику makeCounter() створюється новий об'єкт лексичного оточення, в якому зберігаються змінні для конкретного запуску makeCounter.
Таким чином, ми маємо два вкладені лексичні оточення, як у прикладі вище:
Відмінність полягає в тому, що під час виконання makeCounter() створюється крихітна вкладена функція, що складається всього з одного рядка: return count++. Ми її ще не запускаємо, а лише творимо.
Усі функції пам'ятають лексичне оточення, де вони були створені. Технічно тут немає ніякої магії: всі функції мають приховану властивість [[Environment]] , яка зберігає посилання на лексичне оточення, в якому було створено функцію:
Отже, counter.[[Environment]] має посилання лексичного оточення. Так функція запам'ятовує де вона була створена, незалежно від того, де вона викликається. Посилання на [[Environment]] встановлюється один раз і назавжди під час створення функції.
Згодом, за виклику counter() , цього виклику створюється нове лексичне оточення, яке зовнішнє посилання лексичне оточення береться з counter.[[Environment]] :
Тепер, коли код всередині counter() шукає змінну count , він спочатку шукає її у власному лексичному оточенні (порожньому, тому що там немає локальних змінних), а потім у лексичному оточенні зовнішнього виклику makeCounter() , де знаходить count та змінює її.
Змінна оновлюється у тому лексичному оточенні, де вона існує.
Ось стан після виконання:
Якщо ми викличемо counter() кілька разів, то в тому самому місці змінна count буде збільшена до 2, 3 і т.д.
У програмуванні є загальний термін: «замикання», який повинен знати кожен розробник.
Замикання – це функція, яка запам'ятовує свої зовнішні змінні та може отримати доступ до них. У деяких мовах це неможливо, або функція повинна бути написана спеціальним чином, щоб вийшло замикання. Але, як було описано вище, JavaScript, всі функції спочатку є замикання (є тільки один виняток, про який буде розказано в Синтаксис "new Function").
Тобто вони автоматично запам'ятовують, де були створені, за допомогою прихованої властивості [[Environment]] , і всі вони можуть отримати доступ до зовнішніх змінних.
Коли на співбесіді фронтенд-розробнику запитують: «що таке замикання?», правильною відповіддю буде визначення замикання та пояснення того факту, що всі функції в JavaScript є замиканнями, і, можливо, кілька слів про технічні деталі: властивість [[Environment ]] і про те, як працює лексичне оточення.
Складання сміття
Зазвичай лексичне оточення видаляється з пам'яті разом із усіма змінними після завершення виклику функції. Це з тим, що у нього немає посилань. Як і будь-який об'єкт JavaScript, воно зберігається в пам'яті лише доти, доки до нього можна звернутися.
Однак якщо існує вкладена функція, яка все ще доступна після завершення функції, вона має властивість [[Environment]] , що посилається на лексичне оточення.
І тут лексичне оточення залишається доступним навіть після завершення роботи функції.
function f() < let value = 123; return function() < alert(value); >> let g = f(); // g.[[Environment]] зберігає посилання лексичне оточення // з відповідного виклику f()
Зверніть увагу, що якщо f() викликається багато разів і результуючі функції зберігаються, всі відповідні об'єкти лексичного оточення також будуть збережені в пам'яті. У наведеному нижче коді – всі три:
function f() < let value = Math.random(); return function() < alert(value); >; > // 3 функції масиві, кожна з яких посилається на лексичне оточення // з відповідного виклику f() let arr = [f(), f(), f()];
Об'єкт лексичного оточення зникає, коли стає недоступним (як будь-який інший об'єкт). Іншими словами, він існує тільки доти, доки на нього посилається хоча б одна вкладена функція.
У наведеному нижче коді після видалення вкладеної функції її навколишнє лексичне оточення (а отже, і value ) очищається з пам'яті:
function f() < let value = 123; return function() < alert(value); >> let g = f(); // Поки існує функція g, value залишається в пам'яті g = null; //. і тепер пам'ять очищена.
Оптимізація на практиці
Як ми бачили, теоретично, поки функція жива, всі зовнішні змінні теж зберігаються.
Але на практиці движки JavaScript намагаються це оптимізувати. Вони аналізують використання змінних і, якщо легко за кодом зрозуміти, що зовнішня змінна не використовується, вона видаляється.
Одним із важливих побічних ефектів у V8 (Chrome, Edge, Opera) є те, що така змінна стає недоступною при налагодженні.
Спробуйте запустити наступний приклад у Chrome із відкритою Developer Tools.
Коли код буде поставлено на паузу, напишіть консолі alert(value) .
function f() < let value = Math.random(); function g() < debugger; // У консолі: напишіть alert (value); Такої змінної немає! >return g; > let g = f(); g();
Як ви можете бачити – такої змінної не існує! Теоретично, вона повинна бути доступна, але потрапила під оптимізацію двигуна.
Це може призвести до кумедних (якщо вдається вирішити швидко) проблем при налагодженні. Одна з них – ми можемо побачити не ту зовнішню змінну при назвах, що збігаються:
let value = "Сюрприз!"; function f() < let value = "найближче значення"; function g() < debugger; // У консолі: напишіть alert (value); Сюрприз! >return g; > let g = f(); g();
Цю особливість V8 корисно знати. Якщо ви займаєтеся налагодженням у Chrome/Edge/Opera, рано чи пізно ви з нею зіткнетеся.
Це не баг у відладчику, а скоріше особливість V8. Можливо, згодом це зміниться. Ви завжди можете перевірити це, запустивши приклади на цій сторінці.
Завдання
Чи функція враховує останні зміни?
Функція sayHi використовує ім'я зовнішньої змінної. Яке значення використовуватиметься під час виконання функції?
let name = "John"; function sayHi() < alert("Hi, " + name); > name = "Pete"; sayHi(); // Що буде показано: "John" чи "Pete"?
Такі ситуації зустрічаються як під час розробки для браузера, так сервера. Функція може бути призначена на виконання пізніше, ніж вона була створена, наприклад після дії користувача або мережного запиту.
Отже, питання: чи враховує вона останні зміни?
Відповідь: Pete.
Функція отримує зовнішні змінні у тому вигляді, в якому вони знаходяться зараз, вона використовує останні значення.
Старі значення змінних ніде не зберігаються. Коли функція звертається до змінної, вона набирає поточного значення зі свого чи зовнішнього лексичного оточення.
Які змінні доступні?
Наведена нижче функція makeWorker створює іншу функцію та повертає її. Ця нова функція може бути викликана з іншого місця.
Чи матиме вона доступ до зовнішніх змінних з місця свого створення, або з місця виклику, або з обох місць?
function makeWorker() < let name = "Pete"; return function() < alert(name); >; > let name = "John"; // Створюємо функцію let work = makeWorker(); // Викликаємо її work(); // Що буде показано?
Яке значення буде показано? "Pete" або "John"?
Відповідь: Pete.
Функція work() у наведеному нижче коді отримує name з місця його походження через посилання на зовнішнє лексичне оточення:
Таким чином, в результаті ми отримуємо "Pete".
Але якби в makeWorker() не було let name, то пошук йшов би зовні і брав глобальну змінну, що ми бачимо з наведеного вище ланцюжка. У цьому випадку результатом було б "John".
Чи незалежні лічильники?
Тут ми робимо два лічильники: counter і counter2, використовуючи одну і ту ж функцію makeCounter.
Вони незалежні? Що покаже другий лічильник? 0,1 чи 2,3 чи щось ще?
function makeCounter() < let count = 0; return function() < return count++; >; > let counter = makeCounter(); let counter2 = makeCounter(); alert (counter ()); / / 0 alert (counter ()); // 1 alert (counter2 ()); //? alert (counter2 ()); //?
Відповідь: 0,1.
Функції counter і counter2 створені різними викликами makeCounter.
Отже, у них незалежні зовнішні лексичні оточення, у кожного з яких свій власний count.
Замикання
Замикання - це комбінація функції та лексичного оточення, в якому ця функція була визначена. Іншими словами, замикання дає вам доступ до Scope зовнішньої функції із внутрішньої функції. У JavaScript замикання створюються щоразу під час створення функції, під час її створення.
Лексична область видимості
Розглянемо наступний приклад:
function init() < var name = "Mozilla"; // name - локальна змінна, створена init function displayName() < // displayName() - внутрішня функція, замикання alert(name); // displayName() використовує змінну, оголошену батьківської функції >displayName(); > init();
init() створює локальну змінну name та визначає функцію displayName() . displayName() - це внутрішня функція - вона визначена всередині init() і доступна лише всередині тіла функції init(). Зверніть увагу, що функція displayName() не має власних локальних змінних. Однак, оскільки внутрішні функції мають доступ до змінних зовнішніх функцій, displayName() може мати доступ до змінної name, оголошеної в батьківській функції init().
Виконайте цей код і зверніть увагу, що команда alert() всередині displayName() благополучно виводить на екран вміст змінної name, оголошеної в батьківській функції. Це приклад так званої лексичної галузі видимості (lexical scoping): JavaScript область дії змінної визначається за її розташуванням в коді (це очевидно лексично), та вкладені функції мають доступ до змінних, оголошених зовні. Цей механізм називається Lexical scoping (область дії, обмежена лексично).
Замикання
Розглянемо наступний приклад:
function makeFunc() < var name = "Mozilla"; function displayName() < alert(name); >return displayName; > var myFunc = makeFunc(); myFunc();
Якщо виконати цей код, то результат буде таким самим, як і виконання init() з попереднього прикладу: рядок "Mozilla" буде показаний у JavaScript alert діалозі. Що відрізняє цей код і представляє для нас інтерес, так це те, що внутрішня функція displayName() була повернута із зовнішньої до того, як була виконана.
На перший погляд, здається неочевидним, що цей код є правильним, але він працює. У деяких мовах програмування локальні змінні функції існують лише під час виконання цієї функції. Після завершення виконання makeFunc() очікується, що змінна name більше не буде доступним. Однак оскільки код продовжує нормально працювати, очевидно, що це не так у випадку JavaScript.
Причина в тому, що функції JavaScript формують так звані замикання. Замикання - це комбінація функції та лексичного оточення, в якому ця функція була оголошена. Це оточення складається з довільної кількості локальних змінних, які були в дію функції під час створення замикання. У розглянутому прикладі myFunc це посилання на екземпляр функції displayName , створеної в результаті виконання makeFunc . Примірник функції displayName у свою чергу зберігає посилання на своє лексичне оточення, в якому є змінна name . З цієї причини, коли відбувається виклик функції myFunc , змінна name залишається доступною для використання і збережений текст "Mozilla" передається в alert .
А ось трохи цікавіший приклад - функція 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
Тут ми визначили функцію makeAdder(x), яка отримує єдиний аргумент x і повертає нову функцію. Ця функція отримує єдиний аргумент y і повертає суму x та y.
По суті, makeAdder — це фабрика функцій: вона створює функції, які можуть додавати певного значення до свого аргументу. У прикладі вище ми використовуємо нашу фабричну функцію для створення двох нових функцій - одна додає 5 до свого аргументу, друга додає 10.
add5 і add10 – це приклади замикань. Ці функції ділять одне визначення тіла функції, але вони зберігають різні оточення. В оточенні функції add5 x – це 5, тоді як в оточенні add10 x – це 10.
Замикання практично
Замикання корисні тим, що дозволяють пов'язати дані (лексичне оточення) з функцією, яка працює з цими даними. Очевидною є паралель з об'єктно-орієнтованим програмуванням, де об'єкти дозволяють нам пов'язати деякі дані (властивості об'єкта) з одним або декількома методами.
Отже, замикання можна використовувати скрізь, де зазвичай використовували об'єкт з одним єдиним методом.
Такі ситуації повсюдно зустрічаються у веб-розробці. Велика кількість front-end коду, який ми пишемо на JavaScript, базується на обробці подій. Ми описуємо якусь поведінку, а потім пов'язуємо її з подією, яку створює користувач (наприклад, клік мишкою або натискання клавіші). При цьому наш код зазвичай прив'язується до події у вигляді зворотного/відповідного виклику (callback): callback функція - функція, що виконується у відповідь на виникнення події.
Давайте розглянемо практичний приклад: скажімо, ми хочемо додати на сторінку кілька кнопок, які змінюватимуть розмір тексту. Як варіант ми можемо вказати властивість font-size на елементі body в пікселах, а потім встановлювати розмір інших елементів сторінки (таких, як заголовки) з використанням відносних одиниць em:
Тоді наші кнопки змінюватимуть властивість font-size елемента body, а решта елементів сторінки просто отримає це нове значення і відмасштабує розмір тексту завдяки використанню відносних одиниць.
Використовуємо наступний JavaScript:
function makeSizer(size) < return function () < document.body.style.fontSize = size + "px"; >; > var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);
Тепер size12 , size14 і size16 - це функції, які змінюють розмір тексту в елементі body на значення 12, 14, і 16 пікселів, відповідно. Після чого ми чіпляємо ці функції на кнопки приблизно так:
document.getElementById("size-12").onclick = size12; document.getElementById("size-14").onclick = size14; document.getElementById("size-16").onclick = size16;
Емуляція приватних (private) методів за допомогою замикань
Мови типу Java дозволяють нам оголошувати приватні (private) методи. Це означає, що можуть бути викликані лише методами тієї самої класу, у якому оголошені.
JavaScript не має вбудованої можливості зробити таке, але це можна емулювати за допомогою замикання. Приватні методи корисні не лише тим, що обмежують доступ до коду, це також потужний засіб глобальної організації простору імен, що дозволяє не засмічувати публічний інтерфейс коду внутрішніми методами класів.
Код нижче ілюструє, як можна використовувати замикання для визначення публічних функцій, які мають доступ до закритих від користувача (private) функцій та змінних. Така манера програмування називається модульне програмування:
var Counter = (function () < var privateCounter = 0; function changeBy(val) < privateCounter += val; >return < increment: function () < changeBy(1); >, decrement: function () < changeBy(-1 );>, value: function () < return privateCounter; >; alert(Counter.value()); /* Alerts 0 */ Counter.increment(); Counter.increment(); alert(Counter.value()); /* Alerts 2 */ Counter.decrement(); alert(Counter.value()); /* Alerts 1 */
Тут багато чого змінилося. У попередньому прикладі кожне замикання мало власний контекст виконання (оточення). Тут ми створюємо єдине оточення для трьох функцій: Counter.increment, Counter.decrement, та Counter.value.
Єдине оточення створюється в тілі анонімної функції, яка виконується на момент опису. Це оточення містить два приватні елементи: змінну privateCounter і функцію changeBy(val) . Жоден з цих елементів не доступний безпосередньо, за межами цієї анонімної функції. Натомість вони можуть і повинні використовуватися трьома публічними функціями, які повертаються анонімним блоком коду (anonymous wrapper), що виконується в тій самій анонімній функції.
Ці три громадські функції є замиканнями, які використовують загальний контекст виконання (оточення). Завдяки механізму lexical scoping в Javascript, всі вони мають доступ до змінної privateCounter і функції changeBy .
Зауважте, ми описуємо анонімну функцію, що створює лічильник, і відразу запускаємо її, привласнюючи результат виконання змінної Counter . Але ми також можемо не запускати цю функцію відразу, а зберегти її в окремій змінній, щоб використовувати для подальшого створення кількох лічильників так:
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 */
Зауважте, що лічильники працюють незалежно один від одного. Це відбувається тому, що у кожного в момент створення функцією makeCounter() також створювався свій окремий контекст виконання (оточення). Тобто приватна змінна privateCounter у кожному з лічильників це справді окрема, самостійна змінна.
Використовуючи замикання подібним чином, ви отримуєте ряд переваг, які зазвичай асоціюються з об'єктно-орієнтованим програмуванням, таких як ізоляція та інкапсуляція.
Створення замикань у циклі: Дуже часта помилка
До того, як у версії ECMAScript 6 ввели ключове слово let, постійно виникала наступна проблема при створенні замикань усередині циклу. Розглянемо приклад:
Helpful notes will appear here
E-mail:
Name:
Age:
function showHelp(help) < document.getElementById("help").innerHTML = help; >function setupHelp() < var helpText = [ < id: "email", help: "Ваша адреса e-mail" >, < id: "name", help: "Ваше повне ім'я" >, < id: "age" , help: "Ваш вік (Вам має бути більше 16)" >,]; for (var i = 0; i < helpText.length; i++) < var item = helpText[i]; document.getElementById(item.id).onfocus = function() < showHelp(item.help); >; > > setupHelp();
Масив HelpText описує три підказки для трьох полів введення. Цикл пробігає ці описи по черзі і для кожного з полів введення визначає, що при виникненні події нафокусі для цього елемента повинна викликатися функція, що показує відповідну підказку.
Якщо ви запустите цей код, то побачите, що він працює не так, як ми очікуємо інтуїтивно. Яке поле ви б не вибрали, як підказка завжди буде висвічуватися повідомлення про вік.
Проблема в тому, що функції, присвоєні як обробники події нафокус, є замикання. Вони складаються з опису функції та контексту виконання (оточення), успадкованого від функції setupHelp. Було створено три замикання, але всі вони були створені з тим самим контекстом виконання. До моменту виникнення події нафокус цикл вже давно відпрацював, а значить, змінна item (одна і та ж для всіх трьох замикань) вказує на останній елемент масиву, який якраз у полі віку.
Як рішення в цьому випадку можна запропонувати використання функції, фабричної функції (function factory), як було описано вище в прикладах:
function showHelp(help) < document.getElementById("help").innerHTML = help; >function makeHelpCallback(help) < return function () < showHelp(help); >; > function setupHelp() < var helpText = [ < id: "email", help: "Ваша адреса e-mail" >, < id: "name", help: "Ваше повне ім'я" >, < id: "age" , help: "Ваш вік (Вам має бути більше 16)" >,]; for (var i = 0; i < helpText.length; i++) < var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); >> setupHelp();
Ось це працює як слід. Замість того, щоб ділити на всіх одне оточення, функція makeHelpCallback створює кожному із замикань своє власне, в якому змінна item вказує на правильний елемент масиву helpText .
Міркування щодо продуктивності
Не потрібно без необхідності створювати функції всередині функцій у випадках, коли замикання не потрібні. Використання цієї техніки збільшує вимоги до продуктивності як у частині швидкості, і у частині споживання пам'яті.
Як приклад, при написанні нового класу є сенс поміщати всі методи у прототип його об'єкта, а не описувати їх у тексті конструктора. Якщо зробити по-іншому, то при кожному створенні об'єкта для нього буде створено свій екземпляр кожного з методів замість того, щоб успадковувати їх з прототипу.
Давайте розглянемо не дуже практичний, але показовий приклад:
function MyObject(name, message) <this.name=name.toString(); this.message = message.toString(); this.getName = function() < return this.name; >; this.getMessage = function() < return this.message; >; >
Оскільки наведений вище код ніяк не використовує переваги замикань, його можна переписати наступним чином:
function MyObject(name, message) <this.name=name.toString(); this.message = message.toString(); >MyObject.prototype = < getName: function () < return this.name; >, getMessage: function () < return this.message; >, >;
Методи винесено до прототипу.Тим не менш, перевизначати прототип — саме по собі є поганою звичкою, тому перепишемо все так, щоб нові методи просто додалися до вже існуючого прототипу.
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; >;
Код вище можна зробити акуратнішим:
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);
В обох прикладах вище методи визначаються один раз у прототипі. І всі об'єкти, які використовують цей прототип, використовуватимуть це визначення без додаткової витрати обчислювальних ресурсів. Детальний опис у статті Детальніше про об'єктну модель.
