ФорумПрограммированиеПыхнуть хотите?F.A.Q. → JS: : функции, контексты, замыкания

JS: : функции, контексты, замыкания

  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:14

    [size=12]JavaScript: функции, контексты, замыкания[/size]

    Каждый javascript-программист, с самого начала твердо усваивает истину: "JavaScript, это ни какая не Java, а совсем другое".
    Гораздо больше времени ему требуется, чтобы понять, что "JavaScript, это и никакой не C++, не PHP и не Бейсик". Большинство совпадений между этими языками заканчиваются на пресловутом "си-подобном синтаксисе".

    Одно из важнейших отличий javascript от того же PHP является то, что js — функциональный язык программирования (не путать с процедурным). Не совсем, конечно, чистый функциональный язык, но основные моменты присутствуют. Если ваше js-программирование заключается в выбрасывании окошка "вы уверены, что хотите отправить форму?", то об этих моментах можно и не знать. Для реализации же более-менее сложных вещей их понимание обязательно.

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

  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:14, спустя 10 секунд

    [size=11]Функция — объект[/size]

    В JS функция, это обычный объект. Как все остальные. С ней можно делать всё что угодно.


    function func() {
    alert('Йа, объегд!');
    }
    var x = func; // Присваивать
    alert(func); // Передавать в качестве аргумента
    alert("Функция - " + func); // Складывать. Ничего полезного не выйдет (функция приведется к строке), но всё равно можно
    func.x = 1; // Как обычному объекту присваивать любые свойства
    func.f = (function () {alert('f()!');}); // В то числе другие функции
    func.f(func.x); // А потом использовать их
    alert(func.toString()); // Вызывать предопределенные методы
    func = false; // Или просто заменить значение переменной


    Единственное, чем объект-функция для простого программиста отличается от других объектов, это тем, что для неё определена операция выполнения.

    var returnValue = nameFunction(arg1, arg2, arg3);
  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:14, спустя 8 секунд

    [size=11]Определение функции[/size]

    Как известно из документации, функции можно определить тремя способами:

    Статическое определение (FunctionDeclaration)

    Как и в PHP, C++ и других:

    function func(x)
    {
    return x * x;
    }


    Динамическое определение (FunctionExpression)

    Определение функции здесь выступает в качестве обычного выражения

    var func = function(x) {return x * x;}

    Данное выражение еще любят называть анонимной функцией, хотя можно сделать и не анонимную:

    var func = function nameFunc(x) {return x * x;}

    Но, не будем углубляться в ньюансы.

    Определение с помощью конструктора Function


    var func = new Function("x", "y", "return x * y");



    Во всех трех случаях, мы получаем переменную "func", ссылающуюся на функцию.
    Первый способ более привычный, более эффективных и функции определяются сразу перед началом исполнения.


    func1(); // Можем вызвать выше определения
    func2(); // Error! func2 is not a function

    function func1() {alert('func1');}
    var func2 = (function() {alert('func2');});

    func1();
    func2(); // Теперь функция определена (вернее, присвоена переменной)


    Однако, во многих случаях способ с FunctionExpression удобнее, а иногда и единственный возможный (например, определение методов объекта).

    Способ с конструктором Function() лучше лишний раз не использовать. Кроме неявного eval()'а здесь еще несколько темных моментов, некоторые из которых обсудим ниже.
  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:14, спустя 9 секунд

    [size=11]Исполнительный контекст[/size]

    Всякий код выполняется в каком-то контексте. Основной параметр контекста — коллекция локальных переменных с уникальными именами.
    Контекст создается при вызове функции и код этой функции работает в своём контексте.
    Кроме того, есть глобальный контекст, в котором выполняется глобальный код. Правда и его можно умозрительно представить в виде функции — конструктора объекта window.

    Рассмотрим страницу:

    <script type="text/javascript">
    function func(q)
    {
    for (var i = 0; i < q; i++) {
    alert('click');
    }
    }
    var x = 3;
    func(x);
    </script>
    <div onclick="var z = 2 + 3; alert(z);">Click me</div>
    <div onclick="var x = 3; func(x)">And me</div>


    При загрузке, сначала выполняется глобальный код (тот, что в <SCRIPT>) в глобальном контексте. То, что обычно называют глобальными переменными, на самом деле обычные локальные переменные, но глобального контекста. Здесь их две — "x" и "func" (как мы помним, функции, это обычные переменные). Конечно, есть и предопределенные объекты (например, window), но сейчас речь не о них.

    В процессе выполнения вызывается функция func(). При её вызове создается локальный контекст данной функции. В нем определены две переменные: "i" (определена с помощью var) и "q" (аргумент). Опять-таки здесь есть и предопределенные переменные (this, arguments, но о них ниже). По завершении выполнения функции, её контекст удаляется.

    После завершения выполнения глобального кода (того, что в теге <SCRIPT>), работа javascript приостанавливается. До тех пор пока пользователь не решит щелкнуть мышкой по одному из слоев. При этом запускается обработчик. На самом деле код обработчика представляет собой анонимную функцию. Т.е. следующие определния практически равноправны:

    <div onclick="alert(1)" id="iDiv">Div</div>
    <script type="text/javascript">
    document.getElementById("iDiv").onclick = (function() {alert(1);});
    </script>

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

    Щелкаем по "Click me" - создался новый контекст, в нем определилась локальная переменная "z", выполнился alert(), обработчик завершился, контекст удалился, javascript отдыхает до следующего события.

    Щелкаем по "And me" - создался контекст обработчика, в нем опеределась переменная "x", была вызвана функция func, создался контекст функции, в нем определились переменные "q" и "i", функция выполнилась, её контекст удалился, завершился обработчик и удалился его контекст, опять ожидание…


    Важно понимать, что контекст привязан к функции, но он не связан с ней постоянно.
    В примере функция func существует всё время отображения документа в браузере, а её контекст создается только на момент её исполнения. Вызвали два раза - два раза на время создались различные контексты. Если вызовем внутри func её саму рекурсивно, получим одновременно два контекста одной функции.
  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:14, спустя 7 секунд

    [size=11]Инициализация контекста[/size]

    После вызова функции и создания её исполнительного контекста он первым делом инициализуется. В том числе он наполняется переменными.

    1. Создается переменная this, указывающая объект в контексте которого выполняется функция (см. ниже).
    2. Создается переменная arguments, содержащая список аргументов функции.
    3. Создаются и заполняются переменные, соответствующие аргументам функции.
    4. Создаются переменные, соответствующие статически определенным функциям.
    5. Создаются переменные, объявленные с помощью var, их значение изначально устанавливается в undefined.

    Последовательность этих пунктов не такая, как я привел и может отличаться в различных браузерах.
    Важное следствие из пунктов 4-5: контекст функции по сути статический. То есть с первого взгляда на JS кажется, что мы можем создавать переменные, когда захотим и какие захотим, но это не так.


    var x = 1;
    function func()
    {
    alert(x); // undefined
    var x = 2;
    alert(x); // 2
    }
    func();

    Может показаться, что первый alert() выведет значение глобальной переменной "x", т.к. определение локальной переменной происходит до его вызова.
    Но нет, определение происходит до запуска кода функции.

    Иногда спрашивают: вот есть цикл:

    for (var i = 0; i < 5; i++) {
    var j = i * 2;
    alert(j);
    }

    Здесь на каждой иттерации происходит объявление и создание 2-х переменных, не лучше ли вынести их обявление перед циклом, чтобы не тратить лишнее время? Не лучше. Создание происходит только один раз.


    if (f) {
    var x = 1;
    } else {
    var y = 2;
    }

    В зависимости от условия будет создана одна или другая переменная? Нет, будут созданы обе переменные и созданы в самом начале. В зависимости от условия, просто одна из переменных получит какое-то значение.

    Кроме создания переменных на этапе инициализации происходит еще одно важное действие — связывание с родительским контекстом. О чем ниже.
  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:14, спустя 10 секунд

    [size=11]Вложенные функции и разрешение имен переменных[/size]

    JavaScript поддерживает определение вложенных функций.


    function func1()
    {
    alert('Первый, на!');
    function func2() {
    alert('А я второй!');
    }
    func3 = function() {alert('А я томат!');}
    func2();
    func3();
    }
    func1();


    По большому счету ничего необычного, множество языков программирования позволяет создавать вложенные функции (кроме, к сожалению, PHP). Внутри родительской функции создали две локальные, выполняющие вспомагательную для родительской работу. Извне они не видны. При завершении func1(), func2 и func3 уничтожаются, как и все другие переменные локального контекста.

    Разрешение имен переменных

    Разберем следующий пример:

    var x = 'global';
    var y = 'global';
    var z = 'global';
    function func1()
    {
    var x = 'func1';
    var y = 'func1';
    function func2()
    {
    var x = 'func2';
    alert(x); // func2
    alert(y); // func1
    alert(z); // global
    }
    func2();
    }
    func1();


    Что делает JS, когда встречает имя переменной (например, "x" в alert(x))? Она определяет её значение, т.е. пытается сопоставить идентификатору объект, на который он указывает.
    Первым делом ищется локальная переменная (свойство локального контекста) "x". Найдено — всё отлично. Что же происходит, когда не найдено? JS начинает искать переменную в родительском контексте.

    Каждая функция в JS имеет неявную связь с контекстом в котором определена. func2 определена в контексте func1, поэтому сохраняет скрытую ссылку на этот контекст. Таким образом, в каждый момент выполнения существует последовательность контекстов (областей видимости), т.н. Scope Chain. В нашем случае это:
    <Глобальный контекст> —> <Контекст func1> —> <Контекст func2>.

    Если переменной нет в текущем контексте, то поиск продолжается в родительском и так далее. Если поиск дошел до глобального контекста и там тоже не дал результата - происходит ошибка.

    Примерно то же самое происходит и с присвоением значения переменной:

    var x;
    function func()
    {
    var y;
    x = 1; // Не нашли локальной - присвоили глобальной
    y = 2; // А эту нашли
    }
    func();


    Важно: функция привязывается к контексту, в котором определена, а не в котором вызывается:

    var x = 'global';
    function func1()
    {
    alert(x);
    }
    function func2()
    {
    var x = 'local';
    func1(); // global
    }
    func2();

    Функция func1 определена в глобальном контексте и всегда будет связана с ним, хотя и может вызываться из какого-то другого контекста (в нашем случае func2)

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

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:15, спустя 28 секунд

    [size=11]Замыкания[/size]

    Замыкание (closure), это одной из важнейших понятий в JavaScript.
    Есть множество споров, как лучше переводить "closure", и что конкретно считать замыканием (сохранение ссылки на контекст при его уничтожении, использование свободных переменных или вообще каждую функцию). Не будем этим заниматься, а опишем сам механизм возникновения этого чуда.

    То что я писал выше про связь контекстов, пока не открыло ничего принципиально отличного от того, что существует, например, в C++. Там так же сначала ищется локальная переменная, потом переменная родительской функции.
    А принципиальное отличие заключается всё в том же с чего начали — функция, это обычный объект. Если в Си, вложенные функции умирали вместе с родительской, то в JS они этого делать совсем не обязаны.


    function func1()
    {
    var x = 1;
    function setX(value)
    {
    x = value;
    }
    function getX(value)
    {
    return x;
    }
    return [setX, getX];
    }

    ret = func1();
    var setX = ret[0];
    var getX = ret[1];

    alert(getX()); // 1
    setX(5);
    alert(getX()); // 5


    Что произошло? Мы создали две локальные внутри func1(), а потом взяли и возвратили из в качестве результата. Результат присвоили глобальным переменным и функции продолжают жить после смерти func1, так как на них ссылаются переменные всё еще существующего контекста (глобального).
    Более того, продолжает и жить контекст функции func1(). Так как функции setX и getX связаны с ним и используют его переменную "x". То есть функция выполнилась и завершилась, а её контекст всё еще существует. И единственное место, где можно получить к нему доступ, это функции setX() и getX().

    Вот это и есть замыкание.

    В третий раз повторяю: контекст и функция, это разные вещи:


    ret = func1();
    var setX1 = ret[0];
    var getX1 = ret[1];

    ret = func1();
    var setX2 = ret[0];
    var getX2 = ret[1];

    setX1(5);
    alert(getX1()); // 5
    alert(getX2()); // 1

    setX2(10);
    alert(getX1()); // 5
    alert(getX2()); // 10

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

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:15, спустя 43 секунды

    [size=11]Применение замыканий[/size]

    Замыкание, это одна из альтернатив объектам из ООП. Оно так же осуществляет связь данных (переменные замкнутого контекста) и функций по работе с ними.

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

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

    element.onclick = func;


    А если с аргументом?

    var arg = 1;
    element.onclick = "func(" + arg + ")";


    Использовать строку в качестве значения обработчика не совсем здорово (неявный eval и т.п.), но, кажется, работает и то хорошо.
    А если arg не число, а сложный объект или он может изменяться впоследствии?

    var arg = new Object(…);
    element.onclick = "func(arg)";

    Т.е. здесь мы не сразу же формируем значение аргумента в виде строки, а вставляем имя переменной.
    Не считая многих оговорок, это может работать, только в случае если arg — глобальная переменная.
    Если обработчик мы устанавливаем внутри функции, то к моменту вызова этого обработчика никакой arg уже существовать не будет (функция завершится, локальные переменные удалились).

    По правильному же это делается с помощью замыканий:

    var arg = 1;
    element.onclick = (function() {func(arg);});

    Обработчику присваиваем анонимную функцию, внутри которой вызывается нужная нам функция с нужным аргументом. Так как определение анонимной функции находится в том же контексте, где и живет переменная arg, то наша анонимная функция захватывает эту переменную и продолжает на неё ссылаться всё время своего существования.
  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:16, спустя 12 секунд

    [size=11]Разделение переменных между замыканиями[/size]

    Есть, допустим, у нас слой, а в нем ссылки:

    <div id="idiv">
    <a href="#">One</a>
    <a href="#">Two</a>
    <a href="#">Three</a>
    <a href="#">Four</a>
    <a href="#">Five</a>
    <a href="#">Six</a>
    </div>


    И хотим мы, чтобы при нажатии на эту ссылку выскакивал её порядковый номер ("1", "2"…"6").
    Кажется, элементарно. Перебрали все ссылки в цикле и повесили на них обработчик, использовав наши любимые замыкания:

    var idiv = document.getElementById("idiv");
    var anchors = idiv.getElementsByTagName("A");
    for (var i = 0; i < anchors.length; i++) {
    var a = anchors.item(i);
    a.onclick = (function() {alert(i + 1);});
    }


    Жмем по ссылкам — ни тут то было. Везде выскакивает "7". Что за "7"? Его вообще быть не должно!

    Просто мы создали шесть анонимных функций (обработчиков), которые все разделяют одну и ту же переменную "i". Переменная была счетчиком цикла, дошла в нем до 7 и осталась с этим значением. Навсегда.

    Что делать? Нужно каждому замыканию выделить свою переменную.

    function makeHandler(num)
    {
    return (function() {alert(num);});
    }

    var idiv = document.getElementById("idiv");
    var anchors = idiv.getElementsByTagName("A");
    for (var i = 0; i < anchors.length; i++) {
    var a = anchors.item(i);
    a.onclick = makeHandler(i + 1);
    }


    Вот теперь работает как надо. Что мы сделали? На каждой итерации цикла мы вызывали функцию makeHandler с параметром, при этом каждый раз создавался контекст функции makeHandler, а уже в нем определялась анонимная функция, использующая локальную переменную num своего конкретного контекста.
    То есть на данный момент имеем — шесть обработчиков замыкаются на шесть различных контекстов одной функции. В каждом контексте значение num различно.
  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:16, спустя 8 секунд

    [size=11]Объекты и их контексты[/size]

    JavaScript не только функциональный язык, но и объекто-ориентированный. То есть куда не плюнь — везде объекты.
    Некоторые его объектно-ориентированность считают не совсем правильно ориентированной, так как в JS нет классов, но есть прототипы, но сейчас не об этом.

    Каждая функция выполняется в контексте какого-то объекта. По большому счету это значит только одно — при инициализации её контекста появляется переменная this, которая указывает на этот объект. "Глобальные" функции вызываются в контексте объекта window.

    Обычно принято считать, что функция выполняющаяся в контексте какого-то объекта, является "методом" этого объекта. Но в JS это не совсем так.
    Во-первых, в JS не разделяются свойства и методы. Все свойства объектов — переменные, среди которых могут оказаться объекты-функции.
    Во-вторых, функции в JS не привязаны жестко ни к какому объекту и могут вызываться в различных контекстах.


    obj.func(); // Вызвали "метод" объекта
    obj2.f = obj.func; // Опля, а теперь та же функция метод совершенно другого объекта, да еще и с другим именем
    obj2.f();


    По большому счету следующая запись:

    obj.method();

    означает, что функцию, на которую ссылается свойство "method" объекта obj нужно вызвать в контексте этого самого объекта. И ничего больше.

    С одной стороны, "непривязанность" функций к объектам, очень гибкая и мощная возможность. С другой стороны не всё так здорово.

    Обычная проблема:

    function construct()
    {
    this.x = "this is x";
    this.one = (
    function () {
    alert(this.x);
    }
    );
    this.two = (
    function () {
    this.one();
    }
    );
    }

    obj = new construct();
    obj.two();


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

    И так, создаем объект, какого-то "класса". Один из его "методов" вызывает другой метод, который так же использует "свойство" всё того же объекта.
    А как нам жестко связать two() с объектом, чьим "методом" он является? Иначе фиг с ним сделаешь, например такое:

    function func(method)
    {
    method(); // this - window, window.one() - not defined
    }
    func(obj.two);


    А поможет нам снова наше любимое замыкание:

    function construct()
    {
    var _this = this;
    this.x = "this is x";
    this.one = (
    function () {
    alert(_this.x);
    }
    );
    this.two = (
    function () {
    _this.one();
    }
    );
    }

    Мы создали в конструкторе переменную _this, которая ссылается на конструируемый объект. Внутри всех функций используем "_this". В то время, как this будет внутри функций перекрываться, создаваемым при инициализации контекста своим "this", "_this" продолжит ссылаться на исходный объект.


    Private

    Ну и напоследок, эмуляция приватных свойств:

    function construct()
    {
    var privVar;
    this.setVar = (
    function (value) {
    privVar = value;
    }
    );
    this.getVar = (
    function getVar(value) {
    return privVar;
    }
    );
    }

    obj = new construct();
    obj.setVar(11);
    alert(obj.getVar());
  • AlexB

    Сообщения: 4306 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:38, спустя 22 минуты 22 секунды

    множество языков программирования позволяет создавать вложенные функции (кроме, к сожалению, PHP).


    Как нах? Вроде PHP тоже всю жизнь позволяет ….
    Наверно здесь надо разъяснить, что в PHP такой синтаксис допустим, но вот локальной области видимости внутренняя функция не приобретает.
  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:42, спустя 3 минуты 32 секунды

    Правда? Пример
  • AlexB

    Сообщения: 4306 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:46, спустя 4 минуты 21 секунду

    Да собственно, какой тут может быть особый пример


    function a()
    {
    function b()
    {
    echo "test<br>";
    }
    b();
    }
    a();
    b();
  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:50, спустя 3 минуты 47 секунд

    Это не вложенная функция (ограниченная областью видимости родительской), это всё то же объявление глобальной функции, о чем красноречиво говорит вызов b() в глобальной области.
    Так же как и:

    if (f) {
    function func() { /* Тело */ }
    } else {
    function func() { /* Другое тело */}
    }
  • AlexB

    Сообщения: 4306 Репутация: N Группа: в ухо

    Spritz 22 октября 2007 г. 15:55, спустя 5 минут 25 секунд

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


Пожалуйста, авторизуйтесь, чтобы написать комментарий!