Недавно во время молниеносной беседы Гэри Бернхардта « Wat » была отмечена интересная особенность JavaScript: вы получаете неожиданные результаты при добавлении объектов и / или массивов. Этот пост объясняет эти результаты.
Общее правило добавления в JavaScript простое: вы можете добавлять только числа и строки, все остальные значения будут преобразованы в один из этих типов. Чтобы понять, как работает это преобразование, нам сначала нужно понять несколько других вещей. Всякий раз, когда упоминается параграф (например, §9.1), он ссылается на языковой стандарт ECMA-262 (ECMAScript 5.1).
Давайте начнем с быстрого освежения. В JavaScript есть два вида значений: примитивы и объекты [1] . Примитивные значения: undefined, null, логические значения, числа и строки. Все остальные значения являются объектами, включая массивы и функции.
Преобразование значений
Оператор плюс выполняет три вида преобразования: он преобразует значения в примитивы, числа и строки.
Преобразование значений в примитивы с помощью ToPrimitive ()
Внутренняя операция ToPrimitive () имеет следующую подпись:
ToPrimitive(input, PreferredType?)
Необязательный параметр PreferredType — это Number или String. Это только выражает предпочтение, результатом всегда может быть любое примитивное значение. Если PreferredType равен Number, выполняются следующие шаги для преобразования введенного значения (§9.1):
- Если вход является примитивным, вернуть его как есть.
- В противном случае вход является объектом. Вызовите obj.valueOf (). Если результат примитивный, верните его.
- В противном случае вызовите obj.toString (). Если результат является примитивным, верните его.
- В противном случае выведите ошибку TypeError.
Если PreferredType — String, шаги 2 и 3 меняются местами. Если PreferredType отсутствует, то он устанавливается на String для экземпляров Date и на Number для всех других значений.
Преобразование значений в числа с помощью ToNumber ()
В следующей таблице объясняется, как ToNumber () преобразует примитивы в число (§9.3).
аргументация | Результат |
не определено | NaN |
ноль | +0 |
логическое значение | true конвертируется в 1, false конвертируется в +0 |
числовое значение | нет необходимости в преобразовании |
строковое значение | разобрать номер в строке. Например, «324» преобразуется в 324 |
Объект obj преобразуется в число путем вызова ToPrimitive (obj, Number) и последующего применения ToNumber () к результату (примитиву).
Преобразование значений в строки с помощью ToString ()
В следующей таблице объясняется, как ToString () преобразует примитивы в строку (§9.8).
аргументация | Результат |
не определено | «Неопределенный» |
ноль | «ноль» |
логическое значение | либо «правда», либо «ложь» |
числовое значение | число в виде строки, например «1.765» |
строковое значение | нет необходимости в преобразовании |
Объект obj преобразуется в число, вызывая ToPrimitive (obj, String) и затем применяя ToString () к результату (примитиву).
Пробовать
Следующий объект позволяет наблюдать за процессом конвертации.
var obj = { valueOf: function () { console.log("valueOf"); return {}; // not a primitive }, toString: function () { console.log("toString"); return {}; // not a primitive } }
Число, вызываемое как функция (в отличие от конструктора), внутренне вызывает ToNumber ():
> Number(obj) valueOf toString TypeError: Cannot convert object to primitive value
прибавление
Учитывая следующее дополнение.
value1 + value2
Чтобы оценить это выражение, предпринимаются следующие шаги (§11.6.1):
- Преобразуйте оба операнда в примитивы (математическая запись, а не JavaScript):
prim1 := ToPrimitive(value1) prim2 := ToPrimitive(value2)
PreferredType опущен и, таким образом, Number для не-дат, String для дат.
- Если либо prim1, либо prim2 является строкой, преобразуйте оба в строки и верните объединение результатов
- В противном случае, преобразуйте оба числа prim1 и prim2 в числа и верните сумму результатов.
Ожидаемые результаты
Когда вы добавляете два массива, все работает как положено:
> [] + [] ''
Преобразование [] в примитив сначала пытается использовать функцию valueOf (), которая возвращает сам массив (this):
> var arr = []; > arr.valueOf() === arr true
Так как этот результат не является примитивом, toString () вызывается следующим и возвращает пустую строку (которая является примитивом). Следовательно, результатом [] + [] является объединение двух пустых строк.
Добавление массива и объекта также соответствует нашим ожиданиям:
> [] + {} '[object Object]'
Объяснение: преобразование пустого объекта в строку приводит к следующему результату.
> String({}) '[object Object]'
Предыдущий результат, таким образом, создается путем объединения «» и «[объект объекта]».
Еще примеры, где объекты преобразуются в примитивы:
> 5 + new Number(7) 12 > 6 + { valueOf: function () { return 2 } } 8 > "abc" + { toString: function () { return "def" } } 'abcdef'
Неожиданные результаты
Ситуация становится странной, если первый операнд + является пустым литералом объекта (результаты, как видно на консоли Firefox):
> {} + {} NaN
Что здесь происходит? Проблема в том, что JavaScript интерпретирует первый {} как пустой блок кода и игнорирует его. Поэтому NaN вычисляется путем вычисления + {} (плюс плюс второй {}). Плюс, который вы видите здесь, это не бинарный оператор сложения, а унарный префиксный оператор, который преобразует свой операнд в число таким же образом, как Number (). Например:
> +"3.65" 3.65
Следующие выражения все эквивалентны:
+{} Number({}) Number({}.toString()) // {}.valueOf() isn’t primitive Number("[object Object]") NaN
Почему первый {} интерпретируется как блок кода? Поскольку полный ввод анализируется как оператор, а фигурные скобки в начале оператора интерпретируются как начало блока кода. Следовательно, вы можете исправить ситуацию, заставив входные данные быть проанализированы как выражение:
> ({} + {}) '[object Object][object Object]'
Аргументы функций или методов также всегда анализируются как выражения:
> console.log({} + {}) [object Object][object Object]
После предыдущих объяснений вы больше не должны удивляться следующему результату:
> {} + [] 0
Опять же, это интерпретируется как блок кода, за которым следует + []. Следующие выражения эквивалентны:
+[] Number([]) Number([].toString()) // {}.valueOf() isn’t primitive Number("") 0
Интересно, что REPL Node.js анализирует свои входные данные иначе, чем Firefox или Chrome (который даже использует тот же движок V8 JavaScript, что и Node.js). Следующий вход анализируется как выражение, и результаты менее удивительны:
> {} + {} '[object Object][object Object]' > {} + [] '[object Object]'
Это имеет то преимущество, что больше похоже на результаты, которые вы получаете, используя входные данные в качестве аргументов console.log (). Но это также не похоже на использование ввода в качестве операторов в программах.
Что все это значит?
В большинстве случаев не так сложно понять, как + работает в JavaScript: вы можете добавлять только цифры или строки. Объекты преобразуются либо в строку (если другой операнд является строкой), либо в число (в противном случае). Если вы хотите объединить массивы, вам нужно использовать метод:
> [1, 2].concat([3, 4]) [ 1, 2, 3, 4 ]
В JavaScript нет встроенного способа «объединять» (объединять) объекты. Вам нужно использовать библиотеку, такую как
Underscore :
> var o1 = {eeny:1, meeny:2}; > var o2 = {miny:3, moe: 4}; > _.extend(o1, o2) { eeny: 1, meeny: 2, miny: 3, moe: 4 }
Примечание. В отличие от Array.prototype.concat (), extension () изменяет свой первый аргумент:
> o1 { eeny: 1, meeny: 2, miny: 3, moe: 4 } > o2 { miny: 3, moe: 4 }
Если вы хотите повеселиться с операторами, вы можете прочитать пост «
Перегрузка поддельных операторов в JavaScript ».
Рекомендации