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
для представления других данных (например направление перемещения).
В этих случаях, как в примере выше, если переменная достигнет нуля и потеряет знак, тогда, вы потеряете информацию о том, откуда она пришла, до того как достигла нулевого значения. Сохранение знака нуля предупреждает потерю этой информации.
Как мы заметили ранее, значения NaN
и -0
имеют особенное поведение в случае сравнения. NaN
никогда не равно самому себе, поэтому необходимо использовать функцию из ES6 Number.isNaN(..)
(или же полифил). Аналогично, -0
лжет и притворяется равным (даже ===
строго равным -- см. Главу 4) положительному 0
, поэтому необходимо использовать хитрый способ isNegZero(..)
, который мы предложили выше.
В ES6 появилась новая возможность, которую мы можем использовать для тестирования двух значений на абсолютное равенство без вышеперечисленных исключений. Она называется 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
Есть достаточно простой полифил для Object.is(..)
для браузеров, не поддерживающих ES6:
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(..)
, вероятно, не должен использоваться в случаях, где ==
и ===
достоверно безопасны (см. Главу 4 "Coercion"), поскольку данные операторы скорее более эффективны и определенно более идиоматичны/распространены. Object.is(..)
необходим для особенных случаев равенства.
Во многих языках значения могут присваиваться/передаваться по значению или по ссылке в зависимости от используемого синтаксиса.
Например, если в С++ вы хотите обновить значение числовой переменной, передаваемой функции, то можете объявить параметр функции вида int& myNum
. Тогда при передаче переменной (например x
) myNum
будет содержать ссылку на x
; ссылки работают как специальная разновиднось указателей (то есть фактически синонимы для других переменных). Если же не объявить ссылочный параметр, то передаваемое значение всегда будет копироваться, даже если это сложный объект.
В JavaScript указатели не существуют, а ссылки работают несколько иначе. Одна переменная JS не может хранить ссылку на другую переменную -- это попросту невозможно.
Ссылка в JS указывает на (общее) значение, так что, если вы создаете 10 разных ссылок, они всегда будут указывать на одно общее значение; они никогда не ссылаются/не указывают друг на друга.
Более того, в JavaScript не существует синтаксических подсказок, управляющие способом передачи (по ссылке или по значению). Вместо этого тип значения управляет только тем, как будет выполняться присваивание -- копированием значения или копированние ссылки.
Пример:
var a = 2;
var b = a; // `b` всегда содержит копию значения из `a`
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // `d` - ссылка на общее значение `[1,2,3]`
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]
Простые значения (то есть скалярные примитивы) всегда присваиваются/передаются копированием значения: это null
, undefined
, string
, number
, boolean
и symbol
из ES6.
Составные значения -- объекты
(включая массивы
и все объектные обертки -- см. главу 3) и функции
-- при присваивании или передаче всегда создают копию ссылки.
В приведенном примере, поскольку 2
является скалярным примитивом, a
содержит одну исходную копию значения, а b
присваивается другая копия значения.
Но c
и d
представляют собой разные ссылки на одно общее значение [1,2,3]
, которое является составным. Важно заметить, что значение [1, 2, 3]
не "принадлежит" ни c
, ни d
-- это просто равноправные ссылки на значение. Итак, при использовании любой ссылки для изменения общего массива
((.push(4)
)) изменения распространяются на единственное общее значение, а обе ссылки будут указывать на измененное значение [1, 2, 3, 4]
.
Так как ссылки указывают на сами значения, а не на переменные, одна ссылка не может использоваться для изменения того, на что ссылается другая ссылка:
var a = [1,2,3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]
// позднее
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]
Выполняя присваивание b = [4,5,6]
, мы абсолютно ничего не делаем для изменения того, на что сейчас ссылается a
([1,2,3]
). Чтобы это произошло, переменная b
должна быть указателем, а не ссылкой на массив, но в JS такой возможности нет.
Чаще всего такие недоразумения происходят с параметрами функций:
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// позднее
x = [4,5,6];
x.push( 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // [1,2,3,4], а не [4,5,6,7]
При передаче аргумента a
копия ссылки a
присваивается x
. x
и a
-- разные ссылки, указывающие на одно значение [1,2,3]
. Теперь внутри функции можно использовать эту ссылку для изменения самого значения (push(4)
). Но когда мы выполняем присваивание x = [4,5,6]
, оно никак не влияет на то, на что указывает исходная ссылка, она все еще указывает на (уже измененный) [1,2,3,4]
.
Ссылка x
не может использоваться для изменения того, на что указывает a
. Можно только изменить содержимое общего значения, на которое указывает как a
, так и x
.
Чтобы переменная a
изменилась и содержала [4,5,6,7]
, вам не удастся создать новый массив
и выполнить присваивание, необходимо изменить существующее значение массива
:
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// позднее
x.length = 0; // очистить существующий массив на месте
x.push( 4, 5, 6, 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // [4,5,6,7], а не [1,2,3,4]
Как видите, x.length = 0
и x.push(4,5,6,7)
не создает новый массив
, а изменяет существующий общий массив
. И конечно, a
ссылается на новое содержимое [4,5,6,7]
.
Помните: вы не можете напрямую контролировать/переопределять используемый механизм передачи (копирование значения или копирование ссылки) -- эта семантика определяется исключительно типом используемого значения.
Чтобы передать составное значение (например, массив
) посредством копирования значения, необходимо создать его копию вручную, чтобы переданная ссылка не продолжала указывать на оригинал. Пример:
foo( a.slice() );
slice(..)
без параметров по умолчанию создает совершенно новую (поверхностную) копию массива
. Таким образом, передается ссылка только на скопированный массив
, а значит foo(..)
не сможет повлиять на содержимое a
.
Чтобы решить обратную задачу -- передать скалярное примитивное значение так, чтобы его значение обновлялось как ссылка -- необходимо "завернуть" значение в другое составное значение (объект
, массив
, и т. д.), которое может быть передано копированием ссылки:
function foo(wrapper) {
wrapper.a = 42;
}
var obj = {
a: 2
};
foo( obj );
obj.a; // 42
Здесь obj
является оберткой для скалярного примитивного свойства a
. При передаче foo(..)
передается копия ссылки obj
, которая присваивается параметру wrapper
. После этого ссылка wrapper
может использоваться для обращения к общему объекту и обновлению его свойства. После завершения функции obj.a
"увидит" обновленное значение 42
.
И тут возникает другая мысль: если вы хотите передать ссылку на скалярное примитивное значение (например, 2
), нельзя ли упаковать значение в объектную обертку Number
(см. главу 3)?
Действительно, функции будет передана копия ссылки на объект Number
, но, к сожалению, наличие ссылки на общий объект не даст вам возможности изменить общее примитивное значение, как можно было бы ожидать:
function foo(x) {
x = x + 1;
x; // 3
}
var a = 2;
var b = new Number( a ); // эквивалентно `Object(a)`
foo( b );
console.log( b ); // 2, не 3
Проблема в том, что нижележащее скалярное примитивное значение является неизменяемым (то же относится к String
и Boolean
). Если объект Number
содержит скалярное примитивное значение 2
, этот конкретный объект Number
не удастся изменить так, чтобы он содержал другое значение, можно только создать совершенно новый объект Number
с другим значением.
При использовании x
в выражении x + 1
нижележащее скалярное примитивное значение 2
распаковывается (извлекается) из объекта Number
автоматически, так что строка x = x + 1
совершенно незаметно превращает x
из общей ссылки на объект Number
в простое хранилище для скалярного примитивного значения 3
в результате операции сложения 2 + 1
. Следовательно, b
снаружи продолжает ссылаться на исходный неизмененный/неизменяемый объект Number
, содержащий значение 2
.
К объекту Number
можно добавлять новые свойства (только не изменяя его внутреннее примитивное значение), так что вы сможете организовать непрямую передачу информации через эти дополнительные свойства.
Впрочем, такие ситуации встречаются довольно редко. Пожалуй, мало кто из разработчиков сочтет такой трюк хорошей практикой программирования.
Вместо того чтобы использовать объектную обертку Number
подобным образом, лучше использовать "ручное" решение с объектной оберткой (obj
) из предыдущего фрагмента. Это вовсе не означает, что для таких объектных оберток, как Number
, нельзя найти творческое применение. Это значит, что в большинстве случаев лучше использовать форму со скалярным примитивным значением.
Ссылки обладают мощными возможностями, но иногда они начинают мешать, а иногда они нужны там, где их нет. Управлять поведением ссылок (выбирать между копированием значений и копированием ссылок) можно только при помощи типа самого значения, так что вам придется косвенно влиять на поведение присваивания/передачи выбором типов используемых значений.
В JavaScript массивы array
являются численно индексированными коллекциями для любых типов значений. Строки string
несколько "массивоподобны", но обладают отличительными особенностями, и нужно быть аккуратным, если вы хотите рассматривать их в роли массивов array
. Значения чисел в JavaScript включают "целые" ("integers") и с плавающей точкой.
В примитивах определено несколько специальных значений.
Тип null
имеет одно значение: null
, аналогично тип undefined
имеет лишь значение undefined
. undefined
в целом является значением по умолчанию для любой переменной или свойства, если не указано иного значения. Оператор void
позволяет вернуть значение undefined
из любого другого значения.
Числа number
включают несколько специальных значений, например NaN
(условно "НЕ число", но на самом деле скорее "неправильное число"); +Infinity
и -Infinity
; и -0
.
Простые скалярные примитивы (строки string
, числа number
, и т.д.) присваиваются/передаются копированием значения, однако сложные значения (объекты object
и т.д.) присваиваются/передаются копированием ссылки. Ссылки не похожи на ссылки/указатели в других языках -- они никогда не указывают на другие переменные/ссылки, а только на основные значения.