Здесь курят мануал.

Добро пожаловать на Пыху!

Логин:
Пароль:
 

Нет прописки? Зарегистрируйся!

Новости

Пыха переехала на новый сервер, ура!

Краснодарское время: 24 Май, 2012, 12:08:28

Страниц: [1] 2
Печать
Автор Тема: JS: : функции, контексты, замыкания  (Прочитано 9246 раз)
0 Пользователей и 1 Гость смотрят эту тему.
vasa_c    ↓ 
22 Октябрь, 2007, 03:14:04
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

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

vasa_c    ↓ 
22 Октябрь, 2007, 03:14:14 , спустя 10 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

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

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;               // Или просто заменить значение переменной
 

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

var returnValue = nameFunction(arg1, arg2, arg3);
 
Записан

vasa_c    ↓ 
22 Октябрь, 2007, 03:14:22 , спустя 8 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

Определение функции
 
Как известно из документации, функции можно определить тремя способами:
 
Статическое определение (FunctionDeclaration)
 
Как и в PHP, C++ и других:
Javascript

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

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

var func = function(x) {return x * x;}
 
Данное выражение еще любят называть анонимной функцией, хотя можно сделать и не анонимную:
Javascript

var func = function nameFunc(x) {return x * x;}
 
Но, не будем углубляться в ньюансы.
 
Определение с помощью конструктора Function
 
Javascript

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

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

func1(); // Можем вызвать выше определения
func2(); // Error! func2 is not a function
 
function func1() {alert('func1');}
var func2 = (function() {alert('func2');});
 
func1();
func2(); // Теперь функция определена (вернее, присвоена переменной)
 

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

vasa_c    ↓ 
22 Октябрь, 2007, 03:14:31 , спустя 9 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

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

<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 приостанавливается. До тех пор пока пользователь не решит щелкнуть мышкой по одному из слоев. При этом запускается обработчик. На самом деле код обработчика представляет собой анонимную функцию. Т.е. следующие определния практически равноправны:
HTML

<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    ↓ 
22 Октябрь, 2007, 03:14:38 , спустя 7 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

Инициализация контекста
 
После вызова функции и создания её исполнительного контекста он первым делом инициализуется. В том числе он наполняется переменными.
 
1. Создается переменная this, указывающая объект в контексте которого выполняется функция (см. ниже).
2. Создается переменная arguments, содержащая список аргументов функции.
3. Создаются и заполняются переменные, соответствующие аргументам функции.
4. Создаются переменные, соответствующие статически определенным функциям.
5. Создаются переменные, объявленные с помощью var, их значение изначально устанавливается в undefined.
 
Последовательность этих пунктов не такая, как я привел и может отличаться в различных браузерах.
Важное следствие из пунктов 4-5: контекст функции по сути статический. То есть с первого взгляда на JS кажется, что мы можем создавать переменные, когда захотим и какие захотим, но это не так.
 
Javascript

var x = 1;
function func()
{
    alert(x); // undefined
    var x = 2;
    alert(x); // 2
}
func();
 
Может показаться, что первый alert() выведет значение глобальной переменной "x", т.к. определение локальной переменной происходит до его вызова.
Но нет, определение происходит до запуска кода функции.
 
Иногда спрашивают: вот есть цикл:
Javascript

for (var i = 0; i < 5; i++) {
    var j = i * 2;
    alert(j);
}
 
Здесь на каждой иттерации происходит объявление и создание 2-х переменных, не лучше ли вынести их обявление перед циклом, чтобы не тратить лишнее время? Не лучше. Создание происходит только один раз.
 
Javascript

if (f) {
    var x = 1;
} else {
    var y = 2;
}
 
В зависимости от условия будет создана одна или другая переменная? Нет, будут созданы обе переменные и созданы в самом начале. В зависимости от условия, просто одна из переменных получит какое-то значение.
 
Кроме создания переменных на этапе инициализации происходит еще одно важное действие — связывание с родительским контекстом. О чем ниже.
Записан

vasa_c    ↓ 
22 Октябрь, 2007, 03:14:48 , спустя 10 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

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

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

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

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>.
 
Если переменной нет в текущем контексте, то поиск продолжается в родительском и так далее. Если поиск дошел до глобального контекста и там тоже не дал результата - происходит ошибка.
 
Примерно то же самое происходит и с присвоением значения переменной:
Javascript

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

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

