Изучение типов данных JavaScript и их особенностей

Одна из самых фундаментальных концепций программирования - это идея типов данных. Это концепция, которая почти повсеместно используется в основных языках программирования. Короче говоря, типы данных - это инструкции компилятору (или интерпретатору) программы относительно того, как он должен обрабатывать заданное значение. Учитывая, насколько фундаментальна эта идея для большинства языков программирования, вы можете подумать, что поведение определенного типа данных на одном языке будет таким же, как и на другом. В конце концов, почему string в Ruby должен вести себя иначе, чем string в JavaScript?

Что ж, для тех из вас, кто предпочитает простые вещи (например, я), у меня плохие новости: поведение типов данных имеет нюансы и часто зависит от их собственного языка. Вот почему так важно, чтобы при использовании языка вы хорошо понимали его причуды, чтобы не ожидать, что вы ожидаете одного набора поведения, а в конечном итоге получите другой. Имея это в виду, давайте погрузимся в типы данных в JavaScript и посмотрим, сможем ли мы разобраться в их причудах.

Мысленная модель для типов данных

Прежде всего, если мы собираемся говорить о типах данных, нам лучше придумать определение. Ранее я сказал, что типы данных - это инструкции для компилятора / интерпретатора программы, которые сообщают ему, как обрабатывать заданное значение, но что это означает на практике? Посмотрим, сможем ли мы немного развернуть концепцию.

Представьте, что я дал вам значения 2 и 3 и сказал вам сложить их вместе и сообщить мне результат. Вы почти наверняка просто вернете мне 5, никогда не переставая думать, какие виды ценностей я дал вам добавить. Но вот в чем дело - подсознательно вы действительно присвоили тип этим значениям. Учитывая контекст моего вопроса, вы, вероятно, предположили, что я имел в виду, что 2 и 3 должны быть числами, поэтому, конечно, результатом было 5. Но что, если я сейчас дам вам значения “two” и “three” и попрошу вас сложить их вместе? Из контекста я ясно дал вам две строки, и как вы сложите две строки вместе? Должны ли вы предположить, что я имел в виду, что они будут числами и вернуть мне 5? Или, более строго, вы должны предположить, что я хотел получить взамен конкатенированную строку и дать мне “twothree”?

Компиляторы / интерпретаторы несут ответственность за оценку значения предоставленного им кода и определение соответствующего действия. Без типов данных им было бы намного труднее выбирать между потенциально несопоставимыми поведениями, как в приведенном выше примере, когда вам нужно было решить, что означает «сложить» для двух чисел, а не для двух строк. Типы данных позволяют нам более идиоматично понимать значения, которые мы используем, что, в свою очередь, приводит к более предсказуемому поведению при фактическом запуске нашего кода.

Типы типов

В JavaScript есть два типа данных: «примитивные типы данных», которые являются неизменяемыми (подробнее об этом позже); и составные типы данных, которые являются изменяемыми. Пять основных примитивных типов данных: string; number; boolean; undefined; и null. (ECMAScript 2015 также добавил symbol в список примитивов, но мы не будем здесь вдаваться в подробности.) Существует множество составных типов данных, но наиболее распространенными являются: object; array; и function. Строго говоря, array и function являются подтипами object; однако у них есть некоторые уникальные особенности, поэтому мы рассмотрим их отдельно.

Итак, теперь, когда мы знаем, что такое разные типы данных, как мы можем идентифицировать их в наших программах? Для этого вам нужно использовать оператор typeof, который вы указываете значение и возвращает строковое представление типа этого значения. Давайте посмотрим на это в действии.

Как видно из приведенного выше фрагмента, в большинстве случаев оператор typeof дает нам ожидаемый ответ. Строковые значения дают нам “string”, числа дают нам “number”, а логические значения дают нам “boolean”. Однако есть некоторые особенности:

  • typeof 7 и typeof 7.5 оба дают нам “number”, а не различать “integer” тип и “float” / “decimal” тип, как в некоторых других языках.
  • typeof null дает нам “object”, а не “null”, по устаревшим причинам, которые почти наверняка никогда не будут исправлены;
  • typeof [] дает нам “object”, а не “array”, что неудивительно, поскольку массивы являются подтипами объектов (подробнее о том, как различать массивы позже); а также,
  • typeof function(){} дает нам “function”, хотя это тоже подтип объекта.

