Підручник із закриття JavaScript - З прикладом коду закриття JS

Закриття - багато з вас, розробники JavaScript, напевно, вже чули цей термін раніше. Коли я починав свою подорож з JavaScript, я часто стикався із закриттям. І я думаю, що це одне з найважливіших та найцікавіших понять у JavaScript.

Ви не думаєте, що вони цікаві? Це часто трапляється, коли ти не розумієш поняття - ти не вважаєш його цікавим. (Я не знаю, трапляється це з вами чи ні, але це стосується мене).

Тож у цій статті я спробую зробити закриття цікавими для вас.

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

Лексичний обсяг

Можливо, ви думаєте - я знаю місцевий та глобальний обсяг, але який біс лексичний? Я реагував так само, коли чув цей термін. Щоб не хвилюватися! Давайте розглянемо уважніше.

Це просто, як і два інших сфери застосування:

function greetCustomer() { var customerName = "anchal"; function greetingMsg() { console.log("Hi! " + customerName); // Hi! anchal } greetingMsg(); }

З вищенаведеного виходу видно, що внутрішня функція може отримати доступ до змінної зовнішньої функції. Це лексичний обсяг, де область дії та значення змінної визначається тим, де вона визначена / створена (тобто її положення в коді). Зрозумів?

Я знаю, що останній біт міг вас збентежити. Тож дозвольте заглибити вас глибше. Чи знали ви, що лексичний масштаб також відомий як статичний масштаб ? Так, це інша його назва.

Існує також динамічне масштабування , яке підтримують деякі мови програмування. Чому я згадав про динамічне масштабування? Тому що це може допомогти вам краще зрозуміти лексичний обсяг.

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

function greetingMsg() { console.log(customerName);// ReferenceError: customerName is not defined } function greetCustomer() { var customerName = "anchal"; greetingMsg(); } greetCustomer();

Чи погоджуєтесь ви з результатами? Так, це дасть посилання на помилку. Це пояснюється тим, що обидві функції не мають доступу до сфери дії одна одної, оскільки вони визначені окремо.

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

function addNumbers(number1) { console.log(number1 + number2); } function addNumbersGenerate() { var number2 = 10; addNumbers(number2); } addNumbersGenerate();

Наведений вище результат буде 20 для мови з динамічним масштабом. Мови, які підтримують лексичний обсяг дадутьreferenceError: number2 is not defined. Чому?

Оскільки при динамічному масштабуванні пошук спочатку відбувається в локальній функції, потім він переходить у функцію, яка викликала цю локальну функцію. Потім здійснюється пошук функції, яка викликала цю функцію, і так далі, вгору по стеку викликів.

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

Отримали суть динамічного масштабування? Якщо так, тоді просто пам’ятайте, що лексичний обсяг - це його протилежність.

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

Отже, лексичний або статичний обсяг означає, що обсяг і значення змінної визначаються звідки вона визначена. Це не змінюється.

Давайте ще раз розглянемо наведений вище приклад і спробуємо самостійно з’ясувати результат. Лише один поворот - заявіть number2угорі:

var number2 = 2; function addNumbers(number1) { console.log(number1 + number2); } function addNumbersGenerate() { var number2 = 10; addNumbers(number2); } addNumbersGenerate(); 

Чи знаєте ви, яким буде результат?

Правильно - це 12 для мов з лексичним розмахом. Це тому, що спочатку він переглядає addNumbersфункцію (найпотаємніший обсяг), потім здійснює пошук всередину, де ця функція визначена. Отримавши number2змінну, означає вихід 12.

Можливо, вам цікаво, чому я витратив стільки часу на лексичний обсяг тут. Це закриття статті, а не про лексичний обсяг. Але якщо ви не знаєте про лексичний обсяг, тоді ви не зрозумієте закриття.

Чому? Ви отримаєте свою відповідь, коли ми розглянемо визначення закриття. Тож давайте зайдемо на колію і повернемося до закриття.

Що таке закриття?

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

Закриття створюється, коли внутрішня функція має доступ до своїх зовнішніх змінних функції та аргументів. Внутрішня функція має доступ до -

1. Власні змінні.

2. Змінні та аргументи зовнішньої функції.

3. Глобальні змінні.

Чекай! Це визначення закриття чи лексичного обсягу? Обидва визначення виглядають однаково. Чим вони різні?

Ну, ось чому я визначив лексичний обсяг вище. Оскільки закриття пов’язані з лексичним / статичним обсягом.

Давайте ще раз розглянемо інше його визначення, яке покаже вам, чим відрізняються закриття.

Закриття - це коли функція може отримати доступ до своєї лексичної сфери, навіть коли ця функція виконується поза її лексичною сферою.

Або,

Внутрішні функції можуть отримати доступ до своєї батьківської області, навіть після того, як батьківська функція вже виконана.

Розгублений? Не хвилюйтеся, якщо ви ще не зрозуміли суть справи. У мене є приклади, які допоможуть вам краще зрозуміти. Давайте модифікуємо перший приклад лексичного обсягу:

function greetCustomer() { const customerName = "anchal"; function greetingMsg() { console.log("Hi! " + customerName); } return greetingMsg; } const callGreetCustomer = greetCustomer(); callGreetCustomer(); // output – Hi! anchal

Різниця в цьому коді полягає в тому, що ми повертаємо внутрішню функцію і виконуємо її пізніше. У деяких мовах програмування локальна змінна існує під час виконання функції. Але після виконання функції ці локальні змінні не існують, і вони не будуть доступними.

Однак тут сцена інша. Після виконання батьківської функції внутрішня функція (повернена функція) все ще може отримати доступ до змінних батьківської функції. Так, ви правильно вгадали. Причина - закриття.

Внутрішня функція зберігає свій лексичний обсяг, коли виконується батьківська функція, отже, пізніше ця внутрішня функція може отримати доступ до цих змінних.

Щоб краще відчути це, давайте скористаємося dir()методом консолі, щоб вивчити перелік властивостей callGreetCustomer:

console.dir(callGreetCustomer);

З наведеного вище зображення видно, як внутрішня функція зберігає батьківський обсяг ( customerName) при greetCustomer()виконанні. А пізніше він використовувався, customerNameколи callGreetCustomer()був страчений.

Сподіваюся, цей приклад допоміг вам краще зрозуміти наведене вище визначення закриття. І, можливо, зараз ви знайдете закриття трохи веселішим.

То що далі? Давайте зробимо цю тему цікавішою, розглядаючи різні приклади.

Приклади закриття в дії

function counter() { let count = 0; return function() { return count++; }; } const countValue = counter(); countValue(); // 0 countValue(); // 1 countValue(); // 2

Щоразу, коли ви телефонуєте countValue, значення змінної count збільшується на 1. Зачекайте - чи думали ви, що значення count дорівнює 0?

Well, that would be wrong as a closure doesn’t work with a value. It stores the reference of the variable. That’s why, when we update the value, it reflects in the second or third call and so on as the closure stores the reference.

Feeling a bit clearer now? Let’s look at another example:

function counter() { let count = 0; return function () { return count++; }; } const countValue1 = counter(); const countValue2 = counter(); countValue1(); // 0 countValue1(); // 1 countValue2(); // 0 countValue2(); // 1 

I hope you guessed the right answer. If not, here is the reason. As countValue1 and countValue2, both preserve their own lexical scope. They have independent lexical environments. You can use dir() to check the [[scopes]] value in both the cases.

Let’s look at a third example.

This one's a bit different. In it, we have to write a function to achieve the output:

const addNumberCall = addNumber(7); addNumberCall(8) // 15 addNumberCall(6) // 13

Simple. Use your newly-gained closure knowledge:

function addNumber(number1) { return function (number2) { return number1 + number2; }; }

Now let’s look at some tricky examples:

function countTheNumber() { var arrToStore = []; for (var x = 0; x < 9; x++) { arrToStore[x] = function () { return x; }; } return arrToStore; } const callInnerFunctions = countTheNumber(); callInnerFunctions[0]() // 9 callInnerFunctions[1]() // 9

Every array element that stores a function will give you an output of 9. Did you guess right? I hope so, but still let me tell you the reason. This is because of the closure's behavior.

The closure stores the reference, not the value. The first time the loop runs, the value of x is 0. Then the second time x is 1, and so on. Because the closure stores the reference, every time the loop runs it's changing the value of x. And at last, the value of x will be 9. So callInnerFunctions[0]() gives an output of 9.

But what if you want an output of 0 to 8? Simple! Use a closure.

Think about it before looking at the solution below:

function callTheNumber() { function getAllNumbers(number) { return function() { return number; }; } var arrToStore = []; for (var x = 0; x < 9; x++) { arrToStore[x] = getAllNumbers(x); } return arrToStore; } const callInnerFunctions = callTheNumber(); console.log(callInnerFunctions[0]()); // 0 console.log(callInnerFunctions[1]()); // 1

Here, we have created separate scope for each iteration. You can use console.dir(arrToStore) to check the value of x in [[scopes]] for different array elements.

That’s it! I hope you can now say that you find closures interesting.

To read my other articles, check out my profile here.