aboutsummaryrefslogtreecommitdiff
path: root/files/uk/web/javascript/closures/index.html
blob: 1abe835739c0b7759c741342dc0a15d72b4a472b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
---
title: Closures
slug: Web/JavaScript/Closures
tags:
  - Замикання
translation_of: Web/JavaScript/Closures
---
<div>{{jsSidebar("Intermediate")}}</div>

<p class="summary">Замикання - це функції, що посилаються на незалежні (вільні) змінні (змінні, які використовуються локально, але визначені в обмеженій області видимості). Іншими словами, ці функції "пам'ятають" середовище, в якому вони були створені.</p>

<h2 id="Лексичне_середовище">Лексичне середовище</h2>

<p>Розглянемо наступне:</p>

<div>
<pre class="brush: js notranslate">function init() {
  var name = "Mozilla"; // name - це локальна змінна, створена в функції init
  function displayName() { // displayName() - це внутрішня функція, замикання
    alert(name); // використовує змінну, оголошену в батьківській функції
  }
  displayName();
}
init();</pre>
</div>

<p><code>init()</code> створює локальну змінну <code>name</code> та функцію <code>displayName()</code>. <code>displayName()</code> є вкладеною функцією, що оголошена всередині <code>init()</code> та є доступною тільки в тілі цієї функції. <code>displayName()</code> не має локальних змінних, проте вкладені функції мають доступ до змінних зовнішніх функцій саме тому <code>displayName()</code> може використовувати імена змінних, оголошених в батьківській функції <code>init()</code>.</p>

<p>{{JSFiddleEmbed("https://jsfiddle.net/78dg25ax/", "js,result", 200)}}</p>

<p><a href="http://jsfiddle.net/xAFs9/3/" title="http://jsfiddle.net/xAFs9/">Запустіть</a> цей код і побачите, що alert() всередині  функції displayName() успішно відображає змінну name, оголошену в батьківській функції. Це є прикладом лексичного середовища (lexical environment), яке описує, яким чином парсер звертається до змінних, коли функції вкладені. Слово <em>лексичне</em>  посилається на факт, що лексичне середовище використовує розташування, де в коді  була створена змінна задля визначення середовища, де ця змінна доступна. Вкладені функції мають доступ до змінних, оголошених у їх зовнішній області.</p>

<h2 id="Замикання">Замикання</h2>

<p>Тепер розглянемо наступний приклад:</p>

<pre class="brush: js notranslate">function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();
</pre>

<p>Результат запуску цього коду буде таким самим, як і в попередньому прикладі функції <code>init()</code>: рядок  "Mozilla" буде відображений в спливаючому повідомленні <font face="consolas, Liberation Mono, courier, monospace">alert</font>(). Цікавою відмінністю є те, що внутрішня функція <code>displayName() </code>буде повернена з зовнішньої функції перш ніж вона буде виконана. </p>

<p>Той факт, що цей код досі працює, може здаватись неочевидним. В деяких мовах програмування локальні змінні існують лише протягом часу виконання функції. Як тільки <code>makeFunc()</code> завершила своє виконання, слід очікувати, що змінна <font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: #eeeeee;">name</span></font> не буде більше існувати. Однак у JavaScript цей підхід інакший, оскільки даний код працює, як і очікувалось.</p>

<p>Це відбувається тому що ці функції в JavaScript утворюють замикання. <em>Замикання</em> це комбінація функції і лексичного середовища (або просто середовища), всередині якого функція була оголошена. Середовище складається з будь-яких локальних змінних, які були в області видимості в той час, коли замикання було створене. В цьому випадку, <code>myFunc</code> є посиланням на екземпляр функції <font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: #eeeeee;">displayName</span></font> створений, коли <font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: #eeeeee;">makeFunc</span></font> була виконана. Екземпляр <code>displayName</code> включає посилання на власне лексичне середовище в середині якого існує змінна <code>name</code>. З цієї причини, коли <code>myFunc</code> викликається, змінна <code>name</code> залишається доступною для використання і "Mozilla" передається до <code>alert</code>. ​​​​​​</p>

<p>Ось трохи цікавіший приклад — a <code>makeAdder</code> function:</p>

<pre class="brush: js notranslate">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
</pre>

