Декларація функції JavaScript та порядок оцінки


80

Чому перший із цих прикладів не працює, а всі інші працюють?

// 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'); }
})();

Відповіді:


182

Це не проблема сфери застосування та не проблема закриття. Проблема полягає в розумінні між деклараціями та виразами .

Код 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: виконання. Інтерпретатор бачить, що ви хочете передати змінну someFunctionsetTimeout. І так воно і відбувається. На жаль, поточне значення 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 для розриву закриття (наприклад, у циклах).


7
Це була серйозно чудова відповідь.
Matt Briggs,

1
@Matt: Я пояснив це в іншому місці (кілька разів) на SO. Деякі з моїх улюблених пояснень: stackoverflow.com/questions/3572480 / ...
slebetman


3
@Matt: Технічно закриття включає не область дії, а фрейм стека (інакше відомий як запис активації). Закриття - це змінна, спільна для фреймів стека. Фрейм стека призначений для того, щоб визначити, що об’єкт є для класу. Іншими словами, область дії - це те, що програміст сприймає в структурі коду. Стек кадру - це те, що створюється під час виконання в пам'яті. Це насправді не так, але досить близько. Думаючи про поведінку під час виконання, розуміння на основі сфери деколи буває недостатньо.
slebetman

3
@slebetman для свого пояснення прикладу 3, ви згадуєте, що анонімна функція в setTimeout створює закриття змінної someFunction і що на даний момент someFunction все ще невизначена - що має сенс. Здається, єдина причина, чому приклад 3 не повертається невизначеним, - це функція setTimeout (затримка 10 мілісекунд дозволяє JavaScript виконувати наступний оператор присвоєння someFunction, таким чином роблячи його визначеним), правда?
wmock

2

Обсяг Javascript залежить від функцій, а не суто лексичного обсягу. це означає, що

  • Деяка функція1 визначається з початку функції, що обгороджує, але її вміст невизначений до призначення.

  • у другому прикладі присвоєння є частиною декларації, тому воно «рухається» вгору.

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

  • Четвертий приклад має як другу, так і третю причини роботи


1

Тому someFunction1що ще не призначено на момент виконання дзвінка до setTimeout().

someFunction3 може виглядати подібний випадок, але оскільки ви передаєте функцію обгортку , someFunction3()щоб setTimeout()в цьому випадку, виклик до someFunction3()не вирахував , поки пізніше.


Але someFunction2ще не призначено, коли дзвінок setTimeout()виконується, або ...?
We Are All Monica

1
@jnylen: Оголошення функції за допомогою functionключового слова не є точно еквівалентом присвоєння анонімної функції змінній. Функції, оголошені як function foo()"підняті" на початок поточної області дії, тоді як присвоєння змінних відбуваються в точці, де вони записані.
Чак

1
+1 для особливих функцій. Однак те, що це може спрацювати, не означає, що це слід робити. Завжди заявляйте перед використанням.
mway

@mway: у моєму випадку я організував свій код у межах "класу" за розділами: приватні змінні, обробники подій, приватні функції, потім публічні функції. Мені потрібен один із моїх обробників подій, щоб викликати одну з моїх приватних функцій. Для мене підтримка коду, організованого таким чином, виграє над упорядкуванням декларацій лексично.
We Are All Monica

1

Це звучить як основний випадок дотримання хорошої процедури, щоб уникнути неприємностей. Оголосіть змінні та функції перед тим, як використовувати їх, і оголосіть такі функції:

function name (arguments) {code}

Уникайте оголошувати їх var. Це просто неакуратно і призводить до проблем. Якщо у вас з’явиться звичка декларувати все, перш ніж це використовувати, більшість ваших проблем зникнуть дуже поспішно. При оголошенні змінних я б відразу ініціалізував їх дійсним значенням, щоб переконатись, що жодна з них не визначена. Я також схильний включати код, який перевіряє наявність дійсних значень глобальних змінних, перш ніж функція їх використовує. Це додатковий захист від помилок.

Технічні деталі того, як все це працює, нагадують фізику того, як працює ручна граната, коли ти граєш з нею. Моя проста порада - спочатку не грати з ручними гранатами.

Деякі прості декларації на початку коду можуть вирішити більшість подібних проблем, але все одно може знадобитися чистка коду.

Додаткова примітка:
Я провів кілька експериментів, і здається, що якщо ви оголосите всі свої функції способом, описаним тут, насправді не має значення, в якому порядку вони знаходяться. Якщо функція A використовує функцію B, функція B не повинна бути оголошеним перед функцією А.

Отже, спочатку заявіть про всі свої функції, далі про ваші глобальні змінні, а потім поставте інший код останнім. Дотримуйтесь цих правил, і ви не можете помилитися. Можливо, навіть краще розмістити свої декларації в голові веб-сторінки, а інший код - у тілі, щоб забезпечити дотримання цих правил.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.