diff options
author | Peter Bengtsson <mail@peterbe.com> | 2020-12-08 14:42:52 -0500 |
---|---|---|
committer | Peter Bengtsson <mail@peterbe.com> | 2020-12-08 14:42:52 -0500 |
commit | 074785cea106179cb3305637055ab0a009ca74f2 (patch) | |
tree | e6ae371cccd642aa2b67f39752a2cdf1fd4eb040 /files/ru/web/javascript/closures | |
parent | da78a9e329e272dedb2400b79a3bdeebff387d47 (diff) | |
download | translated-content-074785cea106179cb3305637055ab0a009ca74f2.tar.gz translated-content-074785cea106179cb3305637055ab0a009ca74f2.tar.bz2 translated-content-074785cea106179cb3305637055ab0a009ca74f2.zip |
initial commit
Diffstat (limited to 'files/ru/web/javascript/closures')
-rw-r--r-- | files/ru/web/javascript/closures/index.html | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/files/ru/web/javascript/closures/index.html b/files/ru/web/javascript/closures/index.html new file mode 100644 index 0000000000..0febcbb623 --- /dev/null +++ b/files/ru/web/javascript/closures/index.html @@ -0,0 +1,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/" title="http://jsfiddle.net/xAFs9/">Выполните</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"><a href="#" id="size-12">12</a> +<a href="#" id="size-14">14</a> +<a href="#" id="size-16">16</a> +</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" title="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"><p id="help">Helpful notes will appear here</p> +<p>E-mail: <input type="text" id="email" name="email"></p> +<p>Name: <input type="text" id="name" name="name"></p> +<p>Age: <input type="text" id="age" name="age"></p> +</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 < 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 < 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> |