Как и любая компьютерная программа, 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[cKey] = cValue;
// заполняем второй ассоциативный массива (которы в виде 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! Но об этом в следующий раз.
Узнать больше
- Что нужно знать фронтендеру, кроме JavaScript
- 12 полезных книг по JavaScript
- Зачем фронтендерам React, если есть JavaScript
- TypeScript. Зачем он нужен и почему так популярен