Статьи

Закрытие: спереди назад

Закрытия часто рассматриваются как тайное искусство в стране JavaScript. После освоения они позволяют вам написать действительно потрясающий JavaScript. Эта статья познакомит вас с магией JavaScript-замыканий.


Одна из ключевых истин JavaScript состоит в том, что все является объектом. Это, конечно, включает в себя функции.

Закрытие — это не что иное, как объект функции со связанной областью, в которой разрешаются переменные функции.

Закрытия получают свое имя из-за того, как они закрывают содержимое. Рассмотрим следующий фрагмент JavaScript:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
topping = «anchovi»;
function pizzaParty(numSlices) {
    var topping = «pepperoni»,
 
    innerFunction = function() {
        var topping = «ham»;
        console.log(» …..But put » + topping + » on » + numSlices + » slices»);
    };
 
    console.log(«This pizza is all about the » + topping);
 
    innerFunction();
}
pizzaParty(3);

Если вы откроете свою любимую консоль и запустите этого плохого парня, вас встретят с восхитительным сообщением о том, что «Эта пицца — это все о пепперони ….. Но положите ветчину на 3 ломтика». Этот пример иллюстрирует некоторые ключевые концепции JavaScript, которые имеют решающее значение для удержания замыканий.

Сколько функциональных объектов в приведенном выше коде? Ну … у нас есть функция pizzaParty , и в эту функцию innerFunction функция innerFunction . Математика не всегда была моей сильной стороной, но 1 + 1 = 2 в моей книге. Каждый объект функции имеет свой собственный набор переменных, которые разрешаются в области действия каждой функции.

Замыкания не могут быть полностью поняты без прочного обоснования в объеме. Механизм видимости JavaScript — это то, что позволяет каждой функции иметь свою собственную переменную topping , и без нее у нас может быть слишком много пепперони, слишком мало ветчины или * задыхание * … некоторые анчоусы на нашей вечеринке с пиццей. Давайте воспользуемся быстрой иллюстрацией, чтобы лучше проиллюстрировать эту идею.

Функции выполняются с использованием области действия, которая действовала, когда функция была определена. Это не имеет ничего общего с областью действия, действующей при вызове функции.

Зеленые стрелки показывают, что доступность работает извне. Переменные, определенные в области действия вне функции, доступны изнутри.

Если бы мы пропустили переменную topping внутри функции pizzaParty , то получили бы сообщение типа «Эта пицца — все о anchovi», но поскольку pizzaParty имеет переменную pizzaParty в своей области видимости; эти соленые присоски никогда не приблизятся к нашей вечеринке с пиццей.

Аналогично, к параметру numSlices можно получить доступ изнутри innerFunction потому что он определен в области видимости выше — в этом случае область видимости pizzaParty .

Красные стрелки показывают, что переменные в области видимости для функции никогда не доступны вне этой функции. Это имеет место только тогда, когда переменная удовлетворяет одному из следующих условий:

  1. Ключевое слово var используется.
  2. Переменная является параметром функции или внешней функцией.
  3. Переменная является вложенной функцией.

Пропуск ключевого слова var при установке переменной приведет к тому, что JavaScript установит ближайшую именованную переменную во внешних функциях вплоть до глобальной области видимости. Таким образом, в нашем примере доступ к pizzaParty ветчины в innerFunction невозможен из pizzaParty , а к pizzaParty пепперони в pizzaParty нельзя получить доступ в глобальной области видимости, где обитает анчоус.

Лексическая область действия означает, что функции выполняются с использованием переменной области действия, действующей на момент определения функции. Это не имеет ничего общего с областью действия, действующей при вызове функции. Этот факт имеет решающее значение для раскрытия силы замыканий.

Теперь, когда мы понимаем, что такое замыкание и что означает область действия для замыканий, давайте рассмотрим некоторые классические варианты использования.


Замыкания — это способ скрыть ваш код от всеобщего обозрения. С замыканиями вы можете легко иметь закрытых членов, которые защищены от внешнего мира:

01
02
03
04
05
06
07
08
09
10
11
12
(function(exports){
 
    function myPrivateMultiplyFunction(num,num2) {
        return num * num2;
    }
 
    //equivalent to window.multiply = function(num1,num2) { …
    exports.multiply = function(num1,num2) {
        console.log(myPrivateMultiplyFunction(num1,num2));
    }
 
})(window);

С замыканиями вы можете легко иметь частных членов, которые защищены от внешнего мира.

Давайте разберемся с этим. Наш объект функции верхнего уровня является анонимной функцией:

1
2
3
(function(exports){
     
})(window);

Мы немедленно вызываем эту анонимную функцию. Мы передаем ему глобальный контекст (в данном случае window ), чтобы мы могли «экспортировать» одну публичную функцию, но скрыть все остальное. Поскольку функция myPrivateMultiplyFunction является вложенной функцией, она существует только в рамках нашего замыкания; поэтому мы можем использовать его где угодно внутри этой области, и только в этой области.

JavaScript будет содержать ссылку на нашу приватную функцию для использования внутри функции умножения, но myPrivateMultiplyFunction не может быть доступен за пределами замыкания. Давайте попробуем это:

1
2
multiply(2,6) // => 12
myPrivateMultiplyFunction(2,6) // => ReferenceError: myPrivateMultiplyFunction is not defined

Закрытие позволило нам определить функцию для частного использования, в то же время позволяя нам контролировать то, что видит остальной мир. Что еще могут сделать затворы?


Замыкания довольно удобны, когда дело доходит до генерации кода. Надоело помнить все эти надоедливые коды клавиш для событий клавиатуры? Обычная техника — использовать карту ключей:

1
2
3
4
5
6
var KeyMap = {
    «Enter»:13,
    «Shift»:16,
    «Tab»:9,
    «LeftArrow»:37
};

Затем в нашем событии клавиатуры мы хотим проверить, была ли нажата определенная клавиша:

1
2
3
4
5
6
7
var txtInput = document.getElementById(‘myTextInput’);
txtInput.onkeypress = function(e) {
    var code = e.keyCode ||
    if (code === KeyMap.Enter) {
        console.log(txtInput.value);
    }
}

Приведенный выше пример не является худшим, но мы можем использовать метапрограммирование и замыкания, чтобы сделать еще лучшее решение. Используя наш существующий объект KeyMap , мы можем сгенерировать несколько полезных функций:

01
02
03
04
05
06
07
08
09
10
11
for (var key in KeyMap) {
 
    //access object with array accessor to set «dyanamic» function name
    KeyMap[«is» + key] = (function(compare) {
        return function(ev) {
            var code = ev.keyCode ||
            return code === compare;
        }
    })(KeyMap[key]);
 
}

Замыкания настолько мощны, потому что они могут захватывать привязки локальной переменной и параметра функции, в которой они определены.

Этот цикл генерирует функцию is для каждой клавиши в KeyMap , и наша функция KeyMap становится немного более читаемой:

1
2
3
4
5
6
var txtInput = document.getElementById(‘myTextInput’);
txtInput.onkeypress = function(e) {
    if(KeyMap.isEnter(e)) {
        console.log(txtInput.value);
    }
}

Волшебство начинается здесь:

1
2
3
KeyMap[«is» + key] = (function(compare){
     
})(KeyMap[key]);

Когда мы перебираем ключи в KeyMap , мы передаем значение, на которое ссылается этот ключ, анонимной внешней функции и немедленно ее вызывают. Это связывает это значение с параметром compare этой функции.

Мы заинтересованы в закрытии, которое мы возвращаем из анонимной функции:

1
2
3
4
return function(ev) {
    var code = ev.keyCode ||
    return code === compare;
}