<p>У цьому прикладі ми визначили функцію <code>makeAdder(x),</code> яка приймає єдиний аргумент <code>x</code> і повертає нову функцію. Функція, яку вона повертає, приймає єдиний аргумент <code>y</code>, і повертає суму  <code>x</code> і <code>y</code>.</p>

<p>По суті, <code>makeAdder</code> це фабрика функцій — вона створює функції, які можуть додавати певне значення до свого аргументу. У наведеному вище прикладі ми використовуємо нашу фабричну функцію для створення двох нових функцій — одна додає 5 до свого аргументу, і інша додає 10.</p>

<p><code>add5</code> і <code>add10</code> є прикладами замикання. Вони поділяють одне визначення тіла функції, але зберігають різні лексичні середовища. В лексичному середовищі <code>add5</code>  <code>x</code> - це 5, а в лексичному середовищі <code>add10</code> <code>x</code>- це 10.</p>

<h2 id="Замикання_на_практиці">Замикання на практиці</h2>

<p>Замикання корисні, оскільки вони дозволяють зв'язати деякі дані (лексичне середовище) з функцією, яка працює з цими даними. Це має очевидні паралелі з об'єктно-орієнтованим програмуванням, де об'єкти дозволяють нам зв'язати деякі дані (властивості об'єкта) з одним або декількома методами.</p>

<p>Отже, замикання можна використовувати всюди, де  зазвичай використовується об'єкт з одним єдиним методом. </p>

<p>Ситуації, в яких ви можливо захочете зробити це, особливо поширені в web-розробці. Велика частина front-end коду, написана на JavaScript, основана на обробці подій. Ви визначаєте деяку поведінку, а потім прикріплюєте її до події, яка запускається користувачем (наприклад, клік або натискання клавіші). Код, як правило, прив'язується до події як зворотній виклик <strong>(callback)</strong> - функція, яка виконується у відповідь на виникнення події.</p>

<p>Наприклад припустимо, що ми хочемо додати кілька кнопок на сторінку, які змінюватимуть розмір тексту. Один із способів зробити це - вказати <code>font-size</code> елементу <code>body</code> у пікселях, а потім встановити розмір інших елементів на сторінці (таких як заголовки) за допомогою відносних одиниць виміру <code>em</code>:</p>

<pre class="brush: css notranslate">body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}
</pre>

<p>Такі інтерактивні кнопки розміру тексту можуть змінювати властивість <code>font-size</code> елемента <code>body</code>, і налаштування будуть підхоплені іншими елементами на сторінці завдяки відносним одиницям.</p>

<p>Ось JavaScript:</p>

<pre class="brush: js notranslate">function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
</pre>

<p> Тепер <code>size12</code>, <code>size14</code>, і <code>size16</code> є функціями, які будуть змінювати розмір тексту елемента body відповідно до 12, 14 та 16 пікселів. Ми можемо прив'язати їх до кнопок таким чином:</p>

<pre class="brush: js notranslate">document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
</pre>

<pre class="brush: html notranslate">&lt;a href="#" id="size-12"&gt;12&lt;/a&gt;
&lt;a href="#" id="size-14"&gt;14&lt;/a&gt;
&lt;a href="#" id="size-16"&gt;16&lt;/a&gt;
</pre>

<p>{{JSFiddleEmbed("https://jsfiddle.net/vnkuZ/","","200")}}</p>

<h2 id="Емуляція_приватних_методів_з_замиканнями">Емуляція приватних методів з замиканнями</h2>

<p>В таких мовах як Java є можливість об'являти методи приватними, що означає їх здатність бути викликаними лише іншими методами того ж класу.  </p>

<p>JavaScript не має власного способу зробити це, але є можливість емуляції приватних методів використовуючи замикання. Приватні методи корисні не лише для обмеження доступу до коду: вони також забезпечують потужний спосіб управління глобальним простором імен, зберігаючи несуттєві методи від захаращення публічного інтерфейсу до вашого коду.</p>

<p>Ось як визначити деякі загальнодоступні функції, які можуть отримати доступ до приватних функцій та змінних, використовуючи замикання, також відомі як модель модуля (<a class="external" href="http://www.google.com/search?q=javascript+module+pattern" title="http://www.google.com/search?q=javascript+module+pattern">module pattern</a>).</p>

