Замыкания, функции изнутри
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/closure.
В этой главе мы продолжим рассматривать, как работают переменные, и, как следствие, познакомимся с замыканиями. От глобального объекта мы переходим к работе внутри функций.
Лексическое окружение
Все переменные внутри функции – это свойства специального внутреннего объекта LexicalEnvironment , который создаётся при её запуске.
Мы будем называть этот объект «лексическое окружение» или просто «объект переменных».
При запуске функция создаёт объект LexicalEnvironment , записывает туда аргументы, функции и переменные. Процесс инициализации выполняется в том же порядке, что и для глобального объекта, который, вообще говоря, является частным случаем лексического окружения.
В отличие от window , объект LexicalEnvironment является внутренним, он скрыт от прямого доступа.
Пример
Посмотрим пример, чтобы лучше понимать, как это работает:
function sayHi(name) < var phrase = "Привет, " + name; alert( phrase ); >sayHi('Вася');
- До выполнения первой строчки её кода, на стадии инициализации, интерпретатор создаёт пустой объект LexicalEnvironment и заполняет его. В данном случае туда попадает аргумент name и единственная переменная phrase :
function sayHi(name) < // LexicalEnvironment = < name: 'Вася', phrase: undefined >var phrase = "Привет, " + name; alert( phrase ); > sayHi('Вася');
function sayHi(name) < // LexicalEnvironment = < name: 'Вася', phrase: undefined >var phrase = "Привет, " + name; // LexicalEnvironment = < name: 'Вася', phrase: 'Привет, Вася'>alert( phrase ); > sayHi('Вася');
Если почитать спецификацию ECMA-262, то мы увидим, что речь идёт о двух объектах: VariableEnvironment и LexicalEnvironment .
Но там же замечено, что в реализациях эти два объекта могут быть объединены. Так что мы избегаем лишних деталей и используем везде термин LexicalEnvironment , это достаточно точно позволяет описать происходящее.
Более формальное описание находится в спецификации ECMA-262, секции 10.2-10.5 и 13.
Доступ ко внешним переменным
Из функции мы можем обратиться не только к локальной переменной, но и к внешней:
var userName = "Вася"; function sayHi() < alert( userName ); // "Вася" >
Интерпретатор, при доступе к переменной, сначала пытается найти переменную в текущем LexicalEnvironment , а затем, если её нет – ищет во внешнем объекте переменных. В данном случае им является window .
Такой порядок поиска возможен благодаря тому, что ссылка на внешний объект переменных хранится в специальном внутреннем свойстве функции, которое называется [[Scope]] . Это свойство закрыто от прямого доступа, но знание о нём очень важно для понимания того, как работает JavaScript.
При создании функция получает скрытое свойство [[Scope]] , которое ссылается на лексическое окружение, в котором она была создана.
В примере выше таким окружением является window , так что создаётся свойство:
Это свойство никогда не меняется. Оно всюду следует за функцией, привязывая её, таким образом, к месту своего рождения.
При запуске функции её объект переменных LexicalEnvironment получает ссылку на «внешнее лексическое окружение» со значением из [[Scope]] .
Если переменная не найдена в функции – она будет искаться снаружи.
Именно благодаря этой механике в примере выше alert(userName) выводит внешнюю переменную. На уровне кода это выглядит как поиск во внешней области видимости, вне функции.
- Каждая функция при создании получает ссылку [[Scope]] на объект с переменными, в контексте которого была создана.
- При запуске функции создаётся новый объект с переменными LexicalEnvironment . Он получает ссылку на внешний объект переменных из [[Scope]] .
- При поиске переменных он осуществляется сначала в текущем объекте переменных, а потом – по этой ссылке.
Выглядит настолько просто, что непонятно – зачем вообще говорить об этом [[Scope]] , об объектах переменных. Сказали бы: «Функция читает переменные снаружи» – и всё. Но знание этих деталей позволит нам легко объяснить и понять более сложные ситуации, с которыми мы столкнёмся далее.
Всегда текущее значение
Значение переменной из внешней области берётся всегда текущее. Оно может быть уже не то, что было на момент создания функции.
Например, в коде ниже функция sayHi берёт phrase из внешней области:
var phrase = 'Привет'; function sayHi(name) < alert(phrase + ', ' + name); >sayHi('Вася'); // Привет, Вася (*) phrase = 'Пока'; sayHi('Вася'); // Пока, Вася (**)
На момент первого запуска (*) , переменная phrase имела значение ‘Привет’ , а ко второму (**) изменила его на ‘Пока’ .
Это естественно, ведь для доступа к внешней переменной функция по ссылке [[Scope]] обращается во внешний объект переменных и берёт то значение, которое там есть на момент обращения.
Вложенные функции
Внутри функции можно объявлять не только локальные переменные, но и другие функции.
К примеру, вложенная функция может помочь лучше организовать код:
function sayHiBye(firstName, lastName) < alert( "Привет, " + getFullName() ); alert( "Пока, " + getFullName() ); function getFullName() < return firstName + " " + lastName; >> sayHiBye("Вася", "Пупкин"); // Привет, Вася Пупкин ; Пока, Вася Пупкин
Здесь, для удобства, создана вспомогательная функция getFullName() .
Вложенные функции получают [[Scope]] так же, как и глобальные. В нашем случае:
getFullName.[[Scope]] = объект переменных текущего запуска sayHiBye
Благодаря этому getFullName() получает снаружи firstName и lastName .
Заметим, что если переменная не найдена во внешнем объекте переменных, то она ищется в ещё более внешнем (через [[Scope]] внешней функции), то есть, такой пример тоже будет работать:
var phrase = 'Привет'; function say() < function go() < alert( phrase ); // найдёт переменную снаружи >go(); > say();
Возврат функции
Рассмотрим более «продвинутый» вариант, при котором внутри одной функции создаётся другая и возвращается в качестве результата.
В разработке интерфейсов это совершенно стандартный приём, функция затем может назначаться как обработчик действий посетителя.
Здесь мы будем создавать функцию-счётчик, которая считает свои вызовы и возвращает их текущее число.
В примере ниже makeCounter создаёт такую функцию:
function makeCounter() < var currentCount = 1; return function() < // (**) return currentCount++; >; > var counter = makeCounter(); // (*) // каждый вызов увеличивает счётчик и возвращает результат alert( counter() ); // 1 alert( counter() ); // 2 alert( counter() ); // 3 // создать другой счётчик, он будет независим от первого var counter2 = makeCounter(); alert( counter2() ); // 1
Как видно, мы получили два независимых счётчика counter и counter2 , каждый из которых незаметным снаружи образом сохраняет текущее количество вызовов.
Где? Конечно, во внешней переменной currentCount , которая у каждого счётчика своя.
Если подробнее описать происходящее:
- В строке (*) запускается makeCounter() . При этом создаётся LexicalEnvironment для переменных текущего вызова. В функции есть одна переменная var currentCount , которая станет свойством этого объекта. Она изначально инициализуется в undefined , затем, в процессе выполнения, получит значение 1 :
function makeCounter() < // LexicalEnvironment = < currentCount: undefined >var currentCount = 1; // LexicalEnvironment = < currentCount: 1 >return function() < // [[Scope]] ->LexicalEnvironment (**) return currentCount++; >; > var counter = makeCounter(); // (*)
На этом создание «счётчика» завершено.
Итоговым значением, записанным в переменную counter , является функция:
function() < // [[Scope]] -> return currentCount++; >;
Возвращённая из makeCounter() функция counter помнит (через [[Scope]] ) о том, в каком окружении была создана.
Это и используется для хранения текущего значения счётчика.
Далее, когда-нибудь, функция counter будет вызвана. Мы не знаем, когда это произойдёт. Может быть, прямо сейчас, но, вообще говоря, совсем не факт.
Эта функция состоит из одной строки: return currentCount++ , ни переменных ни параметров в ней нет, поэтому её собственный объект переменных, для краткости назовём его LE – будет пуст.
Однако, у неё есть свойство [[Scope]] , которое указывает на внешнее окружение. Чтобы увеличить и вернуть currentCount , интерпретатор ищет в текущем объекте переменных LE , не находит, затем идёт во внешний объект, там находит, изменяет и возвращает новое значение:
function makeCounter() < var currentCount = 1; return function() < return currentCount++; >; > var counter = makeCounter(); // [[Scope]] -> alert( counter() ); // 1, [[Scope]] -> alert( counter() ); // 2, [[Scope]] -> alert( counter() ); // 3, [[Scope]] ->
Переменную во внешней области видимости можно не только читать, но и изменять.
В примере выше было создано несколько счётчиков. Все они взаимно независимы:
var counter = makeCounter(); var counter2 = makeCounter(); alert( counter() ); // 1 alert( counter() ); // 2 alert( counter() ); // 3 alert( counter2() ); // 1, счётчики независимы
Они независимы, потому что при каждом запуске makeCounter создаётся свой объект переменных LexicalEnvironment , со своим свойством currentCount , на который новый счётчик получит ссылку [[Scope]] .
Свойства функции
Функция в JavaScript является объектом, поэтому можно присваивать свойства прямо к ней, вот так:
function f() <> f.test = 5; alert( f.test );
Свойства функции не стоит путать с переменными и параметрами. Они совершенно никак не связаны. Переменные доступны только внутри функции, они создаются в процессе её выполнения. Это – использование функции «как функции».
А свойство у функции – доступно отовсюду и всегда. Это – использование функции «как объекта».
Если хочется привязать значение к функции, то можно им воспользоваться вместо внешних переменных.
В качестве демонстрации, перепишем пример со счётчиком:
function makeCounter() < function counter() < return counter.currentCount++; >; counter.currentCount = 1; return counter; > var counter = makeCounter(); alert( counter() ); // 1 alert( counter() ); // 2
При запуске пример работает также.
Принципиальная разница – во внутренней механике и в том, что свойство функции, в отличие от переменной из замыкания – общедоступно, к нему имеет доступ любой, у кого есть объект функции.
Например, можно взять и поменять счётчик из внешнего кода:
var counter = makeCounter(); alert( counter() ); // 1 counter.currentCount = 5; alert( counter() ); // 5
Иногда свойства, привязанные к функции, называют «статическими переменными».
В некоторых языках программирования можно объявлять переменную, которая сохраняет значение между вызовами функции. В JavaScript ближайший аналог – такое вот свойство функции.
Итого: замыкания
Замыкание – это функция вместе со всеми внешними переменными, которые ей доступны.
Таково стандартное определение, которое есть в Wikipedia и большинстве серьёзных источников по программированию. То есть, замыкание – это функция + внешние переменные.
Тем не менее, в JavaScript есть небольшая терминологическая особенность.
Обычно, говоря «замыкание функции», подразумевают не саму эту функцию, а именно внешние переменные.
Иногда говорят «переменная берётся из замыкания». Это означает – из внешнего объекта переменных.
Иногда говорят «Вася молодец, понимает замыкания!». Что это такое – «понимать замыкания», какой смысл обычно вкладывают в эти слова?
«Понимать замыкания» в JavaScript означает понимать следующие вещи:
- Все переменные и параметры функций являются свойствами объекта переменных LexicalEnvironment . Каждый запуск функции создаёт новый такой объект. На верхнем уровне им является «глобальный объект», в браузере – window .
- При создании функция получает системное свойство [[Scope]] , которое ссылается на LexicalEnvironment , в котором она была создана.
- При вызове функции, куда бы её ни передали в коде – она будет искать переменные сначала у себя, а затем во внешних LexicalEnvironment с места своего «рождения».
В следующих главах мы углубим это понимание дополнительными примерами, а также рассмотрим, что происходит с памятью.