Это сайт — моя персональная записная книжка. Интересна мне, по большей части, история, своя жизнь и немного программирование.

JavaScript: баг с замыканием в FireFox

Какой alert должен показать следующий JavaScript в браузере?

<html>
<head>
<script>
/* When are functions defined? */
function really() { alert("Original"); }
if (0) {
    alert("No");
    function really() { alert("Yes, really"); }
}
really();
</script>
</head>
<body>Really</body>
</html>

Поведение тут зависит от браузера, FF выведет «Original», а остальные браузеры — «Yes, really».

29 комментариев
ELV1S (elv1s.ru) 2008

«Yes, really» будет везде, кроме Firefox-а. И в Опере, и в Сафари, и в IE.

http://dmitry.baranovskiy.com/post/36156571

ELV1S (elv1s.ru) 2008

И замыкания я здесь не вижу.

Alisey (alisey.myopenid.com) 2008

Так даже нагляднее:
really();
function really() { alert(1) }
if (1) function really() { alert(2) }

If не создаёт новой области видимости, значит второе определение должно перетирать первое. И Firefox ведёт себя неправильно.

Привязка переменных должна делаться только в момент входа в контекст выполнения:
lol = 1; (function () { alert(lol); var lol; })();

В PHP, конечно, царит шизофрения. Например, функции внутри if создаются по мере выполнения кода, а функции после безусловного exit — в момент инициализации.

Евгений Степанищев (bolknote.ru) 2008

Комментарий для elv1s.ru:

OMG! Я всегда считал, что function really() {} полный эквивалент var really = function () {}

Замыкание — это функция внутри функции со своим окружением. То, что здесь окружение внутри функции не используется, мне кажется частным случаем замыкания.

Евгений Степанищев (bolknote.ru) 2008

Комментарий для alisey.myopenid.com:

В PHP до версии 5.3 нет лямбд, так что о чём речь?

Евгений Степанищев (bolknote.ru) 2008

Комментарий для elv1s.ru:

Кстати, ты не заглядывал в стандарт? FF точно себя неправильно ведёт?

Alisey (alisey.myopenid.com) 2008

Нашёл, ECMA-262. В момент входа в контекст выполнения (Global, Function, Eval), создаётся объект с переменными для этого контекста. Сначала туда заносятся формальные параметры, потом определения функций, в том порядке, как они появляются в коде, их значением становится функция, созданная на основе определения в коде. Если имя занято, то значение перетирается. Потом назначаются переменные (var), со значением undefined. И если имя уже занято, то привязка не меняется.

Alisey (alisey.myopenid.com) 2008

Комментарий для Евгения Степанищева:

В PHP до версии 5.3 нет лямбд, так что о чём речь?

Причём здесь лямбды? Речь о порядке инициализации и области видимости. И замыкание — это не «функция внутри функции». Замыкание — это функция, с привязкой переменных на основе лексического контекста. Она может быть совсем даже снаружи, а не внутри.
Некоторые вообще предпочитают называть замыканием не саму функцию, а только эту привязку.

i = 1;
function really() { alert(i) };
function lol(){ var i = 2; really();} // единица, замыкание есть, «функций внутри функции» нет
lol();

Евгений Степанищев (bolknote.ru) 2008

Комментарий для alisey.myopenid.com:

И замыкание — это не «функция внутри функции».

Я упрощённо говорю, ага.

Причём здесь лямбды?

Я подумал, что речь идёт о create_function, только сейчас догадался, что речь идти может и о function вложенном в function.

Нашёл, ECMA-262.

Меня волнует моё невольно заблуждение. Я всегда считал, что два упоминаемых описания эквиваленты в языке. По стандарту это так или нет?

Alisey (alisey.myopenid.com) 2008

Комментарий для Евгения Степанищева:

По стандарту это не так, что является хорошим дизайнерским решением.
<script>
a();
function a(){ alert(1) }
a = function(){ alert(2) }
a();
</script>

Как это работает: попадаем в новую область видимости, в этом конкретном случае Global. [...много предварительных действий...], ищем декларации функций. function a()... — это декларация. Запоминаем привязку имя -> тело функции. Больше объявлений функций нет, теперь ищем переменные. Есть объявление переменной a. Если бы это имя уже не ссылалось на функцию, мы бы инициализировали его undefined, а так — пропускаем.
Теперь, когда все имена привязаны, начинаем выполнять код. a() выдаёт единицу, что подтверждает идею о том, что декларации функций обрабатываются до начала выполнения кода. Доходим до строки с присвоением: a = ..., в результате присвоения «a» начинает ссылаться на анонимную функцию, и следующая строка выдаёт 2.

Это вроде бы очевидно — присвоение происходит в момент выполнения кода.
Но может быть неочевидно, что вот это тоже присвоение: var a = function(){ }
И это: a = true ? alert : 9; a(5);
Функции — такие же значения как числа и строки.
Можно в массив запихнуть: [function() { alert(1) }, function() { alert(2) }][1]();

А вот у деклараций (и переменных, и функций) есть та особенность, что они инициализируются в момент вхождения в новую область видимости. В вашем первом примере if не создаёт новой области видимости, это можно проверить задав в нём какой-нибудь var, контекст останется тот же. Значит декларация должна обрабатываться ещё до этапа выполнения. Но Firefox почему-то так не считает.

Евгений Степанищев (bolknote.ru) 2008

Комментарий для alisey.myopenid.com:

Я где-то прочитал и запомнил, что два этих способа определить функцию равнозначны, теперь вижу, что нет. Спасибо!

P.S. заметку поправил.

Евгений Степанищев (bolknote.ru) 2008

Комментарий для alisey.myopenid.com:

Рассказывать что есть такой тип «функция» не нужно. Из традиционного, я давно пишу на JS, писал на Pascal, Perl, где есть такой тип и недавно — на Python.

Alisey (alisey.myopenid.com) 2008

Комментарий для Евгения Степанищева:

В Пайтоне и некоторых версиях Javascript ещё есть yield, вроде базовые вещи, не понимаю как большинство «прекрасно обходится и без них».

Евгений Степанищев (bolknote.ru) 2008

Комментарий для alisey.myopenid.com:

В Python/JS 2.0 есть yield, да. Это удобно.

jahson.livejournal.com 2008

Комментарий для Евгения Степанищева:

Ну вот, всё уже объяснили ) Положу свою копейку:

В JavaScript, по-сути, три способа объявлять функции — function statement, function expression и function constructor, не буду заморачиваться переводом. Первый — это пресловутый function name( [params] ) { body }, второй — function [name] ( [params] ) { body }, третий — new Function(’args’, ’body’). Первый используется для объявления «нормальных» (не знаю, как назвать иначе) функций, а второй — используется внутри выражений (expressions). Про третий забудем, единственное, что нужно отметить — у функций созданных этим способом нет имени и они наследуют только глобальную область видимости %)

Function statement хорош тем, что может быть использован до появления — почему уже описали. Т. е. даже function a() { return b(); function b() { return 1; }; }; отработает и вернёт 1.

Function expression создаёт анонимную функцию, но наша анонимная функция может иметь имя: var x = function y() { alert(x); alert(y); };, притом о настоящем имени функции никто, кроме неё не знает.

Firefox вообще говоря, прав, исходя из написанного на mdc определения function expression:
A function declaration is very easily (and often unintentionally) turned into a function expression. A function declaration ceases to be one when it either:

    * becomes part of an expression
    * is no longer a «source element» of a function or the script itself. A «source element» is a non-nested statement in the script or a function body:

      var x = 0; // source element
      if (x == 0) { // source element
         x = 10; // not a source element
         function boo() {} // not a source element
      }
      function foo() { // source element
         var y = 20; // source element
         function bar() {} // source element
         while (y == 10) { // source element
            function blah() {} // not a source element
            y++; // not a source element
         }
      }
https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Functions

Alisey (alisey.myopenid.com) 2008

Комментарий для Евгения Степанищева:

Даже в JS 1.7, их можно вывернуть наизнанку и сделать что-то вроде:

var event = yield waitForMouseClick();
alert(event.x);

Alisey (alisey.myopenid.com) 2008

Комментарий для jahson.livejournal.com:

Взял яваскриптовый движок, написанный на яваскрипте автором яваскрипта.
http://mxr.mozilla.org/mozilla/source/js/narcissus/

console.log(evaluate(’function a() { return 1 }; if (0) function a() { return 2 }; a();’));

Действительно возвращает единицу. Не могу найти в коде, как это происходит.

Alisey (alisey.myopenid.com) 2008

Комментарий для Евгения Степанищева:

Впору переименовывать заметку :)

jahson.livejournal.com 2008

Комментарий для alisey.myopenid.com:

Мне вот больше интересно, как это так:
function a() { return 1 }; if (1) { function a() { return 2 }; } a(); => 2

Alisey (alisey.myopenid.com) 2008

Комментарий для jahson.livejournal.com:

Это в глобальной области видимости.
А теперь что-то вообще чудесное (FF):

(function() {
  function a() { return 1 };
  if (1) function a() { return 2 };
  return a();
})(); // 1

zeroglif.myopenid.com 2008

В эпоху NN функция внутри if() легко и непринуждённо объявлялась (в стиле IE). В дальнейшем от этого отказались, но чтобы не ломать дурацкий код добавили что-то вроде экстеншна к движку. Подаётся это обычно или под соусом 16-ого раздела экмы, где разрешено отступать от буквы стандарта, или под соусом, который описал выше jahson (про source elements)...

jahson.livejournal.com 2008

Комментарий для alisey.myopenid.com:

Так понятней выйдет:
(function() {
if (1) function a() { return 2 };
return a();
function a() { return 1 };
})();

Почему так выходит — скорее всего из-за от этого

// function statement
function a() {
   // function statement
   function b() {}
   if (0) {
      // function expression
      function c() {}
   }
}

jahson.livejournal.com 2008

Комментарий для zeroglif.myopenid.com:

Ох уж эта экма )

Евгений Степанищев (bolknote.ru) 2008

Комментарий для jahson.livejournal.com:

Синтаксис JS я знаю. Я уже объяснил что для меня было непонятным.

Alisey (alisey.myopenid.com) 2008

Комментарий для zeroglif.myopenid.com:

но чтобы не ломать дурацкий код...

Так какой же код официально «дурацкий»? IE-style, Mozilla-style?

(function() {
function a() { return 1 };
if (1) function a() { return 2 };
return a();
})(); // 1 в FF3

Если считать, что вторая функция a() — это Declaration, то она переопределяет первую на этапе инициализации, результат 2. Если считать её Expression — она переопределяет первую на этапе выполнения. (Обратите внимание, условие выполняется). Опять результат 2.
Движок Брендана Айка выдаёт 2, зато SpiderMonkey — единицу. Непонятно что это за «экстеншен» такой.

Кстати :) По вашим комментариям на dklab понял как работают прототипы и конструкторы. Спасибо.

jahson.livejournal.com 2008

Комментарий для Евгения Степанищева:

Именно.

jahson.livejournal.com 2008

Комментарий для alisey.myopenid.com:

А если считать, что первая — declaration, а вторая — expression? Я там выше привёл пример.

Alisey (alisey.myopenid.com) 2008

Комментарий для jahson.livejournal.com:

Да, рассмотрим её как Exression, то есть из-за if констукция будет обрабатываться как
if (1) var a = function () { return 2 };

Окажется, что если перевести в такую форму, то результат станет 2, вполне ожидаемый результат, выражение обработалось, значение присвоилось. Почему тогда в первом случае не так?
Вы можете сказать, что это не совсем правильная аналогия, а правильно будет
if (1) function /a/ () { return 2 };

Вроде бы тогда всё логично, функция есть, но она не используется. Результат 1.
Но есть одна вещь, которая ломает эту теорию:
(function() {
//function a() { return 1 };
if (1) function a() { return 2 };
return a();
})(); // 2!!!

Ещё один эксперимент, убираем обёрточую функцию:
function a() { return 1 };
if (1) function a() { return 2 };
a(); // 2

Результат снова два. Я не вижу логики.

zeroglif.myopenid.com 2008

Объявленная функция (FD) должна быть или внутри функции, или выше всех (в Program), внутри statement-a типа if(1){} функцию объявить нельзя. Одновременно с этим функция-выражение (FE) может быть до-фига-где, но только вот нельзя ей быть в качестве ExpressionStatement, там не должно быть слова function в начале. Отсюда выходит, что внутри if(1){} и не FD, и не FE, а... отложенное объявление, это расширение, добавленное в Mz лет 9-10 назад, с тех пор им никто и не пользуется... ;)