Большинство разработчиков скажут, что динамический язык (как 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» («неопределена»).
Тем не менее, эта мера безопасности является полезной функцией при работе с 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
при использовании с необъявленной переменной может быть полезна в некоторых случаях.