2015-01-19

Маленькие javascript-ости: lambdas and closures.

Subj, все знают? Несмотря на мудреное название, lambda = closure = "замыкание" это просто функция, которая имеет доступ к переменным окружения. Говорят, она их "захватывает", поскольку внутри она должна хранить информацию о них. Зачем замыкание нужно? Чтобы можно было делать отложенные вычисления, определить некоторое действие, которое будет выполнено потом, например, когда пользователь нажмет на кнопку. Или передать некоторое действие в качестве параметра вызываемой процедуре. Напимер типа вот такого:
var found={};
database.iterate_records(
  function(record){ found[record.phone]=record.name; }
  );

Напишем простой тест. Определим десять функций, печатающих числа от 0 до 9 и вызовем их последовательно:
var a=[];
for(var i=0; i<10; i++) a.push(function(){ document.write(i); });
for(var i=0; i<10; i++) a[i]();
Запускаем, получаем: "0123456789". Что и требовалось, так? Ship it!

Теперь изменим немножко заменив переменную во втором цикле:
var a=[];
for(var i=0; i<10; i++) a.push(function(){ document.write(i); });
for(var j=0; j<10; j++) a[j]();
Что ожидаем? То же самое. Что получаем? "10101010101010101010". Как это?
Это очень просто. Замыкание захватывает не значение i в какой-то момент времени, а саму переменную, объект переменной. Выражение document.write(i) печатает текущее значение переменной i сейчас, а те то, которое было в момент, когда создавалась 'function'. Это очень важная особенность javascript, которая часто приводит к большому количеству ошибок.

Ok, скажем мы, давайте изменим, чтобы была не переменная, а выражение:
var a=[];
for(var i=0; i<10; i++) a.push(function(){ document.write(i+' '); });
for(var j=0; j<10; j++) a[j]();
Получаем: "10 10 10 10 10 10 10 10 10 10 ". Не помогает.
Потому, что выражение вычисляется в момент вызова функции, а не в момент создания замыкания.

Сделаем копию:
var a=[];
for(var i=0; i<10; i++){ var ii=i; a.push(function(){ document.write(ii); }); }
for(var j=0; j<10; j++) a[j]();
Получаем "9999999999". Как это?
Потому, что замыкание захватило переменную ii. И текущее значение ii = последнее, что было присвоено = 9.

Ну и что можно сделать? Единственное, что можно сделать в javascript, это вызвать функцию с параметром. Параметры передаются по копии значения, а не по ссылке. Как-то так:
var a=[];
for(var i=0; i<10; i++) a.push(
  function(i){ return function(){ document.write(i); }} (i)
  );
for(var j=0; j<10; j++) a[j]();
Получаем "0123456789". Ура! Заработало. Теперь не забыть писать такое везде, где есть замыкание. Здорово, правда?

Понятно почему так? Здесь i в замыкании и i цикла - разные i. Можно заменить внутреннюю i:
var a=[];
for(var i=0; i<10; i++) a.push(
  function(a){ return function(){ document.write(a); }} (i)
  );
for(var j=0; j<10; j++) a[j]();
Здесь мы определяем функцию и тут же ее вызываем: function(a){...}(i)
Этот вызов создает копию значения переменной i и присваивает его параметру a.
Результатом этой функции будет уже замыкание, которое захватило a, и это a - локальная переменная функции, поэтому каждый вызов порождает новый экземпляр a и каждое замыкание захватывает свою переменную, а не все одну и ту же, как в первых примерах.

Такая вот захватывающая история. Как с этим практически работать без превращения кода в нечитаемый кошмар напишу в одном из следующих выпусков. А пока попробую подсветить синтаксис в приведенных примерах. Заранее извиняюсь за многочисленные обновления данного текста.

3 comments:

Anonymous said...

Немного не в тему, но ты меня прости. (полезная вещица.)

Если в HTML добавить строку:

script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=default"

(скобочки не забудь добавить)

То модно станет писать формулы в формате ЛатеХа. Проверь сам (тестовая строка):

\(G(x,y)=\sum\limits_{i=0}^{\infty}X_i(x)Y_i(y)\)

Ты можешь сам заказывать размер и цвет шрифта формулы.

Valery Tolkov said...

Да, mathjax я видел. Если бы писал формулы, наверняка бы воспользовался. А до этого была mimetex, на стороне сервера. У меня даже на сайте стоит: test

Anonymous said...

mimetex генерит битмап, а MathJax динамически (внутри браузера) из ЛаТеха генерит HTML.
Я свою монографию опубликовал в Internet.

http://bolshoyforum.com/forum/index.php?topic=410865.0