Слабый и динамический набор текста

Конечно, если у нас есть конкретное значение, его тип - это не единственное, что нас беспокоит. В большинстве случаев нам также нужна переменная для хранения значения, чтобы мы могли использовать его позже. Здесь проявляются два дополнительных аспекта типизации JavaScript. Во-первых, JavaScript слабо типизирован, что означает, что вам не нужно сообщать интерпретатору, какое значение вы планируете хранить в конкретной переменной. В C, например, если вы хотите сохранить целочисленное значение в переменной, вы должны использовать переменную, которая специально инициализирована для хранения целых чисел, как в int i = 7. Однако в JavaScript нет необходимости сообщать интерпретатору, какое значение вы планируете хранить в данной переменной. Вы просто объявляете свою переменную с помощью var, let или const и двигайтесь дальше!

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

В приведенном выше фрагменте мы определяем переменную someValue в строке 1 и предоставляем ей строку “Hello, world!”. Мы можем подтвердить, что someValue содержит строку, используя наш верный друг оператор typeof. Впоследствии в строке 5 мы переназначаем someValue для хранения числового значения 2018, и действительно, в строке 6, когда мы проверяем тип someValue, оно отображается как “number”. То же самое происходит в строках 9/10, когда мы снова переназначаем someValue пустому объекту и еще раз проверяем его тип. Обратите внимание, что когда мы проверяем тип someValue, мы проверяем не тип переменной (в конце концов, у нее ее нет), а тип текущего значения хранится в переменной.

Проверка случаев особого типа

Ранее мы обсуждали несколько причуд в системе типов JavaScript, например, когда массивы набираются как “object” (что является точным, но не очень полезным), целые и десятичные числа набираются как “number”, а не имеют свои собственные типы (что опять же верно, но не хватает конкретики), а null набирает “object” из-за устаревшей ошибки, которую нельзя исправить, не нарушив половину Интернета. Так что же делать, если вы хотите узнать, является ли конкретное значение массивом или объектом? Что ж, к счастью, есть несколько служебных методов и других приемов, которые вы можете использовать в таких случаях.

В приведенном выше фрагменте показано несколько примеров особых случаев. В строках 2–4 мы видим, что null значение типа “object”, но, к счастью, возвращает true по сравнению с null. В строках 7–10 мы используем встроенный метод Array.isArray, чтобы проверить, является ли значение именно массивом, а не просто объектом в целом. Точно так же в строках 13–16 мы используем встроенный метод Number.isInteger, чтобы проверить, является ли значение целым числом, а не числом. И, наконец, в строках 19–23 мы играем с раздражающе сбивающим с толку значением NaN («не-число»), которое возвращает “number” при вводе и false при сравнении с самим собой. Чтобы проверить, равно ли значение NaN, вам нужно либо использовать Number.isNaN встроенный метод, либо проверить, возвращает ли значение false по сравнению с самим собой (NaN - это единственное значение в JavaScript, которое имеет этот нечетный поведение.)

Изменчивость

Как кратко упоминалось ранее, одним из ключевых отличительных атрибутов между примитивными типами данных и составными типами данных является то, что первые являются неизменяемыми. Это означает, что вы не можете изменить примитивное значение. Важно, чтобы вы усвоили этот принцип, потому что от него зависит то, как работает ваш код. Более того, если вы переходите на JavaScript с другого языка, правила изменчивости могут быть другими.

Итак, что означает неизменность значения? Представьте, что у вас есть переменная с именем myInt, содержащая числовое значение 5. Независимо от того, какие методы вы вызываете для myInt, само значение 5 никогда не изменится, потому что числа являются одним из примитивных типов. 5 всегда 5. Однако это не означает, что myInt переменная (в отличие от значения 5, которое она хранит в настоящее время) никогда не может измениться. Вы можете запустить выражение, которое говорит myInt += 10, и действительно, myInt теперь будет 15. Но это не мутация, это переназначение. 5 по-прежнему 5, но myInt больше не указывает на него. Рассмотрим следующий пример:

