Как и любая компьютерная программа, JavaScript нуждается в наведении порядка в данных, в их структурировании. На языке JavaScript мы говорим, что если данные имеют одинаковую структуру, то они имеют одинаковый тип. Внутренняя организация данных может быть простой, как ДА или НЕТ, а может быть весьма замысловатой, как дерево HTML-элементов или маршруты на карте навигатора.

Примитивные типы. JavaScript предлагает разработчику несколько простых, примитивных, типов. Среди них: boolean, number, string. Примитивными эти типы называют за то, что значения этих типов нельзя поменять. Их можно клонировать, встроить в другие значения... Через минуту увидим как это происходит

Работа с наборами. Дополнительно JavaScript предлагает несколько типов объектов для работы с наборами — массивы, словари, множества. Это очень кстати, попробуй вспомни, как эффективно реализовать сортировку.

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

Давайте обсудим детали разных типов данных, как примитивных, так и встроенных. Поговорим и о кастомных типах.

Примитивные типы

JavaScript различает семь типов:

undefined — обозначает тип значения переменной, которую объявили, но не инициализировали. Этот тип для данных, которых нет.

boolean — принимает только два значения «истина» и «ложь».

number — попытка научиться записывать любое число в ограниченную память компьютера (провалилась).

string — последовательность символов. JavaScript умеет преобразовывать значение любого примитивного типа в строку. Значение типа string в JavaScript неизменяемое, нельзя изменить одну букву в слове.

symbol — специальный тип данных. Задача значений типа symbol — служить специальными именами для обозначения специальных свойств объектов. Так много слов «специально», что мы дадим разъяснения ниже.

bigint — низкий поклон в сторону крипро- валют, токенов, контрактов. Они оперируют большими числами, и там этот тип данных очень пригождается. Скорее всего, не пригодится в вашем обычном фронтенде.

object — кроме примитивных типов в JavaScript есть структурные типы (объекты), а среди объектов особо выделяют функции. Обратите внимание, что в JavaScript функции это тоже объекты.

А как же null? null — ещё одна возможность обозначить данные, которых нет. Да, в JavaScript есть целых два способа для обозначения ситуации, когда в переменной нет значения. Обратите внимание, что в JavaScript null это значение, а не тип данных и это значение может содержаться в JSON, а undefined в JSON не бывает.

Тип undefined

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

let buffer;

Переменная buffer объявлена с ключевым словом let, но до тех пока она не получила явного значения ее значение undefined.

Хитрость JavaScript в том, что иногда разработчик присваивает переменной результат работы функции, но переменная все равно остается undefined. В следующем примере buffer получает значение при объявлении. Можете ли вы догадаться почему buffer всё равно undefined?

function trickyComputation (){
  let a = 0;
  while(a<3){
    a+=2;
  }
}
let buffer = trickyComputation();

Все дело в том, что trickyComputationне возвращает значения. Попробуйте рассуждать о том, что цель функции — «присвоить значение» ключевому слову return. В вышеприведенном примере нет return, и этот самый воображаемый return остается неинициализированным, отсюда и undefined.

Будьте внимательны к функциям.

Тип boolean

В JavaScript про любое значение можно задать вопрос «похоже ли оно на правду?». Значение true — правда, false — ложь. Пустое значение обычно значит false.

Посмотрим на практике. Для проверки правдивости будем пользоваться вот таким методом showTrueness.

const showTrueness = (n,value)=> console.log(`${n} - ${value?'true':'false'}`);

Вот явная ложь:

// эти значения похожи на ложь
showTrueness(1,false); // false
showTrueness(2,''); // false
showTrueness(3,0) // false

Когда значение отличается от банально-начального, оно обычно истинно.

showTrueness(1, true); // true
showTrueness(2, 42); // true
showTrueness(3, 'false') // true

Как видите, слово false получает приговор — истина! Но это еще не самый курьёзный случай с boolean:

showTrueness(1, []); // true
showTrueness(2, {}); // true
showTrueness(3, [] === []); // false
showTrueness(4, {} === {}); // false

Хотя обычно пустые значения это ложь, пустой массив (1) и пустой объект (2) оказались истиной. А ещё в следующем отрывке JavaScript рассматривает число 0 эквивалентной строке ‘0’.

