aboutsummaryrefslogtreecommitdiff
path: root/files/ru/web/javascript/closures/index.html
blob: 5d8d80d292bf6657dd00174901710c886d72a085 (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
---
title: Замыкания
slug: Web/JavaScript/Closures
tags:
  - ES5
  - Замыкание
translation_of: Web/JavaScript/Closures
---
<p>{{jsSidebar("Intermediate")}}</p>

<p class="summary">Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определена. Другими словами, замыкание даёт вам доступ к <a href="/ru/docs/Glossary/Scope">Scope</a> внешней функции из внутренней функции. В JavaScript замыкания создаются каждый раз при создании функции, во время её создания.</p>

<h2 id="Лексическая_область_видимости" style="margin-bottom: 20px; line-height: 30px; font-size: 2.14285714285714rem;">Лексическая область видимости</h2>

<p>Рассмотрим следующий пример:</p>

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

<p><code style="font-size: 14px;">init()</code><span style="line-height: 1.572;"> создаёт локальную переменную </span><code style="font-size: 14px;">name</code><span style="line-height: 1.572;"> и определяет функцию </span><code style="font-size: 14px;">displayName()</code><span style="line-height: 1.572;">. </span><code style="font-size: 14px;">displayName()</code><span style="line-height: 1.572;"> — это внутренняя функция </span><span style="line-height: 1.572;">— она определена внутри </span><code style="font-size: 14px;">init()</code><span style="line-height: 1.572;"> и доступна только внутри тела функции </span><code style="font-size: 14px;">init()</code><span style="line-height: 1.572;">. Обратите внимание, что функция</span> <code style="font-size: 14px;">displayName()</code><span style="line-height: 1.572;"> не имеет никаких собственных локальных переменных. Однако, поскольку внутренние функции имеют доступ к переменным внешних функций, </span><code style="font-size: 14px;">displayName()</code><span style="line-height: 1.572;"> может иметь доступ к переменной </span><code style="font-size: 14px;">name</code><span style="line-height: 1.572;">, объявленной в родительской функции </span><code style="font-size: 14px;">init()</code><span style="line-height: 1.572;">.</span></p>

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

<p><a href="http://jsfiddle.net/xAFs9/3/">Выполните</a> этот код и обратите внимание, что команда <code>alert()</code>  внутри <code>displayName()</code> благополучно выводит на экран содержимое переменной <code>name</code> объявленной в родительской функции. Это пример так называемой лексической области видимости <em>(lexical</em> <em>scoping)</em>: в JavaScript область действия переменной определяется по её расположению в коде (это очевидно <em>лексически</em>), и вложенные функции имеют доступ к переменным, объявленным вовне. Этот механизм и называется Lexical scoping (область действия, ограниченная лексически).</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" будет показана в JavaScript alert диалоге. Что отличает этот код и представляет для нас интерес, так это то, что внутренняя функция <code>displayName()</code> была возвращена из внешней до того, как была выполнена.</p>

<p>На первый взгляд, кажется неочевидным, что этот код правильный, но он работает. В некоторых языках программирования локальные переменные-функции существуют только во время выполнения этой функции. После завершения выполнения <code>makeFunc()</code> можно ожидать, что переменная <em>name</em> больше не будет доступна. Однако, поскольку код продолжает нормально работать, очевидно, что это не так в случае JavaScript.</p>

<p>Причина в том, что функции в JavaScript формируют так называемые <em>замыкания</em>. <em>Замыкание </em>— это комбинация функции и лексического окружения, в котором эта функция была объявлена. Это окружение состоит из произвольного количества локальных переменных, которые были в области действия функции во время создания замыкания. В рассмотренном примере <code>myFunc</code> — это ссылка на экземпляр функции <code>displayName</code>, созданной в результате выполнения <code>makeFunc</code>. Экземпляр функции <code>displayName</code> в свою очередь сохраняет ссылку на своё лексическое окружение, в котором есть переменная <code>name</code>.  По этой причине, когда происходит вызов функции <code>myFunc</code>, переменная <code>name</code> остаётся доступной для использования и сохранённый в ней текст "Mozilla" передаётся в <code>alert</code>.</p>

<p>А вот немного более интересный пример — функция <code>makeAdder</code>:</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, основано на обработке событий. Мы описываем какое-то поведение, а потом связываем его с событием, которое создаётся пользователем (например, клик мышкой или нажатие клавиши). При этом наш код обычно привязывается к событию в виде обратного/ответного вызова (callback): <em>callback функция - функция выполняемая в ответ на возникновение события</em>.</p>

<p>Давайте рассмотрим практический пример: допустим, мы хотим добавить на страницу несколько кнопок, которые будут менять размер текста. Как вариант, мы можем указать свойство font-size на элементе body в пикселах, а затем устанавливать размер прочих элементов страницы (таких, как заголовки) с использованием относительных единиц em:</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>Тогда наши кнопки будут менять свойство font-size элемента body, а остальные элементы страницы просто получат это новое значение и отмасштабируют размер текста благодаря использованию относительных единиц.</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/7726/","","200")}}</p>

<h2 id="Эмуляция_частных_private_методов_с_помощью_замыканий">Эмуляция частных (private) методов с помощью замыканий</h2>

<p>Языки вроде Java позволяют нам объявлять частные (private) методы . Это значит, что они могут быть вызваны только методами того же класса, в котором объявлены.</p>

<p>JavaScript не имеет встроенной возможности сделать такое, но это можно эмулировать с помощью замыкания. Частные методы полезны не только тем, что ограничивают доступ к коду, это также мощное средство глобальной организации пространства имён, позволяющее не засорять публичный интерфейс вашего кода внутренними методами классов.</p>

<p>Код ниже иллюстрирует, как можно использовать замыкания для определения публичных функций, которые имеют доступ к закрытым от пользователя (private) функциям и переменным. Такая манера программирования называется <a class="external" href="http://www.google.com/search?q=javascript+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;
    }
  };
})();

