Skip to content

Latest commit

 

History

History
executable file
·
985 lines (656 loc) · 64.6 KB

File metadata and controls

executable file
·
985 lines (656 loc) · 64.6 KB

Вы не знаете JS: Типы и грамматика

Глава 2: Типы

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 для представления других данных (например направление перемещения).

В этих случаях, как в примере выше, если переменная достигнет нуля и потеряет знак, тогда, вы потеряете информацию о том, откуда она пришла, до того как достигла нулевого значения. Сохранение знака нуля предупреждает потерю этой информации.

Специальное равенство

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.

Value vs. Reference

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 -- objects (including arrays, and all boxed object wrappers -- see Chapter 3) and functions -- 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.

Review

In JavaScript, arrays are simply numerically indexed collections of any value-type. strings are somewhat "array-like", but they have distinct behaviors and care must be taken if you want to treat them as arrays. 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.

numbers include several special values, like NaN (supposedly "Not a Number", but really more appropriately "invalid number"); +Infinity and -Infinity; and -0.

Simple scalar primitives (strings, numbers, etc.) are assigned/passed by value-copy, but compound values (objects, 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.