Видите 0=='0' показывает true. При этом по отдельности число 0 и строка '0' относятся к разным типам.

showTrueness(1, 0 == '0'); // true
showTrueness(2, 0); // false
showTrueness(3, '0'); // true

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

Для избежания сюрпризов используйте явное преобразования значений между типами и строгое сравнение (===) (! ==). В этом вам поможет набор правил eslinter от академии.

Тип number

Тип значения number предназначен для моделирования действительных чисел. Действительных чисел очень много — бесконечно много — а компьютерная память ограничена. Инженерам из IEEE пришлось даже выпустить отдельный международный стандарт для чисел с плавающей точкой — IEEE 754.

Во многих случаях числа с плавающей точкой ведут себя нормально. Сумма чисел 1 и 2 равна 3, а сумма чисел 0.1 и 0.2 равна сумме чисел 0.2 и 0.1. Но это не всегда так, и 0.1 + 0.2 может быть не равно 0.3. Если в школе вам говорили, что от перемены мест сумма не меняется, то в JavaScript это не всегда верно.

И если будете делать выписку в крипто-банке, два раза подумайте, в каком порядке вы хотите складывать числа. Потому что значение переменной currentBalanceV1 больше значения переменной currentBalanceV2, а всё потому что величины складываемых чисел сильно отличаются.

showTrueness(1, 1+2 === 3); // true

const lastMonthCredits = Array.from ({length:30},()=>Number.EPSILON/10);
const previousMonthBalance = 0.8;
const currentBalanceV1 = previousMonthBalance + lastMonthCredits.reduce((a,b)=>a+b,0);
const currentBalanceV2 = 0 + lastMonthCredits.reduce((a,b)=>a+b,previousMonthBalance);
showTrueness(2, currentBalanceV1>currentBalanceV2); // true

showTrueness(3, 0.1+0.2 === 0.2+0.1) // true

Советы при работе с числами

  • При работе с числами старайтесь сначала делать действия над числами сравнимой величины
  • При работе с числами старайтесь сравнивать их порядок, (что больше, а что меньше), а не равенство.
  • Изучите назначение предопределенных констант Number.MAX_SAFE_INTEGER, Number.EPSILON и других.

Задание для самопроверки. На сколько отличаются currentBalanceV1, currentBalanceV2 и previousMonthBalance?

Тип string

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

const show = (value)=>console.log(value);
const alfavit = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя';
show(alfavit); // "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"
show(alfavit[1-1]); // "а"
show(alfavit[33-1]); //"я"

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

alfavit[2-1]='*';
show(alfavit); // "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"

В виде строк можно хранить любой вид данных, нужно только договориться. Однако мы не рекомендуем вам изобретать велосипед. Лучше используйте общепринятые способы преобразования данных в строку и обратно: JSON.stringify, JSON.parse (для превращения в JSON и обратно), Intl.NumberFormat, Intl.DateFormat.

Оператор typeof

Вы можете использовать оператор typeof для определения типа значения. Этот оператор воздействует на переменную и возвращает имя типа (если знает)

const show = (value, type)=>console.log(`typeof (${value}) is ${type}`);

const whatIsMyTypeName = (value)=>{
  switch(typeof value){
  case 'boolean': 
  case 'number':
  case 'string': return show(value, typeof value);
  default: show(value, 'выясним позднее')
  }
}

whatIsMyTypeName(true); // typeof (true) is boolean"
whatIsMyTypeName(42); // "typeof (42) is number"
whatIsMyTypeName('миру-мир!'); // "typeof (миру-мир!) is string"
whatIsMyTypeName({}); // "typeof ([object Object]) is выясним позднее"
whatIsMyTypeName(window); // "typeof ([object Window]) is выясним позднее"

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

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

Array (массивы)

Сразу обратите внимание, что слово Array мы употребляем с большой буквы, в то время как boolean, number, string — с маленькой.

Если вы разработчик на JavaScript, то методы работы с массивами нужно знать и уметь вспомнить, даже если вас разбудили посреди ночи. Проверьте, что вы знаете о существовании методов массива length, from, map, sort, reduce, filter, find, indexOf, findIndex

Вы можете получить значение из массива по индексу, а можете заменить это значение другим. Индексом значения в массиве выступает число. Индексы идут от 0 в сторону увеличения.

