Объявление и порядок оценки функции JavaScript

Почему первый из этих примеров не работает, а все остальные работают?

// 1 - does not work
(function() {
setTimeout(someFunction1, 10);
var someFunction1 = function() { alert('here1'); };
})();

// 2
(function() {
setTimeout(someFunction2, 10);
function someFunction2() { alert('here2'); }
})();

// 3
(function() {
setTimeout(function() { someFunction3(); }, 10);
var someFunction3 = function() { alert('here3'); };
})();

// 4
(function() {
setTimeout(function() { someFunction4(); }, 10);
function someFunction4() { alert('here4'); }
})();

person We Are All Monica    schedule 08.10.2010    source источник


Ответы (4)


Это не проблема области видимости и не проблема закрытия. Проблема заключается в понимании между объявлениями и выражениями.

Код JavaScript, поскольку даже первая версия JavaScript Netscape и первая его копия от Microsoft обрабатываются в два этапа:

Этап 1: компиляция - на этом этапе код компилируется в дерево синтаксиса (и байт-код или двоичный код в зависимости от движка).

Фаза 2: выполнение - затем интерпретируется проанализированный код.

Синтаксис для функции объявления:

function name (arguments) {code}

Аргументы, конечно, необязательны (код тоже необязателен, но какой в ​​этом смысл?).

Но JavaScript также позволяет создавать функции с помощью выражений. Синтаксис для функциональных выражений аналогичен объявлениям функций, за исключением того, что они написаны в контексте выражения. А выражения бывают:

  1. Все, что находится справа от знака = (или : в литералах объекта).
  2. Все, что указано в скобках ().
  3. Параметры функций (на самом деле это уже описано в 2).

Выражения в отличие от объявлений обрабатываются на этапе выполнения, а не на этапе компиляции. И от этого имеет значение порядок выражений.

Итак, чтобы уточнить:


// 1
(function() {
setTimeout(someFunction, 10);
var someFunction = function() { alert('here1'); };
})();

Фаза 1: компиляция. Компилятор видит, что переменная someFunction определена, и создает ее. По умолчанию все созданные переменные имеют значение undefined. Обратите внимание, что компилятор еще не может присвоить значения в этот момент, потому что значениям может потребоваться интерпретатор для выполнения некоторого кода, чтобы вернуть значение для назначения. И на этом этапе мы еще не выполняем код.

Фаза 2: исполнение. Интерпретатор видит, что вы хотите передать переменную someFunction в setTimeout. Так оно и есть. К сожалению, текущее значение someFunction не определено.


// 2
(function() {
setTimeout(someFunction, 10);
function someFunction() { alert('here2'); }
})();

Фаза 1: компиляция. Компилятор видит, что вы объявляете функцию с именем someFunction, и поэтому он ее создает.

Этап 2: интерпретатор видит, что вы хотите передать someFunction в setTimeout. Так оно и есть. Текущее значение someFunction - это его скомпилированное объявление функции.


// 3
(function() {
setTimeout(function() { someFunction(); }, 10);
var someFunction = function() { alert('here3'); };
})();

Фаза 1: компиляция. Компилятор видит, что вы объявили переменную someFunction, и создает ее. Как и раньше, его значение не определено.

Фаза 2: исполнение. Интерпретатор передает анонимную функцию в setTimeout для последующего выполнения. В этой функции он видит, что вы используете переменную someFunction, поэтому он закрывает переменную. На данный момент значение someFunction все еще не определено. Затем он видит, что вы назначаете функцию someFunction. На данный момент значение someFunction больше не является неопределенным. Спустя 1/100 секунды срабатывает setTimeout и вызывается someFunction. Поскольку его значение больше не является неопределенным, он работает.


Случай 4 на самом деле является другой версией случая 2 с добавлением части случая 3. В момент, когда someFunction передается в setTimeout, он уже существует из-за того, что он объявлен.


Дополнительные пояснения:

Вы можете задаться вопросом, почему setTimeout(someFunction, 10) не создает замыкание между локальной копией someFunction и копией, переданной в setTimeout. Ответ на этот вопрос заключается в том, что аргументы функции в JavaScript всегда всегда передаются по значению, если они являются числами или строками, или по ссылке для всего остального. Таким образом, setTimeout фактически не получает переданную ему переменную someFunction (что означало бы создание замыкания), а только получает объект, на который ссылается someFunction (который в данном случае является функцией). Это наиболее широко используемый механизм в JavaScript для взлома замыканий (например, в циклах).