var x = 'global';
function func1()
{
    alert(x);
}
function func2()
{
    var x = 'local';
    func1(); // global
}
func2();
 
Функция func1 определена в глобальном контексте и всегда будет связана с ним, хотя и может вызываться из какого-то другого контекста (в нашем случае func2)
 
Не менее важно: повторю еще раз: функция и контекст, это разные вещи. Функция привязывается ни к функции, в которой определена, а к её конкретному контексту. Важность этого замечания откроется позднее.
« Последнее редактирование: 22 Октябрь, 2007, 04:09:02 от vasa_c » Записан

vasa_c    ↓ 
22 Октябрь, 2007, 03:15:16 , спустя 28 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

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

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().
 
Вот это и есть замыкание.
 
В третий раз повторяю: контекст и функция, это разные вещи:
 
Javascript

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    ↓ 
22 Октябрь, 2007, 03:15:59 , спустя 43 секунды
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

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

element.onclick = func;
 

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

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

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

var arg = new Object(...);
element.onclick = "func(arg)";
 
Т.е. здесь мы не сразу же формируем значение аргумента в виде строки, а вставляем имя переменной.
Не считая многих оговорок, это может работать, только в случае если arg — глобальная переменная.
Если обработчик мы устанавливаем внутри функции, то к моменту вызова этого обработчика никакой arg уже существовать не будет (функция завершится, локальные переменные удалились).
 
По правильному же это делается с помощью замыканий:
Javascript

var arg = 1;
element.onclick = (function() {func(arg);});
 
Обработчику присваиваем анонимную функцию, внутри которой вызывается нужная нам функция с нужным аргументом. Так как определение анонимной функции находится в том же контексте, где и живет переменная arg, то наша анонимная функция захватывает эту переменную и продолжает на неё ссылаться всё время своего существования.
Записан

vasa_c    ↓ 
22 Октябрь, 2007, 03:16:11 , спустя 12 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

Разделение переменных между замыканиями
 
Есть, допустим, у нас слой, а в нем ссылки:
HTML

<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").
Кажется, элементарно. Перебрали все ссылки в цикле и повесили на них обработчик, использовав наши любимые замыкания:
Javascript

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 и осталась с этим значением. Навсегда.
 
Что делать? Нужно каждому замыканию выделить свою переменную.
Javascript

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    ↓ 
22 Октябрь, 2007, 03:16:19 , спустя 8 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

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

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

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

obj.method();
 
означает, что функцию, на которую ссылается свойство "method" объекта obj нужно вызвать в контексте этого самого объекта. И ничего больше.
 
С одной стороны, "непривязанность" функций к объектам, очень гибкая и мощная возможность. С другой стороны не всё так здорово.
 
Обычная проблема:
Javascript

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

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

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

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

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
 
Ну и напоследок, эмуляция приватных свойств:
Javascript

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    ↓ 
22 Октябрь, 2007, 03:38:41 , спустя 22 минуты 22 секунды
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 89
Сообщений: 3423
Сила слова: 2.6

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

Как нах? Вроде PHP тоже всю жизнь позволяет ....
Наверно здесь надо разъяснить, что в PHP такой синтаксис допустим, но вот локальной области видимости внутренняя функция не приобретает.
« Последнее редактирование: 22 Октябрь, 2007, 03:42:58 от AlexB » Записан

vasa_c    ↓ 
22 Октябрь, 2007, 03:42:13 , спустя 3 минуты 32 секунды
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

Правда? Пример
Записан

AlexB    ↓ 
22 Октябрь, 2007, 03:46:34 , спустя 4 минуты 21 секунду
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 89
Сообщений: 3423
Сила слова: 2.6

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

function a()
{
    function b()
    {
        echo "test<br>";
    }
    b();
}
a();
b();
 
Записан

vasa_c    ↓ 
22 Октябрь, 2007, 03:50:21 , спустя 3 минуты 47 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 81
Сообщений: 2459
Сила слова: 3.29

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

if (f) {
    function func() { /* Тело */ }
} else {
    function func() { /* Другое тело */}
}
 
Записан

AlexB    ↓ 
22 Октябрь, 2007, 03:55:46 , спустя 5 минут 25 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: в ухо

Карма: 89
Сообщений: 3423
Сила слова: 2.6

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

Записан

Страниц: [1] 2
Печать
 

Перейти в: