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».
«Yes, really» будет везде, кроме Firefox-а. И в Опере, и в Сафари, и в IE.
http://dmitry.baranovskiy.com/post/36156571
И замыкания я здесь не вижу.
Так даже нагляднее:
really();
function really() { alert(1) }
if (1) function really() { alert(2) }
If не создаёт новой области видимости, значит второе определение должно перетирать первое. И Firefox ведёт себя неправильно.
Привязка переменных должна делаться только в момент входа в контекст выполнения:
lol = 1; (function () { alert(lol); var lol; })();
В PHP, конечно, царит шизофрения. Например, функции внутри if создаются по мере выполнения кода, а функции после безусловного exit — в момент инициализации.
Комментарий для elv1s.ru:
OMG! Я всегда считал, что function really() {} полный эквивалент var really = function () {}
Замыкание — это функция внутри функции со своим окружением. То, что здесь окружение внутри функции не используется, мне кажется частным случаем замыкания.
Комментарий для alisey.myopenid.com:
В PHP до версии 5.3 нет лямбд, так что о чём речь?
Комментарий для elv1s.ru:
Кстати, ты не заглядывал в стандарт? FF точно себя неправильно ведёт?
Нашёл, ECMA-262. В момент входа в контекст выполнения (Global, Function, Eval), создаётся объект с переменными для этого контекста. Сначала туда заносятся формальные параметры, потом определения функций, в том порядке, как они появляются в коде, их значением становится функция, созданная на основе определения в коде. Если имя занято, то значение перетирается. Потом назначаются переменные (var), со значением undefined. И если имя уже занято, то привязка не меняется.
Комментарий для Евгения Степанищева:
Причём здесь лямбды? Речь о порядке инициализации и области видимости. И замыкание — это не «функция внутри функции». Замыкание — это функция, с привязкой переменных на основе лексического контекста. Она может быть совсем даже снаружи, а не внутри.
Некоторые вообще предпочитают называть замыканием не саму функцию, а только эту привязку.
i = 1;
function really() { alert(i) };
function lol(){ var i = 2; really();} // единица, замыкание есть, «функций внутри функции» нет
lol();
Комментарий для alisey.myopenid.com:
Я упрощённо говорю, ага.
Я подумал, что речь идёт о create_function, только сейчас догадался, что речь идти может и о function вложенном в function.
Меня волнует моё невольно заблуждение. Я всегда считал, что два упоминаемых описания эквиваленты в языке. По стандарту это так или нет?
Комментарий для Евгения Степанищева:
По стандарту это не так, что является хорошим дизайнерским решением.
<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 почему-то так не считает.
Комментарий для alisey.myopenid.com:
Я где-то прочитал и запомнил, что два этих способа определить функцию равнозначны, теперь вижу, что нет. Спасибо!
P.S. заметку поправил.
Комментарий для alisey.myopenid.com:
Рассказывать что есть такой тип «функция» не нужно. Из традиционного, я давно пишу на JS, писал на Pascal, Perl, где есть такой тип и недавно — на Python.
Комментарий для Евгения Степанищева:
В Пайтоне и некоторых версиях Javascript ещё есть yield, вроде базовые вещи, не понимаю как большинство «прекрасно обходится и без них».
Комментарий для alisey.myopenid.com:
В Python/JS 2.0 есть yield, да. Это удобно.
Комментарий для Евгения Степанищева:
Ну вот, всё уже объяснили ) Положу свою копейку:
В 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
Комментарий для Евгения Степанищева:
Даже в JS 1.7, их можно вывернуть наизнанку и сделать что-то вроде:
var event = yield waitForMouseClick();
alert(event.x);
Комментарий для 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.myopenid.com:
Мне вот больше интересно, как это так:
function a() { return 1 }; if (1) { function a() { return 2 }; } a(); => 2
Комментарий для jahson.livejournal.com:
Это в глобальной области видимости.
А теперь что-то вообще чудесное (FF):
(function() {
function a() { return 1 };
if (1) function a() { return 2 };
return a();
})(); // 1
В эпоху NN функция внутри if() легко и непринуждённо объявлялась (в стиле IE). В дальнейшем от этого отказались, но чтобы не ломать дурацкий код добавили что-то вроде экстеншна к движку. Подаётся это обычно или под соусом 16-ого раздела экмы, где разрешено отступать от буквы стандарта, или под соусом, который описал выше jahson (про source elements)...
Комментарий для 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() {}
}
}
Комментарий для zeroglif.myopenid.com:
Ох уж эта экма )
Комментарий для jahson.livejournal.com:
Синтаксис JS я знаю. Я уже объяснил что для меня было непонятным.
Комментарий для 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 понял как работают прототипы и конструкторы. Спасибо.
Комментарий для Евгения Степанищева:
Именно.
Комментарий для alisey.myopenid.com:
А если считать, что первая — declaration, а вторая — expression? Я там выше привёл пример.
Комментарий для 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
Результат снова два. Я не вижу логики.
Объявленная функция (FD) должна быть или внутри функции, или выше всех (в Program), внутри statement-a типа if(1){} функцию объявить нельзя. Одновременно с этим функция-выражение (FE) может быть до-фига-где, но только вот нельзя ей быть в качестве ExpressionStatement, там не должно быть слова function в начале. Отсюда выходит, что внутри if(1){} и не FD, и не FE, а... отложенное объявление, это расширение, добавленное в Mz лет 9-10 назад, с тех пор им никто и не пользуется... ;)