Skip to content

Latest commit

 

History

History
300 lines (196 loc) · 27 KB

File metadata and controls

300 lines (196 loc) · 27 KB

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

Глава 1: Типы

Большинство разработчиков скажут, что динамический язык (как JS) не имеет типов. Посмотрим, что спецификация ES5.1 (http://www.ecma-international.org/ecma-262/5.1/) говорит об этом:

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

Тип языка ECMAScript соответствует значениям, которые непосредственно обрабатываются программистом ECMAScript с использованием языка ECMAScript. Типы языка ECMAScript это: Undefined, Null, Boolean, String, Number и Object.

Если вы поклонник строго типизированных (статически типизированных) языков, вы можете возражать против этого использования слова «тип». В этих языках «тип» означает гораздо больше, чем здесь, в JS.

Некоторые люди говорят, что JS не должен претендовать на то, что имеет «типы», и их следует вместо этого называть «тегами» или, допустим, «подтипами».

Фух! Мы будем использовать вот это грубое определение (то же самое, что, похоже, подразумевает формулировка из спецификации): тип — это встроенный набор характеристик, который однозначно идентифицирует поведение конкретного значения и отличает его от других значений, как для движка, так и для разработчика.

Другими словами, если и движок, и разработчик обрабатывают значение 42 (число) иначе, чем они обрабатывают значение "42" (строка), то эти два значения имеют разные типыnumber и string, соответственно. Когда вы используете 42, вы собираетесь делать что-то числовое, например, математику. Но когда вы используете "42", вы собираетесь делать что-то строковое, например, выводить на страницу и т.д. Эти два значения имеют разные типы.

Это далеко не идеальное определение. Но оно достаточно хорошее для обсуждения. И согласуется с тем, как JS описывает себя.

Тип, как бы он ни назывался...

За пределами академических разногласий в определениях, почему важно, имеет JavaScript типы или нет?

Наличие правильного понимания каждого типа и его внутреннего поведения абсолютно необходимо для понимания того, как правильно и точно преобразовывать значения в разные типы (см. «Приведение типов», глава 4). Почти каждая программа на JS, когда-либо написанная, должна будет обрабатывать приведение типов значений в каком-то виде или форме, поэтому важно, чтобы вы делали это ответственно и уверенно.

Если у вас есть значение 42 типа number, но вы хотите обрабатывать его как string, например, вытаскивая "2" в качестве символа в позиции 1, вы, очевидно, должны сначала преобразовать (привести) значение из number к string.

Это кажется довольно простым.

Но существует много разных способов такого приведения. Некоторые из них ясны, понятны и надёжны. Но если вы не будете осторожны, приведение может произойти очень странным и непредсказуемым образом.

Путаница с приведением, возможно, является одним из самых глубоких разочарований для разработчиков JavaScript. Его часто критикуют за то, что оно настолько опасно, что считается недостатком в устройстве языка, которого следует сторониться и избегать.

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

Встроенные типы

В JavaScript определены семь встроенных типов:

  • null
  • undefined
  • boolean
  • number
  • string
  • object
  • symbol — добавлен в ES6!

Примечание: Все эти типы, кроме object, называют "примитивами".

Оператор typeof проверяет тип заданного значения и всегда возвращает одно из семи строковых значений — и как ни странно, они не имеют точного соответствия с семью встроенными типами, которые мы только что перечислили.

typeof undefined     === "undefined"; // true
typeof true          === "boolean";   // true
typeof 42            === "number";    // true
typeof "42"          === "string";    // true
typeof { life: 42 }  === "object";    // true

// добавлен в ES6!
typeof Symbol()      === "symbol";    // true

Эти шесть перечисленных типов имеют значения соответствующего типа и возвращают строковое значение с таким именем, как показано в примере. Symbol — это новый тип данных из ES6, он будет рассмотрен в главе 3.

Как вы могли заметить, я исключил null из приведённого выше списка. Он особенный — особенный в том смысле, что в сочетании с оператором typeof он глючит:

typeof null === "object"; // true

Было бы неплохо (и правильно!), если бы вернулось "null", но эта оригинальная ошибка в JS сохраняется почти два десятилетия и, скорее всего, никогда не будет исправлена, потому что существует настолько много кода в вебе, который полагается на это глючное поведение, что «исправление» ошибки создало бы больше «ошибок» и сломало бы много веб-программ.

Если вы хотите проверить значение null, используя его тип, вам потребуется составное условие:

var a = null;

(!a && typeof a === "object"); // true

null — единственное примитивное значение, которое является «ложным» (или «подобным ложному», см. главу 4), но также возвращает «объект» из проверки типа.

Так каково же седьмое строковое значение, возвращаемое typeof?

typeof function a(){ /* .. */ } === "function"; // true

Легко решить, что function это встроенный тип верхнего уровня в JS, особенно учитывая такое поведение оператора typeof. Однако, если вы прочитаете спецификацию, вы увидите, что это фактически «подтип» объекта. В частности, функцию называют «вызываемым объектом» — объектом, который имеет внутреннее свойство [[Call]], которое позволяет ему вызываться.

Тот факт, что функции фактически являются объектами, весьма полезен. Самое главное, они могут иметь свойства. Например:

function a(b,c) {
	/* .. */
}

Объект функции имеет свойство length, определяемое числом формальных параметров, с которыми он был объявлен.

a.length; // 2

Поскольку вы объявили функцию с двумя формальными именованными параметрами (b и c), «длина функции» равна 2.

Что насчёт массивов? В JS они нативные, так особый ли это тип?

typeof [1,2,3] === "object"; // true

Нет, это просто объекты. Наиболее целесообразно думать о них также как о «подтипе» объектов (см. Главу 3), в данном случае — с дополнительными характеристиками численного индексирования (в отличие от просто строковых ключей, как у обычных объектов) и поддержкой автоматически обновляемого свойства .length.

Значения как типы

В JavaScript переменные не имеют типов — значения имеют типы. Переменные могут хранить любое значение в любой момент времени.

Другими словами, JS не имеет «принудительного применения типа», поскольку движок не настаивает на том, чтобы переменная всегда содержала значение того же начального типа, с которого инициализировалась. Переменная может в одном операторе присваивания содержать string, а в следующем — number и т.д.

Значение 42 имеет встроенный тип number, и этот тип не может быть изменён. Другое значение, например, "42" с типом string, может быть получено из значения 42 типа number с помощью приведения типов (см. Главу 4).

Если вы используете typeof для переменной, он не спрашивает: «Какой тип у этой переменной?», как может показаться, поскольку переменные в JS не имеют типов. Вместо этого он спрашивает: «Какой тип значения в этой переменной?».

var a = 42;
typeof a; // "number"

a = true;
typeof a; // "boolean"

Оператор typeof всегда возвращает строку. Поэтому:

typeof typeof 42; // "string"

Сначала typeof 42 возвращает "number", и typeof "number" это "string".

Неопределённые и необъявленные переменные

Переменные, которые не имеют значения в данный момент, фактически имеют значение undefined. Вызов typeof для таких переменных возвращает "undefined":

var a;

typeof a; // "undefined"

var b = 42;
var c;

// далее
b = c;

typeof b; // "undefined"
typeof c; // "undefined"

Для большинства разработчиков удобно думать о слове «undefined» («неопределённый») как о синониме слова «undeclared» («необъявленный»). Однако в JS две эти концепции — совершенно разные.

«Неопределённая» переменная — это та, которая была объявлена в доступной области видимости, но в данный момент не имеющая значения. В отличие от этого, «необъявленная» переменная — это та, которая не была объявлена в доступной области видимости.

Посмотрите внимательно:

var a;

a; // undefined
b; // ReferenceError: b is not defined

Досадную путаницу создаёт сообщение об ошибке, которое браузеры назначают этой ситуации. Как вы можете видеть, сообщение — «b is not defined» («b не определено»), которое, конечно, очень легко и логично спутать с «b is undefined» («b неопределено»). Ещё раз: «неопределено» и «не определено» — это совершенно разные вещи. Было бы неплохо, если бы браузеры писали что-то вроде «b не найдено» или «b не объявлено», чтобы уменьшить путаницу!

Существует также особое поведение у typeof, связанное с необъявленными переменными, которое ещё больше усиливает путаницу. Смотрите:

var a;

typeof a; // "undefined"

typeof b; // "undefined"

Оператор typeof возвращает "undefined" даже для «необъявленных» (или «не определённых») переменных. Обратите внимание, что при выполнении typeof b ошибки не возникло, хотя b является необъявленной переменной. Это особая мера безопасности в поведении typeof.

Как и в примере выше, было бы неплохо, если бы typeof, используемый с необъявленной переменной, возвращал «undeclared» («необъявлено») вместо объединения результата с другой ситуацией, когда переменная «undefined» («неопределена»).

typeof для необъявленных переменных

Тем не менее, эта мера безопасности является полезной функцией при работе с JavaScript в браузере, где несколько файлов скриптов могут загружать переменные в общее глобальное пространство имён.

Примечание: Многие разработчики считают, что в глобальном пространстве имён никогда не должно быть никаких переменных, и что всё должно содержаться в модулях и закрытых/отдельных пространствах имён. Это прекрасно в теории, но практически невозможно на практике; хотя всё же это хорошая цель, к которой нужно стремиться! К счастью, ES6 добавил первоклассную поддержку модулей, что в конечном итоге сделает это гораздо более удобным.

В качестве простого примера представьте, что в вашей программе есть «режим отладки», который управляется глобальной переменной (флагом) под названием DEBUG. Вы хотите проверить, была ли объявлена эта переменная перед выполнением задачи отладки, например, вывода сообщения в консоль. Глобальное объявление var DEBUG = true верхнего уровня будет включено только в файл «debug.js», который вы загружаете в браузер только тогда, когда находитесь в режиме разработки/тестирования, но не для продакшна.

Однако вы должны позаботиться о том, как вы проверяете глобальную переменную DEBUG в остальной части кода своего приложения, чтобы не получить ReferenceError. В этом случае безопасность в typeof станет вашим другом.

// упс, здесь будет ошибка!
if (DEBUG) {
	console.log( "Debugging is starting" );
}

// это безопасная проверка на наличие
if (typeof DEBUG !== "undefined") {
	console.log( "Debugging is starting" );
}

Этот вид проверки полезен, даже если вы работаете не с переменными, определяемыми пользователем (как DEBUG). Если вы выполняете проверку функционала для встроенного API, для вас также может быть полезна проверка без выброса ошибки:

if (typeof atob === "undefined") {
	atob = function() { /*..*/ };
}

Примечание: Если вы определяете полифилл для функционала, которого ещё не существует, вы, вероятно, захотите избежать использования var, чтобы объявить atob. Если вы объявляете var atob внутри оператора if, это объявление всплывёт (см. книгу Область видимости и замыкания из этой серии) в верхнюю часть области видимости, даже если условие if не выполнится (потому что глобальная atob уже существует!). В некоторых браузерах и для некоторых специальных типов глобальных встроенных переменных (часто называемых «host-объектами») это дублирующее объявление может вызвать ошибку. Опущение var предотвращает это объявление от всплытия.

Другой способ выполнения этих проверок глобальных переменных, но без защиты typeof — это увидеть, что все глобальные переменные также являются свойствами глобального объекта, который в браузере обычно является объектом window. Таким образом, вышеуказанные проверки могли быть выполнены (совершенно безопасно) вот так:

if (window.DEBUG) {
	// ..
}

if (!window.atob) {
	// ..
}

В отличие от ссылок на необъявленные переменные, при попытке доступа к свойству объекта (даже глобального объекта window), ошибки ReferenceError выброшено не будет.

С другой стороны, ручное обращение к глобальной переменной с помощью window — это то, чего некоторые разработчики предпочитают избегать, особенно если ваш код должен запускаться в нескольких JS-средах (не только в браузерах, но и в серверном node.js, например), где глобальная переменная не всегда называется window.

Технически, эта защита в typeof полезна, даже если вы не используете глобальные переменные, хотя такие условия более редки, и некоторые разработчики могут найти этот подход к стилю кода менее желательным. Представьте себе функцию-утилиту, которая предназначена вами для копирования и вставки в чужие программы или модули, в которой вы хотите проверить, определила ли основная программа какую-то переменную (чтобы вы могли её использовать) или нет:

function doSomethingCool() {
	var helper =
		(typeof FeatureXYZ !== "undefined") ?
		FeatureXYZ :
		function() { /*.. функция по умолчанию ..*/ };

	var val = helper();
	// ..
}

doSomethingCool() проверяет переменную с именем FeatureXYZ, и если она найдена, использует её, но если нет, то использует свою собственную. Теперь, если кто-то включает эту утилиту в свой модуль/программу, она надёжно проверяет, определена FeatureXYZ или нет:

// IIFE (см. обсуждение "Immediately Invoked Function Expressions"
// ("Немедленно вызываемых функций") в книге *Область видимости и замыкания* из этой серии)
(function(){
	function FeatureXYZ() { /*.. моя функция XYZ ..*/ }

	// подключаем `doSomethingCool(..)`
	function doSomethingCool() {
		var helper =
			(typeof FeatureXYZ !== "undefined") ?
			FeatureXYZ :
			function() { /*.. функция по умолчанию ..*/ };

		var val = helper();
		// ..
	}

	doSomethingCool();
})();

Здесь FeatureXYZ — вовсе не глобальная переменная, но мы по-прежнему используем защиту typeof, чтобы сделать проверку безопасной. И что важно, здесь нет объекта, который мы можем использовать (как мы делали для глобальных переменных с window.___), чтобы сделать проверку, поэтому typeof весьма полезен.

Другие разработчики предпочли бы шаблон проектирования, называемый «внедрение зависимости», где вместо неявной проверки в doSomethingCool(), объявлена ли FeatureXYZ вне её, нужно явно передать зависимость, например:

function doSomethingCool(FeatureXYZ) {
	var helper = FeatureXYZ ||
		function() { /*.. функция по умолчанию ..*/ };

	var val = helper();
	// ..
}

Существует множество вариантов реализации такого функционала. Ни один шаблон здесь не является «правильным» или «неправильным» — существуют разные нюансы у каждого подхода. Но в целом, приятно, что защита typeof для необъявленных переменных даёт нам больше возможностей.

Обзор

В JavaScript есть семь встроенных типов: null, undefined, boolean, number, string, object, symbol. Их можно определить с помощью оператора typeof.

Переменные не имеют типов, но их имеют значения переменных. Эти типы определяют внутреннее поведение значений.

Многие разработчики полагают, что «неопределённый» и «необъявленный» — это примерно одно и то же, но в JavaScript это совершенно разные вещи. undefined («неопределённый») — это значение, которое может содержать объявленная переменная. «Undeclared» («необъявленный») означает, что переменная не была объявлена.

JavaScript, к сожалению, в некотором роде отождествляет эти два термина, не только в сообщениях об ошибках («ReferenceError: a is not defined»), но также и в значении, возвращаемом typeof, являющемся "undefined" для обоих случаев.

Однако защита (предотвращение ошибки) в typeof при использовании с необъявленной переменной может быть полезна в некоторых случаях.