array
, string
, и number
являются основными составными элементами любой программы, но в JavaScript, при работе с этими типами данных, есть несколько особенностей, которые могут смутить или запутать вас.
Давайте посмотрим на несколько встроенных типов JS, и разберемся как мы можем полностью понять и корректно использовать их поведение.
Если сравнивать с другими строго типизированными языками, в JavaScript массивы - всего лишь контейнеры для любых типов значений, начиная от string
до number
, object
и даже других array
(с помощью которых можно создавать многомерные массивы).
var a = [ 1, "2", [3] ];
a.length; // 3
a[0] === 1; // true
a[2][0] === 3; // true
Вам не нужно предварительно устанавливать размер array
(подробнее в "Массивы" Глава 3), вы можете просто объявить их и добавлять значения когда вам нужно:
var a = [ ];
a.length; // 0
a[0] = 1;
a[1] = "2";
a[2] = [ 3 ];
a.length; // 3
Предупреждение: Используя delete
для значения array
будет удалена ячейка array
с этим значением, но даже если вы удалите последний элемент таким способом, это НЕ обновит свойство length
, так что будьте осторожны! Работа оператора delete
более детально будет рассмотрена в Главе 5.
Будьте осторожны при создании "разрозненных" массивов (оставляя или создавая пустые/пропущенные ячейки):
var a = [ ];
a[0] = 1;
// ячейка `a[1]` отсутствует
a[2] = [ 3 ];
a[1]; // undefined
a.length; // 3
Такой код может привести к странному поведению "пустых ячеек" оставленных между элементами массива. Пустой слот со значением undefined
внутри, ведет себя не так же как явно объявленный элемент массива (a[1] = undefined
). Подробнее в главе 3 "Массивы".
Массивы array
s проиндексированы числами (как и ожидается), но хитрость в том, что они также являются объектами, которые могут иметь string
ключи/свойства, добавленные к ним (но такие свойства не будут посчитаны в длине массива length
):
var a = [ ];
a[0] = 1;
a["foobar"] = 2;
a.length; // 1
a["foobar"]; // 2
a.foobar; // 2
Как бы там ни было, нужно быть осторожнее при использовании индексов массива в виде string
, т.к. это значение может быть преобразовано в тип number
, потому что использование индекса number
для массива предпочтительнее чем string
!
var a = [ ];
a["13"] = 42;
a.length; // 14
В общем, это не самая лучшая идея использовать пару string
ключ/свойство как элемент массива array
. Используйте object
для хранения пар ключ/свойство, а массивы array
s приберегите для хранения значений в ячейках с числовыми индексами.
Бывают случаи когда нужно преобразовать массивоподобное значение (пронумерованную коллекцию значений) в настоящий массив array
, обычно таким образом вы сможете применить методы массива (такие как indexOf(..)
, concat(..)
, forEach(..)
, etc.) к коллекции значений.
Например, различные DOM запросы возвращают список DOM элементов который не является настоящим массивом array
, но, при этом он достаточно похож на массив для преобразования. Другой общеизвестный пример - когда функция предоставляет свои аргументы arguments
в виде массивоподобного объекта (в ES6, считается устаревшим), чтобы получить доступ к списку аргументов.
Один из самых распространенных способов осуществить такое преобразование одолжить метод slice(..)
для значения:
function foo() {
var arr = Array.prototype.slice.call( arguments );
arr.push( "bam" );
console.log( arr );
}
foo( "bar", "baz" ); // ["bar","baz","bam"]
Если slice()
вызван без каких-либо параметров, как в примере выше, стандартные значения его параметров позволят продублировать массив array
(а в нашем случае, массивоподобное значение).
В ES6, есть встроенный метод Array.from(..)
который при вызове выполнит то же самое:
...
var arr = Array.from( arguments );
...
Примечание: Array.from(..)
имеет несколько мощных возможностей, детально о них рассказано в книге ES6 и не только данной серии.
Есть общее мнение, что строки string
являются всего лишь массивами array
из символов. Пока мы решаем можно или нельзя использовать array
, важно осознавать что JavaScript string
на самом деле не то же самое что массивы array
символов. Это сходство по большей части поверхностное.
Например, давайте сравним два значения:
var a = "foo";
var b = ["f","o","o"];
Строки имеют поверхностные сходства по отношению к массивам -- и массивоподобным, как выше -- например, оба из них имеют свойство length
, метод indexOf(..)
(array
только в ES5), и метод concat(..)
:
a.length; // 3
b.length; // 3
a.indexOf( "o" ); // 1
b.indexOf( "o" ); // 1
var c = a.concat( "bar" ); // "foobar"
var d = b.concat( ["b","a","r"] ); // ["f","o","o","b","a","r"]
a === c; // false
b === d; // false
a; // "foo"
b; // ["f","o","o"]
Итак, строки по большей части это "массивы символов", верно? НЕ совсем:
a[1] = "O";
b[1] = "O";
a; // "foo"
b; // ["f","O","o"]
В JavaScript строки string
неизменяемы, тогда как массивы array
достаточно изменяемы. Более того форма доступа к символу строки вида a[1]
не совсем правильный JavaScript. Старые версии IE не разрешают такой синтаксис (в новых версиях IE это работает). Вместо него нужно использовать корректный способ - a.charAt(1)
.
Еще одним последствием неизменяемости строк string
является то что ни один метод строки string
меняющий ее содержимое не может делать это по месту, скорее метод создаст и вернет новые строки. И напротив, большинство методов изменяющих содержимое массива array
действительно делают изменения по месту.
c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"
b.push( "!" );
b; // ["f","O","o","!"]
Также многие из методов массива array
, которые могут быть полезны при работе со строками string
вообще для них недоступны, но мы можем "одолжить" не изменяющие методы массива array
для нашей строки string
:
a.join; // undefined
a.map; // undefined
var c = Array.prototype.join.call( a, "-" );
var d = Array.prototype.map.call( a, function(v){
return v.toUpperCase() + ".";
} ).join( "" );
c; // "f-o-o"
d; // "F.O.O."
Давайте возьмем другой пример: реверсируем строку string
(кстати, это довольно тривиальный общий вопрос на JavaScript собеседованиях!). У массивов array
есть метод reverse()
осуществляющий изменение по месту, но для строки string
такого метода нет:
a.reverse; // undefined
b.reverse(); // ["!","o","O","f"]
b; // ["!","o","O","f"]
К несчастью, это "одалживание" не сработает с методами изменяющими массив array
, потому что строки string
неизменяемы и поэтому не могут быть изменены по месту:
Array.prototype.reverse.call( a );
// все еще возвращаем объект-обертку String (подробнее в Главе 3)
// для "foo" :(
Другое временное решение (хак) отконвертировать строку string
в массив array
, выполнить желаемое действие, и затем отконвертировать обратно в строку string
.
var c = a
// разбиваем `a` на массив символов
.split( "" )
// реверсируем массив символов
.reverse()
// объединяем массив символов обратно в строку
.join( "" );
c; // "oof"
Если кажется, что это выглядит безобразно, так и есть. Тем не менее, это работает для простых строк string
, так что, если вам нужно "склепать" что-нибудь по-быстрому, часто такой подход позволит выполнить работу.
Предупреждение: Будьте осторожны! Этот подход не работает для строк string
со сложными (unicode) символами в них (astral symbols, multibyte characters, etc.). Вам потребуются более сложные библиотеки которые распознают unicode символы для правильного выполнения подобных операций. Подробнее можно посмотреть в работе Mathias Bynens': Esrever (https://github.com/mathiasbynens/esrever).
Хотя с другой стороны: если вы чаще работаете с вашими "строками", интерпретируя их как массивы символов, возможно лучше просто записывать их в массив array
вместо строк string
. Возможно вы избавите себя от хлопот при переводе строки string
в массив array
каждый раз. Вы всегда можете вызвать join("")
для массива array
символов когда вам понадобится представление в виде строки string
.
В JavaScript есть один числовой тип: number
. Этот тип включает в себя как "целые" ("integer") значения так и десятичные дробные числа. Я заключил "целые" ("integer") в кавычки, потому что в JS это понятие подвергается критике, поскольку здесь нет реально целых значений, как в других языках программирования. Возможно в будущем это изменится, но сейчас, у нас просто есть тип number
для всего.
Итак, в JS, "целое" ("integer") это просто числовое значение, которое не имеет десятичной составляющей после запятой . Так например, 42.0
более может считаться "целым" ("integer"), чем 42
.
Как и в большинстве современных языков, включая практически все скриптовые языки, реализация чисел number
в JavaScript основана на стандарте "IEEE 754", который часто называют "числа с плавающей точкой" ("floating-point"). JavaScript особенно использует формат "двойной степени точности" (как "64-битные в бинарном формате") этого стандарта.
В интернете есть множество статей о подробных деталях того, как бинарные числа с плавающей точкой записываются в память, и последствия выбора таких чисел. Т.к. понимание того как работает запись в память не строго необходимо для того чтобы корректно использовать числа number
в JS, мы оставим это упражнение для заинтересованного читателя, если вы захотите более детально разобраться со стандартом IEEE 754.
Числовые литералы в JavaScript в большинстве представлены как литералы десятичных дробей. Например:
var a = 42;
var b = 42.3;
Если целая часть дробного числа - 0
, можно ее опустить:
var a = 0.42;
var b = .42;
Аналогично, если дробная часть после точки .
, - 0
, можно ее опустить:
var a = 42.0;
var b = 42.;
Предупреждение: 42.
выглядит достаточно необычно, и возможно это не лучшая идея если вы хотите избежать недопонимания со стороны других людей при работе с вашим кодом. Но, в любом случае, это корректная запись.
По умолчанию, большинство чисел number
выводятся как десятичные дроби, с удаленными нулями 0
в конце дробной части. Так:
var a = 42.300;
var b = 42.0;
a; // 42.3
b; // 42
Очень большие или очень маленькие числа number
по умолчанию выводятся в экспоненциальной форме, также как и результат метода toExponential()
, например:
var a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10"
var b = a * a;
b; // 2.5e+21
var c = 1 / a;
c; // 2e-11
Т.к. числовые значения number
могут быть помещены в объект - обертку Number
(подробнее Глава 3), числовые значения number
могут получать методы встроенные в Number.prototype
(подробнее Глава 3). Например, метод toFixed(..)
позволяет вам определить с точностью до скольких знаков после запятой вывести дробную часть:
var a = 42.59;
a.toFixed( 0 ); // "43"
a.toFixed( 1 ); // "42.6"
a.toFixed( 2 ); // "42.59"
a.toFixed( 3 ); // "42.590"
a.toFixed( 4 ); // "42.5900"
Заметьте что результат - строковое string
представление числа number
, и таким образом 0
- будет добавлено справа если вам понадобится больше знаков после запятой, чем есть сейчас.
toPrecision(..)
похожий метод, но он определяет сколько цифровых знаков должно использоваться в выводимом значении:
var a = 42.59;
a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"
a.toPrecision( 4 ); // "42.59"
a.toPrecision( 5 ); // "42.590"
a.toPrecision( 6 ); // "42.5900"
Вам не обязательно использовать переменные для хранения чисел, чтобы применить эти методы; вы можете применять методы прямо к числовым литералам number
. Но, будьте осторожны с оператором .
. Т.к. .
это еще и числовой оператор, и, если есть такая возможность, он в первую очередь будет интерпретирован как часть числового литерала number
, вместо того чтобы получать доступ к свойству.
// неправильный ситнтакс:
42.toFixed( 3 ); // SyntaxError
// это корректное обращение к методам:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
42.toFixed(3)
неверный синтаксис, потому что .
станет частью числового литерала 42.
(такая запись корректна -- смотрите выше!), и тогда оператор .
, который должен получить доступ к методу .toFixed
, отсутствует.
42..toFixed(3)
работает т.к. первый оператор .
часть числового литерала number
вторая .
оператор доступа к свойству. Но, возможно это выглядит странно, и на самом деле очень редко можно увидеть что-то подобное в реальном JavaScript коде. Фактически, это нестандартно -- применять методы прямо к примитивным значениям. Нестандартно не значит плохо или неправильно.
Примечание: Есть библиотеки расширяющие встроенные методы Number.prototype
(подробнее Глава 3) для поддержки операций над/с числами number
, и в этих случаях, совершенно правильно использовать 10..makeItRain()
чтобы отключить 10-секундную анимацию денежного дождя, или еще что-нибудь такое же глупое.
Также технически корректной будет такая запись (заметьте пробел):
42 .toFixed(3); // "42.000"
Тем не менее, с числовыми литералами number
особенно, это чрезвычайно запутанный стиль кода и он не преследует иных целей кроме как запутать разработчиков при работе с кодом (в том числе и вас в будущем). Избегайте этого.
Числа number
также могут быть представлены в экспоненциальной форме, которую обычно используют для представления больших чисел number
таких, как:
var onethousand = 1E3; // means 1 * 10^3
var onemilliononehundredthousand = 1.1E6; // means 1.1 * 10^6
Числовые литералы number
могут быть также выражены в других формах, таких как, двоичная, восьмеричная, и шестнадцатеричная.
Эти форматы работают в текущей версии JavaScript:
0xf3; // шестнадцатиричная для: 243
0Xf3; // то же самое
0363; // восьмеричная для: 243
Примечание: Начиная с ES6 с включенным strict
режимом, восьмеричная форма 0363
больше не разрешена (смотрите ниже новую форму). Форма 0363
все еще разрешена в non-strict
режиме, но в любом случае нужно прекратить ее использовать, чтобы использовать современный подход (и потому что пора бы использовать strict
режим уже сейчас!).
Для ES6, доступны новые формы записи:
0o363; // восьмеричная для: 243
0O363; // то же самое
0b11110011; // двоичная для: 243
0B11110011; // то же самое
И, пожалуйста, окажите вашим коллегам - разработчикам услугу: никогда не используйте форму вида 0O363
. 0
перед заглавной O
может лишь вызвать затруднение при чтении кода. Всегда используйте нижний регистр в подобных формах: 0x
, 0b
, и 0o
.
Самый известный побочный эффект от использования бинарной формы чисел с плавающей точкой (которая, как мы помним, справедлива для всех языков использующих стандарт IEEE 754 -- не только JavaScript как многие привыкли предполагать) это:
0.1 + 0.2 === 0.3; // false
Математически, что результатом выражения должно быть true
. Почему же в результате получается false
?
Если по-простому, представления чисел 0.1
и 0.2
в бинарном виде с плавающей точкой не совсем точные, поэтому когда мы их складываем, результат не совсем 0.3
. Это действительно близко: 0.30000000000000004
, но если сравнение не прошло, "близко" уже не имеет значения.
Примечание: Должен ли JavaScript перейти на другую реализацию числового типа number
которая имеет точные представления для всех значений? Некоторые так думают. За все годы появлялось много альтернатив. Никакие из них до сих пор не были утверждены, и возможно никогда не будут. Кажется что это также легко, как просто поднять руку и сказать "Да исправьте вы уже этот баг!", но это вовсе не так. Если бы это было легко, это определенно было бы исправлено намного раньше.
Сейчас, вопрос в том, что если есть числа number
для которых нельзя быть уверенным в их точности, может нам совсем не стоит использовать числа number
? Конечно нет.
Есть несколько случаев применения чисел, где нужно быть осторожными, особенно имея дело с дробными числами. Также есть достаточно (возможно большинство?) случаев когда мы имеем дело только с целыми числами ("integers"), и более того, работаем только с числами максимум до миллиона или триллиона. Такие случаи применения чисел всегда были, и будут, превосходно безопасными для проведения числовых операций в JS.
А что если нам было нужно сравнить два числа number
таких, как 0.1 + 0.2
и 0.3
, зная что обычный тест на равенство не сработает?
Самая общепринятая практика использование миниатюрной "ошибки округления" как допуска для сравнения. Это малюсенькое значение часто называют "машинной эпсилон," которое составляет 2^-52
(2.220446049250313e-16
) для числового типа number
в JavaScript.
В ES6, Number.EPSILON
определено заранее этим пороговым значением, так что если вы хотите его использовать, нужно применить полифилл для определения порогового значения для стандартов до-ES6:
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
Мы можем использовать это значение Number.EPSILON
для проверки двух чисел number
на "равенство" (с учетом допуска ошибки округления):
function numbersCloseEnoughToEqual(n1,n2) {
return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual( a, b ); // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false
Максимальное значение числа с плавающей точкой приблизительно 1.798e+308
(реально огромное число!), определено как Number.MAX_VALUE
. Минимальное значение, Number.MIN_VALUE
приблизительно 5e-324
, оно положительное, но очень близко к нулю!
Из-за представления чисел number
в JS, существует диапазон "безопасных" значений для всех чисел number
"integers", и он существенно меньше значения Number.MAX_VALUE
.
Максимальное целое число, которое может быть "безопасно" представлено (это означает гарантию того, что запрашиваемое значение будет представлено совершенно определенно) это 2^53 - 1
, что составляет 9007199254740991
. Если вы добавите запятые, то увидите что это немного больше 9 квадриллионов. Так что это чертовски много для верхнего диапазона чисел number
.
Это значение автоматически предопределено в ES6, как Number.MAX_SAFE_INTEGER
. Ожидаемо, минимальное значение, -9007199254740991
, соответственно предопределено в ES6 как Number.MIN_SAFE_INTEGER
.
Чаще всего JS программы могут столкнуться с такими большими числами, когда имеют дело с 64-битными ID баз данных, и т.п. 64-битные не могут быть точно представлены типом number
, так что они должны быть записаны (и переданы в/из) JavaScript с помощью строкового string
представления.
Математические операции с ID number
значениями (кроме сравнения, которое отлично пройдет со строками string
) обычно не выполняются, к счастью. Но если вам необходимо выполнить математическую операцию с очень большими числами, сейчас вы можете использовать утилиту big number. Поддержка больших чисел может быть реализована в будущих стандартах JavaScript.
Чтобы проверить, является ли число целым, вы можете использовать специальный ES6-метод Number.isInteger(..)
:
Number.isInteger( 42 ); // true
Number.isInteger( 42.000 ); // true
Number.isInteger( 42.3 ); // false
Полифилл для Number.isInteger(..)
для стандартов до-ES6:
if (!Number.isInteger) {
Number.isInteger = function(num) {
return typeof num == "number" && num % 1 == 0;
};
}
Для проверки на нахождение числа в безопасном диапазоне safe integer, используется ES6-метод Number.isSafeInteger(..)
:
Number.isSafeInteger( Number.MAX_SAFE_INTEGER ); // true
Number.isSafeInteger( Math.pow( 2, 53 ) ); // false
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 ); // true
Полифилл для Number.isSafeInteger(..)
для стандартов до-ES6:
if (!Number.isSafeInteger) {
Number.isSafeInteger = function(num) {
return Number.isInteger( num ) &&
Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
};
}
Пока целые числа могут быть приблизительно до 9 квадриллионов (53 бита), есть несколько числовых операторов (например побитовые операторы), которые определены для 32-битных чисел number
, так "безопасный диапазон" для чисел number
используемый в таких случаях намного меньше.
Диапазоном является от Math.pow(-2,31)
(-2147483648
, около -2.1 миллиардов) до Math.pow(2,31)-1
(2147483647
, около +2.1 миллиардов).
Чтобы записать число number
из переменной a
в 32-битное целое число, используем a | 0
. Это сработает т.к. |
побитовый оператор и работает только с 32-битными целыми числами (это означает что он будет работать только с 32 битами, а остальные биты будут утеряны). Ну, а "ИЛИ" с нулем побитовый оператор, который не проводит операций с битами.
Примечание: Определенные специальные значения (о которых будет рассказано далее) такие как NaN
и Infinity
не являются "32-битными безопасными значениями" и в случае передачи этих значений побитовому оператору, будет применен абстрактный оператор ToInt32
(смотрите главу 4) результатом которого будет значение+0
для последующего применения побитового оператора.
Есть несколько специальных значений, которые распространяются на все типы, и с которыми внимательный JS разработчик должен быть осторожен, и использовать их по назначению.
Для типа undefined
, есть только одно значение: undefined
. Для типа null
, есть только одно значение: null
. Итак, для них обоих, есть свой тип и свое значение.
И undefined
и null
часто считаются взаимозаменяемыми, как либо "пустое" значение, либо его "отсутствие". Другие разработчики различают их в соответствии с их особенностями. Например:
null
пустое значениеundefined
отсутствующее значение
Или:
undefined
значение пока не присвоеноnull
значение есть и там ничего не содержится
Независимо от того, как вы "определяете" и используете эти два значения, null
это специальное ключевое слово, не является идентификатором, и таким образом нельзя его использовать для назначения переменной (зачем вообще это делать!?). Как бы там ни было, undefined
является (к несчастью) идентификатором. Увы и ах.
В нестрогом режиме non-strict
, действительно есть возможность (хоть это и чрезвычайно плохая идея!) присваивать значение глобальному идентификатору undefined
:
function foo() {
undefined = 2; // очень плохая идея!
}
foo();
function foo() {
"use strict";
undefined = 2; // TypeError!
}
foo();
Как в нестрогом non-strict
так и в строгом strict
режимах, тем не менее, вы можете создать локальную переменную undefined
. Но, еще раз, это ужасная идея!
function foo() {
"use strict";
var undefined = 2;
console.log( undefined ); // 2
}
foo();
Настоящие друзья никогда не позволят друзьям переназначить undefined
. Никогда.
Пока undefined
является встроенным идентификатором который содержит (если только кто-нибудь это не изменил -- см. выше!) встроенное значение undefined
, другой способ получить это значение - оператор void
.
Выражение void ___
"аннулирует" любое значение, так что результатом выражения всегда будет являться значение undefined
. Это выражение не изменяет действующее значение; оно просто дает нам уверенность в том, что мы не получим назад другого значения после применения оператора.
var a = 42;
console.log( void a, a ); // undefined 42
По соглашению (большей частью из C-языка программирования), для получения только самого значения undefined
вместо использования void
, вы можете использовать void 0
(хотя и понятно что даже void true
или любое другое void
выражение выполнит то же самое). На практике нет никакой разницы между void 0
, void 1
, и undefined
.
Но, оператор void
может быть полезен в некоторых других обстоятельствах, например, если нужно быть уверенным, что выражение не вернет никакого результата (даже если оно имеет побочный эффект).
Например:
function doSomething() {
// примечание: `APP.ready` поддерживается нашим приложением
if (!APP.ready) {
// попробуйте еще раз позже
return void setTimeout( doSomething, 100 );
}
var result;
// делаем что - нибудь другое
return result;
}
// есть возможность выполнить задачу прямо сейчас?
if (doSomething()) {
// выполняем следующие задания немедленно right away
}
Здесь, функция setTimeout(..)
возвращает числовое значение (уникальный идентификатор интервала таймера, если вы захотите его отменить), но нам нужно применить оператор void
чтобы значение, которое вернет функция не было ложноположительным с инструкцией if
.
Многие разработчики предпочитают выполнять действия по отдельности, что в результате работает так же, но не требует применения оператора void
:
if (!APP.ready) {
// попробуйте еще раз позже
setTimeout( doSomething, 100 );
return;
}
Итак, если есть место где существует значение (как результат выражения), и вы находите полезным получить вместо него undefined
, используйте оператор void
. Возможно это не должно часто встречаться в ваших программах, но в редких случаях, когда это понадобится, это может быть довольно полезным.
Тип number
включает в себя несколько специальных значений. Рассмотрим каждое более подробно.
Любая математическая операция, которую выполняют с операндами не являющимися числами number
(или значениями которые могут быть интерпретированы как числа number
в десятичной или шестнадцатеричной форме) приведет к ошибке при попытке получить значение числового типа number
, в этом случае вы получите значение NaN
.
NaN
буквально означает "not a number
" ("НЕ число"), хотя это название/описание довольно скудное и обманчивое, как мы скоро увидим. Было бы правильнее думать о NaN
как о "неправильном числе", "ошибочном числе", или даже "плохом числе", чем думать о нем как о "НЕ числе".
Например:
var a = 2 / "foo"; // NaN
typeof a === "number"; // true
Другими словами: "Типом НЕ-числа является число 'number'!" Ура запутывающим именам и семантике.
NaN
на вроде "сторожевого значения" (другими словами нормальное значение, которое несет специальный смысл), которое определяет сбой при проведении операции назначения числа number
. Эта ошибка, по сути означает следующее: "Я попробовал выполнить математическую операция и произошла ошибка, поэтому, вместо результата, здесь ошибочное число number
."
Итак, если у вас есть значение в какой-нибудь переменной, и вы хотите проверить, не является ли оно ошибочным числом NaN
, вы должно быть думаете что можно просто его сравнить прямо с NaN
, как с любым другим значением, например null
или undefined
. Неа.
var a = 2 / "foo";
a == NaN; // false
a === NaN; // false
NaN
очень особенное значение и оно никогда не будет равно другому значению NaN
(т.е., оно не равно самому себе). Фактически, это всего лишь значение, которое не рефлексивно (без возможности идентификации x === x
). Итак, NaN !== NaN
. Немного странно, да?
Так как мы можем его проверить, если нельзя сравнить с NaN
(т.к. сравнение не сработает)?
var a = 2 / "foo";
isNaN( a ); // true
Достаточно просто, верно? Мы использовали встроенную глобальную функцию, которая называется isNaN(..)
и она сообщила нам является значение NaN
или нет. Проблема решена!
Не так быстро.
У функции isNaN(..)
есть большой недостаток. Он появляется при попытках воспринимать значение NaN
("НЕ-Число") слишком буквально -- вот, вкратце, как это работает: "проверяем то, что нам передали -- либо это не является числом number
, либо -- это число number
." Но это не совсем правильно.
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; // "foo"
window.isNaN( a ); // true
window.isNaN( b ); // true -- упс!
Понятно, "foo"
буквально НЕ-Число, но и определенно не является значением NaN
! Этот баг был в JS с самого начала (более 19 лет упс).
В ES6, наконец была представлена функция: Number.isNaN(..)
. Простым полифиллом, чтобы вы могли проверить на значение NaN
прямо сейчас, даже в браузерах не поддерживающих-ES6, будет:
if (!Number.isNaN) {
Number.isNaN = function(n) {
return (
typeof n === "number" &&
window.isNaN( n )
);
};
}
var a = 2 / "foo";
var b = "foo";
Number.isNaN( a ); // true
Number.isNaN( b ); // false -- фуух!
Вообще, мы можем реализовать полифилл Number.isNaN(..)
даже проще, если воспользоваться специфической особенностью NaN
, которое не равно самому себе. NaN
единственное для которого это справедливо; любое другое значение всегда равно самому себе.
Итак:
if (!Number.isNaN) {
Number.isNaN = function(n) {
return n !== n;
};
}
Странно, правда? Но это работает!
NaN
могут появляться во многих действующих JS программах, намеренно или случайно. Это действительно хорошая идея проводить надежную проверку, например Number.isNaN(..)
если это поддерживается (или полифилл), чтобы распознать их должным образом.
Если вы все еще используете isNaN(..)
в своей программе, плохая новость: в вашей программе есть баг, даже если вы с ним еще не столкнулись!
Разработчики пришедшие из традиционных компилируемых языков вроде C, возможно, привыкли видеть ошибку компилирования или выполнения, например "деление на ноль," для подобных операций:
var a = 1 / 0;
Как бы там ни было, в JS, эта операция четко определена, и ее результатом будет являться -- бесконечность Infinity
(ну или Number.POSITIVE_INFINITY
). Как и ожидается:
var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity
Как вы видите, -Infinity
(или Number.NEGATIVE_INFINITY
) получается при делении на ноль где один из операторов (но не оба!) является отрицательным.
JS использует вещественное представление чисел (IEEE 754 числа с плавающей точкой, о котором было рассказано ранее), вразрез с чистой математикой, похоже что есть возможность переполнения при выполнении таких операций как сложение или вычитание, и в этом случае результатом будет Infinity
или -Infinity
.
Например:
var a = Number.MAX_VALUE; // 1.7976931348623157e+308
a + a; // Infinity
a + Math.pow( 2, 970 ); // Infinity
a + Math.pow( 2, 969 ); // 1.7976931348623157e+308
Согласно спецификации, если, в результате операции вроде сложения, получается число, превышающее максимальное число, которое может быть представлено, функция IEEE 754 "округления-до-ближайшего" определит, каким должен быть результат. Итак, если проще, Number.MAX_VALUE + Math.pow( 2, 969 )
ближе к Number.MAX_VALUE
чем к бесконечности Infinity
, так что его "округляем вниз," тогда как Number.MAX_VALUE + Math.pow( 2, 970 )
ближе к бесконечности Infinity
, поэтому его "округляем вверх".
Если слишком много об этом думать, то у вас так скоро голова заболит. Не нужно. Серьезно, перестаньте!
Если однажды вы перешагнете одну из бесконечностей, в любом случае, назад пути уже не будет. Другими словами, в почти литературной форме, вы можете прийти из действительности в бесконечность, но не из бесконечности в действительность.
Это фактически философский вопрос: "Что если бесконечность разделить на бесконечность". Наш наивный мозг скажет что-нибудь вроде "1", или, может, "бесконечность." Но ни то, ни другое, не будет верным. И в математике, и в JavaScript, операция Infinity / Infinity
не определена. В JS, результатом будет NaN
.
Но, что если любое вещественное положительное число number
, разделить на бесконечность Infinity
? Это легко! 0
. А что если вещественное отрицательное число number
, разделить на бесконечность Infinity
? Об этом в следующей серии, продолжайте читать!
Это может смутить математически-думающего читателя, но в JavaScript есть два значения 0
: нормальный ноль (также известный как положительный ноль +0
) и отрицательный ноль -0
. Прежде чем объяснять почему существует -0
, мы должны посмотреть как это работает в JS, потому что это может сбить с толку.
Кроме того что значение -0
может быть буквально присвоено, отрицательный ноль может быть результатом математических операций. Например:
var a = 0 / -3; // -0
var b = 0 * -3; // -0
Отрицательный ноль не может быть получен в результате сложения или вычитания.
Отрицательный ноль при выводе в консоль разработчика обычно покажет -0
, хотя до недавнего времени это не было общепринятым, вы можете узнать что некоторые старые браузеры до сих пор выводят 0
.
Как бы там ни было, при попытке преобразования отрицательного нуля в строку, всегда будет выведено "0"
, согласно спецификации.
var a = 0 / -3;
// (некоторые браузеры) выводят в консоль правильное значение
a; // -0
// но спецификация лжет вам на каждом шагу!
a.toString(); // "0"
a + ""; // "0"
String( a ); // "0"
// странно, даже JSON введен в заблуждение
JSON.stringify( a ); // "0"
Интересно, что обратная операция (преобразование из строки string
в число number
) не врет:
+"-0"; // -0
Number( "-0" ); // -0
JSON.parse( "-0" ); // -0
Предупреждение: Поведение JSON.stringify( -0 )
по отношению к "0"
странное лишь частично, если вы заметите то обратная операция: JSON.parse( "-0" )
выведет -0
как вы и ожидаете.
В дополнение к тому что преобразование в строку скрывает реальное значение отрицательного нуля, операторы сравнения также (намеренно) настроены лгать.
var a = 0;
var b = 0 / -3;
a == b; // true
-0 == 0; // true
a === b; // true
-0 === 0; // true
0 > -0; // false
a > b; // false
Очевидно, если вы хотите различать -0
от 0
в вашем коде, вы не можете просто полагаться на то, что выведет консоль разработчика, так что придется поступить немного хитрее:
function isNegZero(n) {
n = Number( n );
return (n === 0) && (1 / n === -Infinity);
}
isNegZero( -0 ); // true
isNegZero( 0 / -3 ); // true
isNegZero( 0 ); // false
Итак, зачем нам нужен отрицательный ноль, вместо обычного значения?
Есть определенные случаи где разработчики используют величину значения для определения одних данных (например скорость перемещения анимации в кадре) а знак этого числа number
для представления других данных (например направление перемещения).
В этих случаях, как в примере выше, если переменная достигнет нуля и потеряет знак, тогда, вы потеряете информацию о том, откуда она пришла, до того как достигла нулевого значения. Сохранение знака нуля предупреждает потерю этой информации.
As we saw above, the NaN
value and the -0
value have special behavior when it comes to equality comparison. NaN
is never equal to itself, so you have to use ES6's Number.isNaN(..)
(or a polyfill). Simlarly, -0
lies and pretends that it's equal (even ===
strict equal -- see Chapter 4) to regular positive 0
, so you have to use the somewhat hackish isNegZero(..)
utility we suggested above.
As of ES6, there's a new utility that can be used to test two values for absolute equality, without any of these exceptions. It's called Object.is(..)
:
var a = 2 / "foo";
var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false
There's a pretty simple polyfill for Object.is(..)
for pre-ES6 environments:
if (!Object.is) {
Object.is = function(v1, v2) {
// test for `-0`
if (v1 === 0 && v2 === 0) {
return 1 / v1 === 1 / v2;
}
// test for `NaN`
if (v1 !== v1) {
return v2 !== v2;
}
// everything else
return v1 === v2;
};
}
Object.is(..)
probably shouldn't be used in cases where ==
or ===
are known to be safe (see Chapter 4 "Coercion"), as the operators are likely much more efficient and certainly are more idiomatic/common. Object.is(..)
is mostly for these special cases of equality.
In many other languages, values can either be assigned/passed by value-copy or by reference-copy depending on the syntax you use.
For example, in C++ if you want to pass a number
variable into a function and have that variable's value updated, you can declare the function parameter like int& myNum
, and when you pass in a variable like x
, myNum
will be a reference to x
; references are like a special form of pointers, where you obtain a pointer to another variable (like an alias). If you don't declare a reference parameter, the value passed in will always be copied, even if it's a complex object.
In JavaScript, there are no pointers, and references work a bit differently. You cannot have a reference from one JS variable to another variable. That's just not possible.
A reference in JS points at a (shared) value, so if you have 10 different references, they are all always distinct references to a single shared value; none of them are references/pointers to each other.
Moreover, in JavaScript, there are no syntactic hints that control value vs. reference assignment/passing. Instead, the type of the value solely controls whether that value will be assigned by value-copy or by reference-copy.
Let's illustrate:
var a = 2;
var b = a; // `b` is always a copy of the value in `a`
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // `d` is a reference to the shared `[1,2,3]` value
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]
Simple values (aka scalar primitives) are always assigned/passed by value-copy: null
, undefined
, string
, number
, boolean
, and ES6's symbol
.
Compound values -- object
s (including array
s, and all boxed object wrappers -- see Chapter 3) and function
s -- always create a copy of the reference on assignment or passing.
In the above snippet, because 2
is a scalar primitive, a
holds one initial copy of that value, and b
is assigned another copy of the value. When changing b
, you are in no way changing the value in a
.
But both c
and d
are separate references to the same shared value [1,2,3]
, which is a compound value. It's important to note that neither c
nor d
more "owns" the [1,2,3]
value -- both are just equal peer references to the value. So, when using either reference to modify (.push(4)
) the actual shared array
value itself, it's affecting just the one shared value, and both references will reference the newly modified value [1,2,3,4]
.
Since references point to the values themselves and not to the variables, you cannot use one reference to change where another reference is pointed:
var a = [1,2,3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]
// later
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]
When we make the assignment b = [4,5,6]
, we are doing absolutely nothing to affect where a
is still referencing ([1,2,3]
). To do that, b
would have to be a pointer to a
rather than a reference to the array
-- but no such capability exists in JS!
The most common way such confusion happens is with function parameters:
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// later
x = [4,5,6];
x.push( 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // [1,2,3,4] not [4,5,6,7]
When we pass in the argument a
, it assigns a copy of the a
reference to x
. x
and a
are separate references pointing at the same [1,2,3]
value. Now, inside the function, we can use that reference to mutate the value itself (push(4)
). But when we make the assignment x = [4,5,6]
, this is in no way affecting where the initial reference a
is pointing -- still points at the (now modified) [1,2,3,4]
value.
There is no way to use the x
reference to change where a
is pointing. We could only modify the contents of the shared value that both a
and x
are pointing to.
To accomplish changing a
to have the [4,5,6,7]
value contents, you can't create a new array
and assign -- you must modify the existing array
value:
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// later
x.length = 0; // empty existing array in-place
x.push( 4, 5, 6, 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // [4,5,6,7] not [1,2,3,4]
As you can see, x.length = 0
and x.push(4,5,6,7)
were not creating a new array
, but modifying the existing shared array
. So of course, a
references the new [4,5,6,7]
contents.
Remember: you cannot directly control/override value-copy vs. reference -- those semantics are controlled entirely by the type of the underlying value.
To effectively pass a compound value (like an array
) by value-copy, you need to manually make a copy of it, so that the reference passed doesn't still point to the original. For example:
foo( a.slice() );
slice(..)
with no parameters by default makes an entirely new (shallow) copy of the array
. So, we pass in a reference only to the copied array
, and thus foo(..)
cannot affect the contents of a
.
To do the reverse -- pass a scalar primitive value in a way where its value updates can be seen, kinda like a reference -- you have to wrap the value in another compound value (object
, array
, etc) that can be passed by reference-copy:
function foo(wrapper) {
wrapper.a = 42;
}
var obj = {
a: 2
};
foo( obj );
obj.a; // 42
Here, obj
acts as a wrapper for the scalar primitive property a
. When passed to foo(..)
, a copy of the obj
reference is passed in and set to the wrapper
parameter. We now can use the wrapper
reference to access the shared object, and update its property. After the function finishes, obj.a
will see the updated value 42
.
It may occur to you that if you wanted to pass in a reference to a scalar primitive value like 2
, you could just box the value in its Number
object wrapper (see Chapter 3).
It is true a copy of the reference to this Number
object will be passed to the function, but unfortunately, having a reference to the shared object is not going to give you the ability to modify the shared primitive value, like you may expect:
function foo(x) {
x = x + 1;
x; // 3
}
var a = 2;
var b = new Number( a ); // or equivalently `Object(a)`
foo( b );
console.log( b ); // 2, not 3
The problem is that the underlying scalar primitive value is not mutable (same goes for String
and Boolean
). If a Number
object holds the scalar primitive value 2
, that exact Number
object can never be changed to hold another value; you can only create a whole new Number
object with a different value.
When x
is used in the expression x + 1
, the underlying scalar primitive value 2
is unboxed (extracted) from the Number
object automatically, so the line x = x + 1
very subtly changes x
from being a shared reference to the Number
object, to just holding the scalar primitive value 3
as a result of the addition operation 2 + 1
. Therefore, b
on the outside still references the original unmodified/immutable Number
object holding the value 2
.
You can add properties on top of the Number
object (just not change its inner primitive value), so you could exchange information indirectly via those additional properties.
This is not all that common, however; it probably would not be considered a good practice by most developers.
Instead of using the wrapper object Number
in this way, it's probably much better to use the manual object wrapper (obj
) approach in the earlier snippet. That's not to say that there's no clever uses for the boxed object wrappers like Number
-- just that you should probably prefer the scalar primitive value form in most cases.
References are quite powerful, but sometimes they get in your way, and sometimes you need them where they don't exist. The only control you have over reference vs. value-copy behavior is the type of the value itself, so you must indirectly influence the assignment/passing behavior by which value types you choose to use.
In JavaScript, array
s are simply numerically indexed collections of any value-type. string
s are somewhat "array
-like", but they have distinct behaviors and care must be taken if you want to treat them as array
s. Numbers in JavaScript include both "integers" and floating-point values.
Several special values are defined within the primitive types.
The null
type has just one value: null
, and likewise the undefined
type has just the undefined
value. undefined
is basically the default value in any variable or property if no other value is present. The void
operator lets you create the undefined
value from any other value.
number
s include several special values, like NaN
(supposedly "Not a Number", but really more appropriately "invalid number"); +Infinity
and -Infinity
; and -0
.
Simple scalar primitives (string
s, number
s, etc.) are assigned/passed by value-copy, but compound values (object
s, etc.) are assigned/passed by reference-copy. References are not like references/pointers in other languages -- they're never pointed at other variables/references, only at the underlying values.