Вместо того, чтобы бросать код на вас, есть два понятия, которые являются ключом к пониманию того, как JS обрабатывает обратные вызовы и асинхронность. (это даже слово?)
Есть три вещи, о которых вам нужно знать; Очередь; цикл события и стек
. В широких упрощенных терминах цикл событий подобен диспетчеру проекта, он постоянно прослушивает любые функции, которые хотят запускать и взаимодействовать между очереди и стека.
while (queue.waitForMessage()) {
queue.processNextMessage();
}
Как только он получает сообщение для запуска чего-то, он добавляет его в очередь. Очередь - это список вещей, которые ждут выполнения (например, ваш запрос AJAX). Представьте себе это так:
1. call foo.com/api/bar using foobarFunc
2. Go perform an infinite loop
... and so on
Когда одно из этих сообщений будет исполнено, оно выталкивает сообщение из очереди и создает стек, стек - это все, что нужно выполнить JS для выполнения инструкции в сообщение. Таким образом, в нашем примере ему говорят позвонить foobarFunc
function foobarFunc (var) {
console.log(anotherFunction(var));
}
. Так что все, что foobarFunc должно выполнить (в нашем случае anotherFunction
), будет вставлено в стек. исполняемый, а затем забытый - цикл события затем переместится на следующую вещь в очереди (или прослушивает сообщения)
. Главное здесь - порядок выполнения. Это
Когда вы совершаете вызов с использованием AJAX для внешней стороны или выполняете любой асинхронный код (например, setTimeout), Javascript зависит от ответ, прежде чем он сможет продолжить.
Большой вопрос, когда он получит ответ? Ответ в том, что мы не знаем, поэтому цикл событий ждет, когда это сообщение скажет: «Эй, забери меня». Если JS просто ждал этого сообщения синхронно, ваше приложение замерзнет, и оно сосать. Таким образом, JS продолжает выполнение следующего элемента в очереди, ожидая, пока сообщение не будет добавлено обратно в очередь.
Вот почему с асинхронной функциональностью мы используем вещи, называемые обратными вызовами. Это похоже на обещание буквально. Как и в I , обещание что-то вернуть в какой-то момент jQuery использует специальные обратные вызовы, называемые deffered.done
deffered.fail
и deffered.always
(среди других). Вы можете увидеть их все здесь
Итак, вам нужно передать функцию, которая в какой-то момент будет выполнена с переданными ей данными.
Поскольку обратный вызов не выполняется немедленно, но в более позднее время важно передать ссылку на функцию, которую она не выполнила. поэтому
function foo(bla) {
console.log(bla)
}
, поэтому большую часть времени (но не всегда) вы пройдете foo
не foo()
. Надеюсь, это будет иметь смысл. Когда вы сталкиваетесь с такими вещами, которые кажутся запутанными, я настоятельно рекомендую полностью прочитать документацию, чтобы хотя бы понять ее. Это сделает вас намного лучшим разработчиком.
Вот список объяснений результатов, которые вы видите (и должен их видеть). Ссылки, которые я использую, из стандарта ECMA-262 .
[] + []
toString()
методом является результатом вызова object.toString()
( §8.12.8 ). Для массивов это то же самое, что и вызов array.join()
( §15.4.4.2 ). Объединение пустого массива приводит к пустой строке, поэтому шаг # 7 оператора сложения возвращает конкатенацию двух пустых строк, которая является пустой строкой. [] + {}
[] + []
оба операнда сначала преобразуются в примитивы. Для «Объектных объектов» (§15.2) это снова является результатом вызова object.toString()
, который для непустых не неопределенных объектов является "[object Object]"
( §15.2.4.2 ). {} + []
{}
не анализируется как объект, а вместо этого как пустой блок ( §12.1 , по крайней мере, до тех пор, пока вы не заставляете это выражение быть выражением, но об этом позже). Возвращаемое значение пустых блоков пуст, поэтому результат этого оператора совпадает с +[]
. Унарный +
оператор ( §11.4.6 ) возвращает ToNumber(ToPrimitive(operand))
. Как мы уже знаем, ToPrimitive([])
является пустой строкой, и согласно §9.3.1 , ToNumber("")
равен 0. {} + {}
{}
анализируется как блок с пустым возвращаемым значением. Опять же, +{}
совпадает с ToNumber(ToPrimitive({}))
, а ToPrimitive({})
- "[object Object]"
(см. [] + {}
). Поэтому, чтобы получить результат +{}
, мы должны применить ToNumber
к строке "[object Object]"
. Следуя шагам из §9.3.1 , мы получаем NaN
в результате: если грамматика не может интерпретировать строку как расширение StringNumericLiteral , то результат ToNumber - NaN. Array(16).join("wat" - 1)
Array(16)
создает новый массив с длиной 16. Чтобы получить значение аргумента для соединения, §11.6.2 шаги # 5 и # 6 показывают, что нам нужно преобразовать оба операнда в число, используя ToNumber
. ToNumber(1)
является просто 1 ( §9.3 ), тогда как ToNumber("wat")
снова NaN
в соответствии с §9.3.1 . Следуя шагу 7 из §11.6.2 , §11.6.3 диктует, что если любой из операндов равен NaN, результатом является NaN. Поэтому аргументом Array(16).join
является NaN
. Следуя § 15.4.4.5 (Array.prototype.join
), мы должны называть ToString
аргументом, который является "NaN"
( §9.8.1 ): если m является NaN, возвращает строку "NaN"
. Следуя шагу 10 из §15.4.4.5 , мы получаем 15 повторений конкатенации "NaN"
и пустой строки, что равно результату, который вы видите. При использовании параметра "wat" + 1
вместо "wat" - 1
в качестве аргумента оператор сложения преобразует 1
в строку вместо преобразования "wat"
в число, поэтому он эффективно вызывает Array(16).join("wat1")
. Что касается того, почему вы видите разные результаты для случая {} + []
: при использовании в качестве аргумента функции вы вынуждаете оператор быть ExpressionStatement , что делает невозможным синтаксический анализ {}
как пустой блок, поэтому вместо этого он анализируется как пустой литерал объекта.
Чтобы подкрепить то, что было ранее ранее.
Основная причина этого поведения частично объясняется слабо типизированным характером JavaScript. Например, выражение 1 + «2» неоднозначно, поскольку существуют две возможные интерпретации, основанные на типах операндов (int, string) и (int int):
Таким образом, с различными типами ввода возможности вывода увеличиваются.
Алгоритм сложения
Примитивы JavaScript - это строка, число, значение null, undefined и boolean (Symbol скоро появится в ES6). Любое другое значение - это объект (например, массивы, функции и объекты). Процесс принуждения для преобразования объектов в примитивные значения описывается таким образом:
Примечание: для значений даты , порядок должен вызывать toString до valueOf.
. Знание различных значений принуждения типов в JavaScript помогает сделать путаные результаты более ясными. См. Таблицу принуждения ниже
+-----------------+-------------------+---------------+
| Primitive Value | String value | Numeric value |
+-----------------+-------------------+---------------+
| null | “null” | 0 |
| undefined | “undefined” | NaN |
| true | “true” | 1 |
| false | “false” | 0 |
| 123 | “123” | 123 |
| [] | “” | 0 |
| {} | “[object Object]” | NaN |
+-----------------+-------------------+---------------+
. Также хорошо знать, что оператор JavaScript + является лево-ассоциативным, поскольку это определяет, какие результаты будут иметь случаи, связанные с более чем одной операцией.
Использование таким образом 1 + «2» даст «12», потому что любое добавление, включающее строку, всегда будет по умолчанию для конкатенации строк.
Вы можете прочитать больше примеров в в этом сообщении в блоге (отказ от ответственности я написал).
I второе решение Ventero. Если вы хотите, вы можете подробнее рассказать о том, как +
преобразует свои операнды.
Первый шаг (§9.1): конвертировать оба операнда в примитивы (примитивные значения: undefined
, null
, booleans, numbers, strings, все остальные значения - объекты, включая массивы и функции). Если операнд уже примитивен, все готово. Если нет, это объект obj
, и выполняются следующие шаги:
obj.valueOf()
. Если он возвращает примитив, вы закончите. Прямые экземпляры Object
и массивы возвращаются, поэтому вы еще не закончили. obj.toString()
. Если он возвращает примитив, вы закончите. {}
и []
оба возвращают строку, поэтому вы закончили. TypeError
. Для дат, шагов 1 и 2 меняются местами. Вы можете наблюдать поведение преобразования следующим образом:
var obj = {
valueOf: function () {
console.log("valueOf");
return {}; // not a primitive
},
toString: function () {
console.log("toString");
return {}; // not a primitive
}
}
Взаимодействие (Number()
сначала преобразуется в примитив, а затем в число):
> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value
Второй шаг (§11.6.1 ): Если один из операндов является строкой, другой операнд также преобразуется в строку, и результат получается путем объединения двух строк. В противном случае оба операнда преобразуются в числа, и результат получается путем их добавления.
Более подробное объяснение процесса преобразования: « Что такое {} + {} в JavaScript? «
Это скорее комментарий, чем ответ, но по какой-то причине я не могу прокомментировать ваш вопрос. Я хотел исправить код JSFiddle. Тем не менее, я опубликовал это в Hacker News, и кто-то предложил мне его перепечатать.
Проблема в коде JSFiddle заключается в том, что ({})
(открытие скобок внутри круглых скобок) не совпадает с {}
(открытие скобок как начало строки кода). Поэтому, когда вы вводите out({} + [])
, вы вынуждаете {}
быть тем, чего нет, когда вы набираете {} + []
. Это часть общей «ваттности» Javascript.
Основная идея заключалась в простом JavaScript, который хотел бы разрешить обе эти формы:
if (u)
v;
if (x) {
y;
z;
}
Чтобы сделать это, два были сделаны интерпретации открывающей скобки: 1. не требуется и 2. он может появиться где угодно .
Это был неправильный ход. Реальный код не имеет открывающей скобки, появляющейся в середине нигде, и реальный код также имеет тенденцию быть более хрупким, когда он использует первую форму, а не вторую. (Примерно раз в другой месяц на моей последней работе меня вызывали на стол коллеги, когда их изменения в моем коде не работали, и проблема заключалась в том, что они добавили строку в «if» без добавления фигурных скобки. В конце концов я просто принял привычку, что фигурные скобки всегда требуются, даже когда вы пишете только одну строку.)
К счастью, во многих случаях eval () будет воспроизводить полную ваттность JavaScript , Код JSFiddle должен читать:
function out(code) {
function format(x) {
return typeof x === "string" ?
JSON.stringify(x) : x;
}
document.writeln('>>> ' + code);
document.writeln(format(eval(code)));
}
document.writeln("<pre>");
out('[] + []');
out('[] + {}');
out('{} + []');
out('{} + {}');
out('Array(16).join("wat" + 1)');
out('Array(16).join("wat - 1")');
out('Array(16).join("wat" - 1) + " Batman!"');
document.writeln("</pre>");
[Также это первый раз, когда я написал document.writeln за многие много лет, и я чувствую себя немного грязным, пишу что-нибудь, связанное с document.writeln () и eval ().]
Мы можем ссылаться на спецификацию, и это замечательно и точно, но большинство случаев также можно объяснить более понятным образом со следующими утверждениями:
+
и -
работают только с примитивными значениями. Более конкретно +
(дополнение) работает с любыми строками или числами, а +
(унарный) и -
(вычитание и унарный) работает только с числами. valueOf
или toString
, которые доступны для любого объекта. Вот почему такие функции или операторы не вызывают ошибок при вызове на объекты. Таким образом, мы можем сказать, что:
[] + []
как String([]) + String([])
, который аналогичен '' + ''
. Я упомянул выше, что +
(дополнение) также справедливо для чисел, но в JavaScript нет допустимого числа в массиве, поэтому вместо этого используется добавление строк. [] + {}
такое же, как String([]) + String({})
, который аналогичен '' + '[object Object]'
{} + []
. Это заслуживает большего объяснения (см. Ответ Вентеро). В этом случае фигурные скобки обрабатываются не как объект, а как пустой блок, поэтому он оказывается таким же, как +[]
. Unary +
работает только с числами, поэтому реализация пытается получить номер из []
. Сначала он пытается valueOf
, который в случае массивов возвращает один и тот же объект, поэтому он пытается использовать последнее средство: преобразование результата toString
в число. Мы можем записать его как +Number(String([]))
, который аналогичен +Number('')
, который аналогичен +0
. Array(16).join("wat" - 1)
вычитание -
работает только с числами, поэтому это то же самое, что: Array(16).join(Number("wat") - 1)
, поскольку "wat"
не может быть преобразовано в действительное число. Мы получаем NaN
, и любая арифметическая операция на NaN
получается с NaN
, поэтому мы имеем: Array(16).join(NaN)
.