<pre class="brush: js notranslate">var counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1
</pre>

<p>Тут багато чого відбувається. В попередніх пригладах кожне замикання мало своє власне оточення. Тут, однак, існує одне лексичне середовище, яке поділяють три функції: <code>counter.increment</code>, <code>counter.decrement</code>, and <code>counter.value</code>.</p>

<p>Спільне лексичне середовище створюється в тілі анонімної функції, яка викликається одразу після створення. Навколишнє середовище містить два приватні складові: змінну <code>privateCounter</code> і функцію  <code>changeBy</code>. Жодна із цих приватних складових не може бути доступна безпосередньо за межами анонімної функції. Натомість до них мають бути доступні три загальнодоступні функції, які повертаються з анонімної обгортки.</p>

<p>Ці три  функції - це замикання, які мають одне і те ж середовище. Завдяки лексичному оточенню JavaScript, кожна з них має доступ до змінної privateCounter та функції changeBy.</p>

<p>Ми визначаємо анонімну функцію, яка створює лічильник, а потім ми викликаємо її негайно і присвоюємо результат змінній лічильника.Ми можемо зберігати цю функцію в окремій змінній makeCounter і використовувати її для створення декількох лічильників</p>

<pre class="brush: js notranslate">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 */
</pre>

<p>Зауважте, як кожен з двох лічильників підтримує свою незалежність від іншого. Його середовище під час виклику функції makeCounter () щоразу відрізняється. Змінна замикання privateCounter містить кожен раз інший екземпляр.</p>

<p>Використання замикання таким чином забезпечує ряд переваг, які зазвичай пов'язані з об'єктно-орієнтованим програмуванням, зокрема приховування даних та інкапсуляція.</p>

<h2 id="Ланцюжок_замикань">Ланцюжок замикань</h2>

<p>Кожне замикання має три оточення:</p>

<ul>
 <li>Local Scope (власне оточення)</li>
 <li>Outer Functions Scope (зовнішнє функціональне оточення)</li>
 <li>Global Scope (глобальне оточення)</li>
</ul>

<p>Поширеною помилкою є не усвідомлення того, що у випадку, коли зовнішня функція сама є вкладеною функцією, доступ до оточення її зовнішньої функції включає вкладене оточення  її як зовнішньої функції - ефективно створюючи ланцюг областей оточення. Для демонстрації розглянемо наступний приклад коду.</p>

<pre class="notranslate">// global scope
var e = 10;
function sum(a){
  return function(b){
    return function(c){
      // outer functions scope
      return function(d){
        // local scope
        return a + b + c + d + e;
      }
    }
  }
}

console.log(sum(1)(2)(3)(4)); // log 20

// You can also write without anonymous functions:

// global scope
var e = 10;
function sum(a){
  return function sum2(b){
    return function sum3(c){
      // outer functions scope
      return function sum4(d){
        // local scope
        return a + b + c + d + e;
      }
    }
  }
}

var s = sum(1);
var s1 = s(2);
var s2 = s1(3);
var s3 = s2(4);
console.log(s3) //log 20
</pre>

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

<h2 id="Creating_closures_in_loops_A_common_mistake">Creating closures in loops: A common mistake</h2>

<p>До введення ключового слова let у ECMAScript 2015, загальна проблема із замиканням виникала, коли вони створювалися всередині циклу. Розглянемо наступний приклад: </p>

<pre class="brush: html notranslate">&lt;p id="help"&gt;Helpful notes will appear here&lt;/p&gt;
&lt;p&gt;E-mail: &lt;input type="text" id="email" name="email"&gt;&lt;/p&gt;
&lt;p&gt;Name: &lt;input type="text" id="name" name="name"&gt;&lt;/p&gt;
&lt;p&gt;Age: &lt;input type="text" id="age" name="age"&gt;&lt;/p&gt;
</pre>

<pre class="brush: js notranslate">function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i &lt; helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();
</pre>

<p>{{JSFiddleEmbed("https://jsfiddle.net/v7gjv/", "", 200)}}</p>

<p>Масив helpText визначає три корисні підказки, кожну пов'язану з ID поля Input. Цикл циклично перераховує ці визначення, підключаючи подію onfocus до кожного, що показує пов'язаний метод help.</p>

<p>Якщо ви спробуєте цей код, ви побачите, що він не працює, як очікувалося. Незалежно від того, на якому полі ви зосереджені, відображатиметься повідомлення про ваш вік.</p>

<p>Причиною цього є те, що функції, призначені для фокусування, - це замикання; вони складаються з визначення функції та захопленого оточення з області видимосты функцій setupHelp.  Три замикання були створені циклом, але кожне ділиться тим самим єдиним середовищем, яке має змінну зі змінними значеннями (item.help).</p>

<p>Коли виконуються зворотні виклики onfocus, доступ до item.help в цей момент викликає таку поведінку (дійсно, оскільки значення змінної доступно / обчислюється лише під час виконання), оскільки цикл до цього часу виконувався, а об'єкт змінної елемента (розділений усіма трьома замиканнями) був залишений, вказуючи на останній запис у списку довідкового тексту.</p>

<p>Одне рішення в цьому випадку - використовувати більше замикань: зокрема, використовувати фабрику функцій, як описано раніше:</p>

<pre class="brush: js notranslate">function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i &lt; helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();
</pre>

<p>{{JSFiddleEmbed("https://jsfiddle.net/v7gjv/1/", "", 300)}}</p>

<p>Це працює як очікувалося. Замість того, щоб зворотні виклики мали спільний доступ до одного середовища, функція makeHelpCallback створює нове середовище для кожного з них, в якій довідка посилається на відповідний рядок із масиву helpText.</p>

<p>Ще один спосіб написати вищезгадане - використання  анонімних  замикань:</p>

<pre class="brush: js notranslate">function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i &lt; helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
  }
}

setupHelp();</pre>

<p>Якщо ви не хочете більше використовувати замикання, можете скористатися ключовим словом let з ES6:</p>

<pre class="brush: js notranslate">function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i &lt; helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();</pre>

<p>Цей приклад використовує let замість var, таким чином кожне замикання прив'язує блочну  змінну,це означає, що додаткові замикання не потрібні.</p>

<p>Іншою альтернативою може бути використання forEach () для повторення масиву helpText та приєднання слухача до кожного &lt;p&gt;, як показано:</p>

<pre class="notranslate">function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  helpText.forEach(function(text) {
    document.getElementById(text.id).onfocus = function() {
      showHelp(text.help);
    }
  });
}

setupHelp();
</pre>

<h2 id="Performance_considerations">Performance considerations</h2>

<p>Нерозумно створювати функції в межах інших функцій, якщо замикання не потрібне для конкретного завдання, оскільки це негативно вплине на продуктивність сценарію як з точки зору швидкості обробки, так і споживання пам'яті.</p>

<p>Наприклад, при створенні нового об'єкта / класу методи, як правило, повинні бути пов'язані з прототипом об'єкта, а не визначені в конструкторі об'єктів. Причина полягає в тому, що щоразу, коли конструктор викликається, методи отримують перепризначення (тобто для кожного створення об'єкта).</p>

<p>Розглянемо наступний непрактичний, але показовий випадок:</p>

<pre class="brush: js notranslate">function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}
</pre>

<p>Попередній код не використовує переваги замикань, а тому може виглядати:</p>

<pre class="brush: js notranslate">function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};
</pre>

<p>Однак переробляти прототип не рекомендується, тому наступний приклад ще кращий, оскільки він додається до існуючого прототипу:</p>

<pre class="brush: js notranslate">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;
};
</pre>

<p>Код вище також може бути записаний чистіше з тим же результатом:</p>

<pre class="brush: js notranslate">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);
</pre>

<p>In the three previous examples, the inherited prototype can be shared by all objects and the method definitions need not occur at every object creation. See <a href="/en-US/docs/Web/JavaScript/Guide/Details_of_the_Object_Model">Details of the Object Model</a> for more details.  У трьох попередніх прикладах успадкований прототип може бути спільним для всіх об'єктів, і визначення методів не повинно виникати при кожному створенні об'єкта. Докладніше див. Деталі Об'єктної Моделі (<a href="/en-US/docs/Web/JavaScript/Guide/Details_of_the_Object_Model">Details of the Object Model</a>).</p>