Помните, что функции выполняются с областью, которая была на месте, когда они были определены. Параметр compare привязан к значению KeyMap которое было на месте во время итерации цикла, и поэтому наше вложенное замыкание может его захватить. Мы берем моментальный снимок того объема, который действовал в тот момент.

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


На этом этапе должно быть относительно легко увидеть, что замыкания жизненно важны для написания высококачественного JavaScript. Давайте применим то, что мы знаем о замыканиях, к расширению одного из нативных типов JavaScript (задыхайтесь!). Сосредоточив внимание на объектах функций, давайте дополним собственный тип Function :

1
2
3
4
5
6
7
8
Function.prototype.cached = function() {
    var self = this, //»this» refers to the original function
        cache = {};
    return function(args) {
        if(args in cache) return cache[args];
        return cache[args] = self(args);
    };
};

Этот маленький драгоценный камень позволяет любой функции создавать кэшированную версию себя. Вы можете видеть, что функция возвращает саму функцию, поэтому это расширение можно применять и использовать следующим образом:

1
2
3
Math.sin = Math.sin.cached();
Math.sin(1) // => 0.8414709848078965
Math.sin(1) // => 0.8414709848078965 this time pulled from cache

Обратите внимание на навыки закрытия, которые входят в игру. У нас есть локальная переменная cache которая хранится в секрете и защищена от внешнего мира. Это предотвратит любое вмешательство, которое может сделать наш кеш недействительным.

Возвращаемое замыкание имеет доступ к привязкам внешней функции, и это означает, что мы можем вернуть функцию с полным доступом к кешу внутри, а также к исходной функции! Эта небольшая функция может творить чудеса с производительностью. Это конкретное расширение настроено для обработки одного аргумента, но я бы хотел увидеть ваш удар по функции кеша с несколькими аргументами.


В качестве дополнительного бонуса давайте рассмотрим пару применений замыканий в дикой природе.

Иногда знаменитая фабрика jQuery $ недоступна (подумайте о WordPress), и мы хотим использовать ее так, как обычно. Вместо того, чтобы достичь jQuery.noConflict , мы можем использовать замыкание, чтобы позволить внутренним функциям иметь доступ к нашей привязке параметра $ .

1
2
3
4
5
(function($){
    $(document).ready(function(){
        //business as usual….
    });
})(jQuery);

В больших проектах Backbone.js может быть целесообразно, чтобы ваши модели приложений были приватными, а затем отображать один общедоступный API в основном представлении приложения. Используя закрытие, вы можете легко достичь этой конфиденциальности.

01
02
03
04
05
06
07
08
09
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
(function(exports){
 
var Product = Backbone.Model.extend({
    urlRoot: ‘/products’,
});
 
var ProductList = Backbone.Collection.extend({
    url: ‘/products’,
    model: Product
});
 
var Products = new ProductList;
 
var ShoppingCartView = Backbone.View.extend({
 
    addProduct: function (product, opts) {
        return CartItems.create(product, opts);
    },
 
    removeProduct: function (product, opts) {
        Products.remove(product, opts);
    },
 
    getProduct: function (productId) {
        return Products.get(productId);
    },
 
    getProducts: function () {
        return Products.models;
    }
});
 
//export the main application view only
exports.ShoppingCart = new ShoppingCartView;
 
})(window);

Краткий обзор того, что мы узнали:

  • Закрытие — это не более чем функциональный объект с областью видимости.
  • Закрытия получают свое имя по тому, как они «закрываются» над своим содержимым.
  • Закрывает наличные деньги в лексической сфере JavaScript.
  • Закрытия — это способ обеспечить конфиденциальность в JavaScript.
  • Замыкания могут захватывать привязки локальной переменной и параметра внешней функции.
  • JavaScript может быть сильно расширен с некоторой магией закрытия.
  • Закрытия могут использоваться со многими вашими любимыми библиотеками, чтобы сделать их еще круче!

Спасибо за прочтение! Не стесняйтесь задавать любые вопросы. Теперь давайте насладимся вечеринкой с пиццей!