JavaScript не будет следить за тем, обращаетесь ли вы по индексу правомерно или выходите за пределы массива. Вы даже можете положить новое значение по индексу «минус один» и получить его обратно. Однако вас ждут сюрпризы, если вы не будете контролировать размеры массива и значения индекса.

const show = (index, value)=> console.log(`${index}: ${value}`);
const items = [
'Chrome',
'Opera',
'Edge'
];

// Предоставляет элемент
show(1,items[1]) // "1: Opera" 
// Ошибки нет, но и элемента тоже
show(100, items[100]); "100: undefined"

// Как, впрочем, и здесь 
show(-1, items[-1]);// "-1: undefined"

items[12] = 'safari';
items[-1] = 'IE';

// Ошибки нет, а элемент появился 
show(12, items[12]);//"12: safari"
show(-1, items[-1]);// "-1: IE"

// а где IE?
show('all', items)"all: Chrome,Opera,Edge,,,,,,,,,,safari"

// А тут всё ещё есть
show(-1, items[-1]);// "-1: IE"

Совет: контролируйте индексы, знайте размер массивов, с которыми работаете.

Set

Тип данных Set позволяет вам хранить набор уникальных элементов. Этим он отличается от массива. При работе с массивом вам придется предпринимать специальные усилия для поддержания уникальности элементов, Set сделает это за вас. В отличии от массива Set не позволяет произвольный доступ к элементу. Вы можете проверить наличие и получить список в порядке вставки.

const items = new Set(Array.from ({length:12},(_,ix)=>ix));

const str = (value)=>`${value}`;
const compare = (left,right)=>left<right?-1:right<left?1:0;
const byNumbericalValue = (left,right)=>compare(left,right);
const byStringValue =(left,right)=>compare(str(left),str(right));

console.log([...items].sort(byNumbericalValue)); 
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

console.log([...items].sort(byStringValue)); 
// [0, 1, 10, 11, 2, 3, 4, 5, 6, 7, 8, 9]

items.add(100);
items.add(20);

//порядок, в котором JavaScript отдает содержимое Set, зависит от порядка добавления элементов
console.log([...items].sort(byNumbericalValue));
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20, 100]

console.log([...items].sort(byStringValue));
// [0, 1, 10, 100, 11, 2, 20, 3, 4, 5, 6, 7, 8, 9]

console.log([...items]); 
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 100, 20]

Совет: для вывода элементов из Set в нужном порядке позаботьтесь о функции сортировки.

Кастомные объекты {} — ассоциативный массив

JavaScript объекты — это словари, где строковому ключу поставлен в соответствие элемент-значение. Возможны два синтаксиса доступа к значению ключа

const dictionary = {
  'language':'javascript',
  'type': 'structured',
  'age': 25
}

console.log(dictionary.language); // javascript
console.log(dictionary['language']); // javascript
const key = 'language';
console.log(dictionary[key]); // javascript

Важно! Ключ в кастомных объектах — это строковое значение. JavaScript неявно преобразует значение к строковому типу перед использованием его в качестве ключа.

Это ограничение не позволяет использовать объекты для создания связей между HTML-элементами и дополнительными данным, нужными для работы программы. Для этого используйте тип Map.

Map

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

const DictionaryCO = {}; // инициализируем вариант ассоциативного массива на основе обчного объекта
const DictionaryMap = new Map(); // инициализинуем вариант ассоциативного массива на основе встроенного типа Map

// Первый эксперимент будет связывать ключ 'k1' и значение 'v1'
const aKey = 'k1';
const aValue = 'v1';

// Вторая второй эксперимент будет связывать ключ в виде объекта и значение - строку
// такой вариант в жизни встечается если для нескольких HTML элементов нужно ввести дополнительные данные, которые нельзя положить в data-xxx атрибут
const bKey = {x:1};
const bValue = 'v2';
// Третий эксперимент покажет нам недостатки ассоциативного массива на основе обычного объекта. Мы используем в качестве ключа - другой объект
const cKey = {y:2};
const cValue = 'v3';

// заполняем первый ассоциативный массив (который в виде объекта)
DictionaryCO[aKey] = aValue;
DictionaryCO[bKey] = bValue;
DictionaryCO[bKey] = bValue;