alert(Counter.value()); /* Alerts 0 */
Counter.increment();
Counter.increment();
alert(Counter.value()); /* Alerts 2 */
Counter.decrement();
alert(Counter.value()); /* Alerts 1 */
</pre>

<p>Тут много чего поменялось. В предыдущем примере каждое замыкание имело свой собственный контекст исполнения (окружение). Здесь мы создаём единое окружение для трёх функций: <code>Counter.increment</code>, <code>Counter.decrement</code>, и <code>Counter.value</code>.</p>

<p>Единое окружение создаётся в теле анонимной функции, которая исполняется в момент описания. Это окружение содержит два приватных элемента: переменную <code>privateCounter</code> и функцию <code>changeBy(val)</code>. Ни один из этих элементов не доступен напрямую, за пределами этой самой анонимной функции. Вместо этого они могут и должны использоваться тремя публичными функциями, которые возвращаются анонимным блоком кода (anonymous wrapper), выполняемым в той же анонимной функции.</p>

<p>Эти три публичные функции являются замыканиями, использующими общий контекст исполнения (окружение). Благодаря механизму lexical scoping в Javascript, все они имеют доступ к переменной <code>privateCounter</code> и функции <code>changeBy</code>.</p>

<p>Заметьте, мы описываем анонимную функцию, создающую счётчик, и тут же запускаем её, присваивая результат исполнения переменной <code>Counter</code>. Но мы также можем не запускать эту функцию сразу, а сохранить её в отдельной переменной, чтобы использовать для дальнейшего создания нескольких счётчиков вот так:</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>Заметьте, что счётчики работают независимо друг от друга. Это происходит потому, что у каждого из них в момент создания функцией <code><span style="font-family: consolas,monaco,andale mono,monospace;">makeCounter()</span></code> также создавался свой отдельный контекст исполнения (окружение). То есть приватная переменная <code>privateCounter </code>в каждом из счётчиков это действительно отдельная, самостоятельная переменная.</p>

<p>Используя замыкания подобным образом, вы получаете ряд преимуществ, обычно ассоциируемых с объектно-ориентированным программированием, таких как изоляция и инкапсуляция.</p>

<h2 id="Создание_замыканий_в_цикле_Очень_частая_ошибка">Создание замыканий в цикле: Очень частая ошибка</h2>