В этом фрагменте у нас есть переменная someGreeting, которая содержит строковое значение “hello” (примитивный тип). У нас также есть вторая переменная, otherGreeting, указывающая на то же строковое значение. Если мы изменим строковое значение, на которое указывают эти две переменные, то мы ожидаем, что обе переменные отразят это изменение. В строке 7 мы пытаемся сделать это, вызывая якобы преобразующий метод concat на someGreeting; однако, когда мы затем регистрируем значения как someGreeting, так и otherGreeting, ни одно из них не изменилось. Это потому, что строки примитивны и не могут быть изменены. Метод concat вернул новую строку, но мы ничего с ней не сделали. Точно так же в строках 13–15 мы видим, что к отдельным символам в строке можно обращаться по индексу (как с массивом); однако, когда мы пытаемся переназначить один из этих символов, это не дает результата для всей строки, потому что, опять же, строки неизменяемы. Наконец, в строке 17 мы снова вызываем concat, на этот раз используя его возвращаемое значение для переназначения переменной someGreeting, и мы действительно видим ожидаемое изменение. Но поскольку это было переназначение, а не мутация, otherGreeting по-прежнему указывает на исходное значение.

Итак, примитивные типы не могут быть изменены, но как насчет составных типов? Что ж, они определенно могут быть видоизмененными. Однако обратите внимание, что составные типы, как следует из их названия, на самом деле являются структурами данных, содержащими отдельные элементы. Эти отдельные элементы могут быть либо составными типами, либо примитивными типами. По мере того, как вы копаетесь в структуре данных, как только вы достигаете примитивов на самом низком уровне, вы получаете данные, которые не могут быть изменены. Представьте себе массив строк - массив составной и изменчивый, но отдельные строки примитивны и неизменны. Давайте посмотрим на пример.

Здесь у нас есть массив с именем favoritePlanets, который содержит несколько строк. В строке 4 мы вызываем метод Array.prototype.sort, который сортирует массив на месте (что означает, что он изменяет исходный массив), и, как и ожидалось, наш массив изменяется. В строке 7 мы помещаем новую строку в массив и еще раз видим, что массив мутирован. Затем в строке 10 мы пытаемся изменить первый элемент в массиве favoritePlanets, но, как мы узнали ранее, на самом деле это не работает, и поэтому мы не получаем никаких изменений - тем самым демонстрируя, что составные типы изменчивы, но содержащиеся в них примитивные типы - нет. . Однако примитивные типы внутри составного типа можно переназначить. Мы можем увидеть это во второй части фрагмента, когда мы определяем объект с именем lifeDiscovered в строке 13, а затем переназначаем один из его элементов в строке 22. Объект lifeDiscovered был изменен в силу переназначения одной из его частей.

Принуждение

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

Принуждение бывает двух форм: неявное; и, явный. В первом случае интерпретатор смотрит на выражение, которое использует два разных типа данных, и использует набор внутренних правил, чтобы решить, следует ли принудительно привести одно или оба значения, чтобы у них были правила сопоставления. В последнем случае исходный код явно указывает интерпретатору, как он должен обрабатывать принуждение, а не позволяет ему полагаться на свои внутренние правила. Как это часто бывает, это легче всего заметить в коде:

В строках 2–6 мы видим несколько примеров неявного принуждения, в том числе:

  • Строка 2. Число 18 неявно приводится к строке, чтобы его можно было объединить в строку “20”.
  • Строка 3. Строка “20” неявно преобразуется в число, чтобы ее можно было умножить на число 18.
  • Строка 4. Логическое значение true неявно преобразуется в число (1), чтобы его можно было добавить к числу 20.
  • Строка 5. Строка “20” неявно преобразуется в число, чтобы ее можно было проверить путем свободного равенства с числом 20.
  • Строка 6: при использовании оператора строгого равенства принуждение не выполняется.

В строках 9–10 мы видим два примера явного принуждения:

  • Строка 9: Строка “20” явно приводится к числу и добавляется к числу 18 (что дает нам результат, отличный от версии с неявным принуждением этого выражения в строке 2).
  • Строка 10: число 20 и логическое значение true явно переводятся в строки и объединяются (что дает нам результат, отличный от версии этого выражения с неявным принуждением в строке 4.)

TL;DR

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

Вот и все, что касается еженедельника JavaScript Weekly на этой неделе. Надеюсь, вам понравилось. Если вы хотите получать уведомления о публикации новой статьи, вы можете подписаться на меня в Твиттере или подписаться на мой личный блог, где эти статьи публикуются между собой. Удачного кодирования!