// заполняем второй ассоциативный массива (которы в виде Map)
DictionaryMap.set(aKey, aValue);
DictionaryMap.set(bKey, bValue);
DictionaryMap.set(cKey, cValue);

console.log('a object', DictionaryCO[aKey]); // "a object", "v1"
console.log('b object', DictionaryCO[bKey]); // "b object", "v2"
// хотя мы пытаемся получить значение по ключу cKey, почему-то
// мы получаем значение, которое связали с ключом bKey
console.log('c object, oops!', DictionaryCO[cKey]); // "c object, oops!", "v2"

console.log('a map', DictionaryMap.get(aKey)); // "a map", "v1"
console.log('b map', DictionaryMap.get(bKey)); // "b map", "v2"
// в этом варианте ассоциативного массива все сработало так, как мы и ожидали
console.log('c map', DictionaryMap.get(cKey)); // "c map", "v3"

Собственные структуры данных

Утиная типизация

Программисты работают с лозунгом «Algorithms + Data Structures = Programs». При работе с JavaScript разработчик преобразует требования заказчика в алгоритмы и структуры данных. Примитивных типов данных, доступных в JavaScript явно недостаточно для всего разнообразия бизнес задач, поэтому приходится использовать кастомные структуры. Для приложение электронной очереди потребуется структура с номером и временем, для умного дома — структура с данными о температуре чайника, заполненности холодильника и т. п.

let cusomerInQueue = {
  numberOnScreen: 'A42',
  timeTaken: '11:40'
}

let smartHouseState = {
  kettler: 80,
  fridge:{
    milk: true,
    banana: false,
  },
}

В вашей программе вы получаете сведения о заполнении холодильника, потому что ожидаете наличия поля fridge в структуре объекта, а не потому, что переменная называется smartHouseState. Выяснив, что в структуре объекта нет поля fridge вы смело можете сказать, что значение переменной cusomerInQueueне связано с управлением умным домом, и наоборот наличие такого поля в переменной smartHouseState подсказывает вам, что её значение описывает умный дом. Такой подход, когда о назначении значения вы судите по его структуре, называется структурной типизацией.

Иногда вы знаете структуру значения (потому что знаете), а иногда вам приходится как-то догадываться. Но как, ведь typeof в этой ситуации не поможет?

Object.keys и другие шпионские средства

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

Основными помощниками в исследовании полученных данных вам будут

  • Статический метод Object.keys
  • Оператор typeof
  • Оператор instanceof
  • Статический метод Object.hasOwn
const someValue = {
  title: 'Cruella',
  release: new Date(2021,05,03),
  empty: undefined,
}

console.log('all keys', Object.keys(someValue));
// "all keys", ["title", "release", "empty"]

console.log('typeof key release', typeof someValue.release);
// "typeof key release", "object"

console.log('instance of Date', someValue.release instanceof Date)
// "instance of Date", true

console.log('missing value', typeof someValue.empty);
// "missing value", "undefined"

console.log('missing key', Object.hasOwn(someValue, 'missing'));
// "missing key", false

Проблемы типов JavaScript

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

Например, у нас была функция transform<. Мы обнаружили, что она ломается при получении значения null.

const transform = (value) => value.replace('с', 'б');

let data = 'соль'

console.log(transform(data).toUpperCase()) // "БОЛЬ"
data = null;
try {
  console.log(transform(data).toUpperCase()) // не выполнится
} catch (err) {
  console.log(err.message) // "Cannot read properties of null (reading 'replace')"
}

Исправили программу в одном месте — она начала ломаться в другом. Как разорвать замкнутый круг?

const transform2 = (value) => {
  if (typeof value === 'string') {
    return value.replace('с', 'б');
  }
}
data = 'соль';
console.log(transform2(data).toUpperCase()) // "БОЛЬ"
data = null
try {
  console.log(transform2(data).toUpperCase())
} catch (err) {
  console.log(err.message) // "Cannot read properties of undefined (reading 'toUpperCase')"
}

Вам поможет TypeScript! Но об этом в следующий раз.

TypeScript — будущее фронтенда

Пройдя курс, вы сможете уверенно использовать TypeScript в любых проектах — как во фронтенде, так и в бэкенде.

Хочу