person slebetman    schedule 08.10.2010
comment
Это был действительно отличный ответ. - person Matt Briggs; 08.10.2010
comment
Вероятно, это непонимание замыканий, но я всегда думал об этом как о доступе к области видимости, а не как о создании чего-то между одной областью видимости и другой. Я также думал, что это было на уровне объема, а не на уровне переменных. Не могли бы вы подробнее рассказать об этом или указать мне на то, что я могу прочитать? Опять же, отличный ответ, хотел бы я проголосовать дважды. - person Matt Briggs; 08.10.2010
comment
Этот ответ побуждает меня голосовать за один и тот же ответ несколько раз. Поистине отличный ответ. Спасибо - person ArtBIT; 08.10.2010
comment
@Matt: Я объяснял это в другом месте (несколько раз) на SO. Некоторые из моих любимых объяснений: stackoverflow.com/questions/3572480/ - person slebetman; 08.10.2010
comment
@Matt: Также: stackoverflow.com/questions/61088 / hidden-features-of-javascript / - person slebetman; 08.10.2010
comment
@Matt: Технически закрытие включает не область видимости, а фрейм стека (также известный как запись активации). Замыкание - это переменная, совместно используемая между кадрами стека. Фрейм стека предназначен для определения класса объекта. Другими словами, область видимости - это то, что программист воспринимает в структуре кода. Фрейм стека - это то, что создается во время выполнения в памяти. Это не совсем так, но достаточно близко. Когда вы думаете о поведении во время выполнения, иногда недостаточно понимания на основе области видимости. - person slebetman; 08.10.2010
comment
@slebetman: Это имеет смысл, спасибо :) По аналогии, замыкание будет частной статической переменной (в терминологии Java), верно? Думаю, это то, что я получил, узнав о них на таких языках, как javascript и ruby, а не на реальном функциональном языке. - person Matt Briggs; 08.10.2010
comment
Спасибо. Отредактировал заголовок вопроса и теги, чтобы отразить тот факт, что на самом деле речь не идет о закрытии. - person We Are All Monica; 08.10.2010
comment
@slebetman для вашего объяснения примера 3, вы упоминаете, что анонимная функция в setTimeout создает закрытие для переменной someFunction и что на этом этапе someFunction все еще не определен, что имеет смысл. Кажется, что единственная причина, по которой пример 3 не возвращает undefined, - это функция setTimeout (задержка в 10 миллисекунд позволяет JavaScript выполнить следующий оператор присваивания для someFunction, тем самым делая его определенным), верно? - person wmock; 28.03.2013
comment
Вы говорите: name and arguments are of course optional в объяснении вашей функции definition. Это сбивает с толку ... MDN, MSDN, ES3, ES5 и все книги, которые я читал, противоречат этому утверждению и говорят, что в объявлении функции необязательны только аргументы: function Identifier ( FormalParameterList opt ){ FunctionBody } (так что идентификатор является обязательным), но FunctionExpression: function Identifier opt ( FormalParameterList opt ){ FunctionBody } Итак, что я Я скучаю? - person GitaarLAB; 28.03.2013
comment
@GitaarLAB: Без идентификатора это все еще законно, только я считаю, что он скомпилирован как выражение вместо объявления. На данный момент ни один браузер не выдаст ошибку, если результат выражения будет отброшен (не назначен с помощью оператора =) - person slebetman; 29.03.2013
comment
@GitaarLAB: Когда я писал свой ответ, я основывал его не на какой-либо документации или спецификации, а на тестовом скрипте, который я тестировал в IE, FF, Chrome и Opera. Ни один из протестированных мной браузеров не генерировал никаких сообщений об ошибках, когда я объявлял функцию без идентификатора. - person slebetman; 29.03.2013
comment
@GitaarLAB: только что протестировал его в последней версии Chrome, и, похоже, теперь он выдает ошибку, если идентификатор не указан. Так что мой ответ кажется устаревшим. Я это исправлю. - person slebetman; 29.03.2013
comment
@slebetman: Спасибо за это! У вас есть ссылка, возможно, на документацию по движку браузера для двух этапов? Я пытался получить больше информации о этапе компиляции и о том, что именно происходит на этом этапе, и мне не удалось найти хороших объяснений. Спасибо! - person Elisabeth; 15.01.2014
comment
На этапе 1 компиляция не выполняется. Не могли бы вы переименовать это во что-нибудь более подходящее, например инициализацию контекста вызова, настройку фрейма стека или создание области? - person Bergi; 25.12.2020
comment
@Bergi Настройка кадра стека происходит после фазы 1. Если фаза 1 - это настройка кадра стека, то я имею в виду фазу 0 - компиляцию. Единственный известный мне современный язык, у которого нет фазы компиляции, - это языки оболочки - bash, sh и т. Д. Даже Tcl имеет компиляцию с начала 2000-х годов. Javascript был языком байт-кода с момента первой реализации Eich (конечно, исходный синтаксис больше походил на Lisp, но эта версия так и не была выпущена) - person slebetman; 25.12.2020
comment
@Bergi Возможно, если вам не нравится компиляция, вы можете думать об этом как о синтаксическом анализе + генерации байт-кода, но в CS синтаксический анализ + генерация байт-кода обычно называется компиляцией - person slebetman; 25.12.2020
comment
@slebetman Хорошо, но если вы действительно имеете в виду фазу 0, то ответ неверен. Выражения, в отличие от объявлений, обрабатываются на этапе выполнения, а не на этапе компиляции. - выражения (должны быть) проанализированы (и преобразованы в байт-код) также на первом проходе. Компилятор видит, что вы объявили [что-то], и создает это - но этого не происходит на этапе 0, это происходит на этапе 1, когда область видимости инициализируется, когда вызывается функция. Код компилируется только один раз, переменные создаются столько раз, сколько необходимо. - person Bergi; 25.12.2020

