Dmitry Vdovichenko

Вы не знаете JS. Типы данных и грамматика. Значения

Что я узнал, прочитав главу 2 книги Кайла Симпсона "Вы не знаете JS. Типы данных и грамматика. Значения".

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 "Массивы".

Массивы arrays проиндексированы числами (как и ожидается), но хитрость в том, что они могут иметь индекс в виде строки 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 для хранения пар ключ/свойство, а массивы arrays приберегите для хранения значений в ячейках с числовыми индексами.

Массивоподобные

Бывают случаи когда нужно преобразовать массивоподобное значение (пронумерованную коллекцию значений) в настоящий массив 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;
	};
}

32-битные целые числа (со знаком)

Пока целые числа могут быть приблизительно до 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 является (к несчастью) идентификатором. Увы и ах.

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. Никогда.

Оператор void

Пока 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) {
		// проверка на `-0`
		if (v1 === 0 && v2 === 0) {
			return 1 / v1 === 1 / v2;
		}
		// проверка на `NaN`
		if (v1 !== v1) {
			return v2 !== v2;
		}
		// любые другие значения
		return v1 === v2;
	};
}

Object.is(..), возможно, не должен быть использован в случаях, когда известно что == или === являются безопасными (подробнее в Главе 4 "Преобразование"), как операторы, они, вероятно, более эффективны и просты в применении. Object.is(..) по большей части применяется в специальных случаях проверки на равенство.

Значение против Ссылки

Какт во многих других языках, значения могут быть присвоены/переданы либо с помощью копирования-по-значению,либо с помощью копирования-по-ссылке oв зависимости от синтаксиса, который вы используете.

Например, в C++ если вы хотите передать число number переменной в функции и иметь обновленное значение переменной, вы можете объявить параметр функции например int& myNum, и когда вы передадите ему переменную например x, myNum будет ссылаться на x; ссылки -- это как особые формы указателей, когда вы получаете указатель на другую переменную (как алиас (псевдоним)). Если вы не объявляете ссылочный параметр, переданное значение всегда будет скопировано, Даэе если это сложный объект.

В JavaScript, нет указателей, и ссылки работают немного по-другому. вы не можете получить ссылку от одной JS переменной на другую. Это просто невозможно.

ССылки в JS указывают на (общее) значение, так если у вас есть 10 разных ссылок, они всегда будут разными ссылками на одно общее значение; ни одна из этих ссылок/указателей tне будет указывать друг на друга.

Более того, в 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,и ES6 symbol.

Сложные значения -- объекты object (включая массивы array, и все объекты-обертки -- подробнее в Главе 3) и функции function -- всегда всегда делают копию по ссылке при назначении или передаче.

В примере выше, т.к. 2 это примитив, a содержит начальную копию этого значения, а переменной b присвоена другая копия значения. При изменении b, вы никоим образом не меняете значение в переменной a.

Но обаc и d отдельные ссылки на одно общее значение [1,2,3], которое является сложным значением. Важно понимать что никто из переменных: ни c ни d не "обладает" значением [1,2,3] в большей степени -- они оба всего лишь равноправные ссылки на значение. Таким образом, когда мы используем любую ссылку для изменения (.push(4)) актуального общего значения array самого по себе, это влияет только на это общее значение, и обе ссылки будут указываьб на новое измененное значение [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 должно указывать на a вместо того,чтобы ссылаться на массив array -- но такой возможности в 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], мы никак не влияем на то значение, на которое изначально указывала переменная a -- значит, она все еще указывает на (теперь измененное) значение [1,2,3,4].

Нельзя с помощью ссылки x изменить место, куда ссылается a. Мы можем лишь изменить содержимое общего значения, на которое указывют a и x.

Чтобы добиться изменения содержимого переменной a на значение [4,5,6,7], вы не можете создать и назначить новый массив array -- вы должны изменить существующее значение массива array:

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) не создавали но массив array, а изменяли существующий общий массив array. Таким образом, конечно, a ссылается на новое значение [4,5,6,7].

Помните: вы не можете напрямую управлять/переопределять тип копирования: по-значению или по-ссылке -- эти правила полностью контролируются типом основного значения.

Чтобы эффективно передать сложное значение (например массивarray) с помощью копирования по-значению, вам понадобится вручную создать его копию, так чтобы переданная ссылка больше не указывала на оригинал. Например:

foo( a.slice() );

slice(..) без параметров по умолчанию делает полностью новую (поверхностую) копию массива array. Таким образом, мы передаем ссылку только на скопированный массив array, а значит foo(..) не может повлиять на содержимое a.

Чтобы выполнить обратное действие -- передать примитивное значение таким способом, что его изменения будут, навроде как ссылка -- вам понадобится обернуть значение в другое сложное значение (object, array, и т.п.), которое может быть передано копированием по-ссылке:

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, переменная x просто содержит примитивное значение 3 являющееся результатом математического действия 2 + 1. Таким образом, b снаружи все еще ссылается на оригинальный неизмененный/неизменный объект Number содержащий значение 2.

Вы можете добавить свойство поверх объекта Number (не изменяя его примитивного значения), так вы сможете обменимваться информацией косвенно через дополнительные свойства.

В любом случае, это не является общепринятым; и, возможно, большинство разработчиков не считают это хорошей практикой.

Вместо использования объекта-обертки Number таким способом, возможно, гораздо удобнее использовать обычный, созданный вручную, объект (obj), о котором говорилось в примере ранее. Никто не говорит, что нет разумного использования объекта-обертки Number -- просто возможно предпочтительнее будет использовать примитивное значение в большинстве случаев.

Ссылки достаточно мощные, но иногда они есть там где вам нужно, а иногда они нужны вам там, где их нет. Единственное влияние которое у вас есть при выборе типа копирования по-ссылке или по-значению это выбор типа самого значения, так что вы должны косвенно влиять на поведение присвоения/передачи путем выбора типа значений.

Обзор

В JavaScript, массивы array -- простые коллекции значений любого типа с пронумерованными ячейками. Строки string что-то "подобное массивам array", но у них есть различия в поведении и нужно быть осторожными при использовании строк как массивов array. Числа в JavaScript включают в себя как "целые" значения так и значения с плавющей точкой.

Среди примитивнх значений есть некоторые специальные значения.

Тип null имеет только одно значение: null, также как и тип undefined имеет только одно значение -- undefined. undefined -- изначальное стандартное значение в любой переменной или свойстве, если никакое другое значение не представлено. Оператор void позволяет вам получить значение undefined от любого другого значения.

Числа number включают в себя несколько специальных значений, например NaN (по идее "Не-Число", но на самом деле более предпочтительно "неправильное число"); бесконечности +Infinity и -Infinity; и -0.

Простые примитивные изначения (строки string, числа number, и т.п.) назначаются/передаются копированием по-значению, но сложные значения (объекты object, и т.п.) назначаются/передаются копированием по-ссылке. Ссылки в JS не такие как ссылки/указатели в других языках -- они никогда не указывают на другие переменные/ссылки, только на сами значения.