<p>До того, как в версии ECMAScript 6 ввели ключевое слово <a href="/en-US/docs/JavaScript/Reference/Statements/let" title="let"><code>let</code></a>, постоянно возникала следующая проблема при создании замыканий внутри цикла. Рассмотрим пример:</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': 'Ваш адрес e-mail'},
      {'id': 'name', 'help': 'Ваше полное имя'},
      {'id': 'age', 'help': 'Ваш возраст (Вам должно быть больше 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/13026/", "", 200)}}</p>

<p>Массив <code>helpText</code> описывает три подсказки для трёх полей ввода. Цикл пробегает эти описания по очереди и для каждого из полей ввода определяет, что при возникновении события <code>onfocus</code> для этого элемента должна вызываться функция, показывающая соответствующую подсказку.</p>

<p>Если вы запустите этот код, то увидите, что он работает не так, как мы ожидаем интуитивно. Какое поле вы бы ни выбрали, в качестве подсказки всегда будет высвечиваться сообщение о возрасте. </p>

<p>Проблема в том, что функции, присвоенные как обработчики события <code>onfocus</code>, являются замыканиями. Они состоят из описания функции и контекста исполнения (окружения), унаследованного от  функции <code>setupHelp</code>. Было создано три замыкания, но все они были созданы с одним и тем же контекстом исполнения. К моменту возникновения события <code>onfocus</code> цикл уже давно отработал, а значит, переменная <code>item</code> (одна и та же для всех трёх замыканий) указывает на последний элемент массива, который как раз в поле возраста.</p>

<p>В качестве решения в этом случае можно предложить использование функции, фабричной функции (function factory), как уже было описано выше в примерах:</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': 'Ваш адрес e-mail'},
      {'id': 'name', 'help': 'Ваше полное имя'},
      {'id': 'age', 'help': 'Ваш возраст (Вам должно быть больше 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/13024/", "", 200)}}</p>

<p>Вот это работает как следует. Вместо того, чтобы делить на всех одно окружение, функция <code>makeHelpCallback</code> создаёт каждому из замыканий своё собственное, в котором переменная <code>item</code> указывает на правильный элемент массива <code>helpText</code>.</p>

<h2 id="Соображения_по_производительности">Соображения по производительности</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  language-js notranslate" style="padding: 1em 0px 1em 30px; font-size: 14px; white-space: normal;"><code class="language-js" style="direction: ltr; white-space: pre;"><span class="keyword token" style="color: #0077aa;">function</span> <span class="function token" style="color: #dd4a68;">MyObject<span class="punctuation token" style="color: #999999;">(</span></span>name<span class="punctuation token" style="color: #999999;">,</span> message<span class="punctuation token" style="color: #999999;">)</span> <span class="punctuation token" style="color: #999999;">{</span>
    <span class="keyword token" style="color: #0077aa;">this</span><span class="punctuation token" style="color: #999999;">.</span>name <span class="operator token" style="background: rgba(255, 255, 255, 0.498039); color: #a67f59;">=</span> name<span class="punctuation token" style="color: #999999;">.</span><span class="function token" style="color: #dd4a68;">toString<span class="punctuation token" style="color: #999999;">(</span></span><span class="punctuation token" style="color: #999999;">)</span><span class="punctuation token" style="color: #999999;">;</span>
    <span class="keyword token" style="color: #0077aa;">this</span><span class="punctuation token" style="color: #999999;">.</span>message <span class="operator token" style="background: rgba(255, 255, 255, 0.498039); color: #a67f59;">=</span> message<span class="punctuation token" style="color: #999999;">.</span><span class="function token" style="color: #dd4a68;">toString<span class="punctuation token" style="color: #999999;">(</span></span><span class="punctuation token" style="color: #999999;">)</span><span class="punctuation token" style="color: #999999;">;</span>
<span class="punctuation token" style="color: #999999;">}</span>
<span class="punctuation token" style="color: #999999;">(</span><span class="keyword token" style="color: #0077aa;">function</span><span class="punctuation token" style="color: #999999;">(</span><span class="punctuation token" style="color: #999999;">)</span> <span class="punctuation token" style="color: #999999;">{</span>
    <span class="keyword token" style="color: #0077aa;">this</span><span class="punctuation token" style="color: #999999;">.</span>getName <span class="operator token" style="background: rgba(255, 255, 255, 0.498039); color: #a67f59;">=</span> <span class="keyword token" style="color: #0077aa;">function</span><span class="punctuation token" style="color: #999999;">(</span><span class="punctuation token" style="color: #999999;">)</span> <span class="punctuation token" style="color: #999999;">{</span>
        <span class="keyword token" style="color: #0077aa;">return</span> <span class="keyword token" style="color: #0077aa;">this</span><span class="punctuation token" style="color: #999999;">.</span>name<span class="punctuation token" style="color: #999999;">;</span>
    <span class="punctuation token" style="color: #999999;">}</span><span class="punctuation token" style="color: #999999;">;</span>
    <span class="keyword token" style="color: #0077aa;">this</span><span class="punctuation token" style="color: #999999;">.</span>getMessage <span class="operator token" style="background: rgba(255, 255, 255, 0.498039); color: #a67f59;">=</span> <span class="keyword token" style="color: #0077aa;">function</span><span class="punctuation token" style="color: #999999;">(</span><span class="punctuation token" style="color: #999999;">)</span> <span class="punctuation token" style="color: #999999;">{</span>
        <span class="keyword token" style="color: #0077aa;">return</span> <span class="keyword token" style="color: #0077aa;">this</span><span class="punctuation token" style="color: #999999;">.</span>message<span class="punctuation token" style="color: #999999;">;</span>
    <span class="punctuation token" style="color: #999999;">}</span><span class="punctuation token" style="color: #999999;">;</span>
<span class="punctuation token" style="color: #999999;">}</span><span class="punctuation token" style="color: #999999;">)</span><span class="punctuation token" style="color: #999999;">.</span><span class="function token" style="color: #dd4a68;">call<span class="punctuation token" style="color: #999999;">(</span></span>MyObject<span class="punctuation token" style="color: #999999;">.</span>prototype<span class="punctuation token" style="color: #999999;">)</span><span class="punctuation token" style="color: #999999;">;</span></code></pre>

<p>В обоих примерах выше методы определяются один раз — в прототипе. И все объекты, использующие данный прототип, будут использовать это определение без дополнительного расхода вычислительных ресурсов. Смотрите подробное описание в статье <a href="/ru/docs/Web/JavaScript/Guide/Details_of_the_Object_Model">Подробнее об объектной модели</a>.</p>

<div>{{PreviousNext("Web/JavaScript/Equality_comparisons_and_sameness")}}</div>