Область видимости Javascript основана на функциях, а не строго на лексической области видимости. что означает, что

  • Somefunction1 определяется с начала включающей функции, но ее содержимое не определено, пока не будет присвоено.

  • во втором примере присвоение является частью объявления, поэтому оно «перемещается» наверх.

  • в третьем примере переменная существует, когда определено анонимное внутреннее замыкание, но она используется только через 10 секунд, после чего значение будет присвоено.

  • В четвертом примере есть как вторая, так и третья причины работать

person Javier    schedule 08.10.2010

Поскольку someFunction1 еще не был назначен на момент выполнения вызова setTimeout().

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

person matt b    schedule 08.10.2010
comment
Но someFunction2 еще не был назначен, когда выполняется вызов setTimeout() ...? - person We Are All Monica; 08.10.2010
comment
@jnylen: объявление функции с ключевым словом function не совсем эквивалентно назначению анонимной функции переменной. Функции, объявленные как function foo(), поднимаются в начало текущей области, а присвоение переменных происходит в той точке, где они записаны. - person Chuck; 08.10.2010
comment
+1 за особые функции. Однако то, что это может работать, не означает, что это нужно делать. Всегда заявляйте перед использованием. - person mway; 08.10.2010
comment
@mway: в моем случае я организовал свой код внутри класса по разделам: частные переменные, обработчики событий, частные функции, затем общедоступные функции. Мне нужен один из моих обработчиков событий для вызова одной из моих частных функций. Для меня такая организация кода важнее лексического упорядочивания объявлений. - person We Are All Monica; 08.10.2010

Это звучит как простой пример следования правильной процедуре, чтобы избежать неприятностей. Объявите переменные и функции перед их использованием и объявите такие функции:

function name (arguments) {code}

Избегайте объявления их с помощью var. Это просто небрежно и приводит к проблемам. Если вы привыкнете все декларировать перед использованием, большинство ваших проблем исчезнет в очень большой спешке. При объявлении переменных я бы сразу инициализировал их допустимым значением, чтобы убедиться, что ни одна из них не является неопределенной. Я также стараюсь включать код, который проверяет допустимые значения глобальных переменных до того, как функция их использует. Это дополнительная защита от ошибок.

Технические детали того, как все это работает, похожи на физику того, как работает ручная граната, когда вы играете с ней. Мой простой совет - вообще не играть с ручными гранатами.

Некоторые простые объявления в начале кода могут решить большинство подобных проблем, но некоторая очистка кода все же может потребоваться.

Дополнительное примечание:
Я провел несколько экспериментов, и мне кажется, что если вы объявляете все свои функции описанным здесь способом, на самом деле не имеет значения, в каком порядке они расположены. Если функция A использует функцию B, функцию B необязательно объявлять перед функцией A.

Итак, сначала объявите все свои функции, затем свои глобальные переменные, а затем поместите другой код в последнюю очередь. Следуйте этим практическим правилам, и вы не ошибетесь. Возможно, лучше всего будет поместить свои объявления в заголовок веб-страницы, а другой код - в тело, чтобы обеспечить соблюдение этих правил.

person Terry Prothero    schedule 24.07.2012