skip to main content

Преобразование типов

js

Кратко 🔗

Представим ситуацию: у нас есть форма с полем, в которое пользователь вписывает свой возраст в годах.

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

Приведение (или преобразование) типов — это процесс конвертации значения из одного типа в другой.

В JavaScript типы можно преобразовывать явно и неявно.

Когда мы вызываем функцию, чтобы получить конкретный тип — это явное преобразование:

const x = "4"
Number(x)

const y = 4
String(y)

Сравнение бывает строгим и нестрогим. При строгом сравнении (===) интерпретатор учитывает типы сравниваемых значений.

Когда же мы *сравниваем значения нестрого_ значения между собой с помощью ==, JavaScript приводит типы самостоятельно:

5 == "5" // true
5 === "5" // false

Чтобы понять, почему так, нам надо сперва разобраться, какие типы в JS есть.

Сперва проведём границу между примитивными типами, объектами и другими.

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

В JavaScript примитивных типов 6.

// 1. Undefined
typeof undefined === "undefined"

// 2. Boolean, логический
typeof true === "boolean"
typeof false === "boolean"

// 3. Number, число
typeof 42 === "number"
typeof 4.2 === "number"
typeof -42 === "number"
typeof Infinity === "number"
typeof -Infinity === "number"

// 4. String, строка
typeof "" === "string"
typeof "string" === "string"
typeof "number" === "string"
typeof "boolean" === "string"

// 5. Symbol, символ, ES6
typeof Symbol() === "symbol"

// 6. BigInt, большое число, ES6
typeof 9007199254740991n === "bigint"
typeof BigInt(9007199254740991) === "bigint"

Примитивные типы — это такие типы, значения которых можно только перезаписать, но нельзя изменить.

Например, если мы создали переменную со значением 42, изменить это значение будет нельзя. Мы сможем его только полностью перезаписать:

let theAnswerToUltimateQuestion = 42
theAnswerToUltimateQuestion = 43
// Новое значение полностью перезаписало старое;
// старое собрано сборщиком мусора и забыто.

let theAnswers = [42, 43, 44]
theAnswers[0] = 142
// Теперь значение переменной [142, 43, 44];
// мы не перезаписали его полностью, а лишь изменили часть.

Этот механизм связан с тем, как значения переменных хранятся в памяти. Мы не пойдём слишком глубоко в эту тему, но, грубо говоря, примитивные типы «ссылаются на одно и то же значение в памяти», а непримитивные — на разные.

Из-за этого, например, примитивы можно сравнивать по значению:

const a = 5
const b = 5
a == b // true

А вот непримитивы — не получится:

const a = [1, 2, 3]
const b = [1, 2, 3]
a == b // false

// Даже несмотря на то, что массивы содержат одни и те же числа,
// при сравнении они не являются «одинаковыми».

// Когда JavaScript сравнивает a и b, он грубо говоря
// «сравнивает места в памяти, на которые ссылаются эти переменные».
// У непримитивов, эти места — разные, из-за чего они считаются неодинаковыми.

Объекты 🔗

Объекты в JavaScript используются для хранения коллекций значений.

Массивы (Array) в JS — тоже объекты.

Как мы уже говорили непримитивы сравниваются не по значению, а по ссылке. Объекты и массивы — это как раз непримитивы.

У объектов в JavaScript собственный тип — object.

const keyValueCollection = { key: "value" }
typeof keyValueCollection === "object"

const listCollection = [1, 2, 3]
typeof listCollection === "object"

null также относится к объектам:

typeof null === "object"

Обратите внимание, что для массивов и null оператор typeof возвращает также object.

Функции 🔗

У функций в JavaScript тоже собственный тип — function.

function simpleFunction() {}
typeof simpleFunction === "function"

const assignedFunction = function () {}
typeof assignedFunction === "function"

const arrowFunction = () => {}
typeof arrowFunction === "function"

typeof function () {} === "function"

Разницу между разными видами функций мы описали в статье о функциях Функции.

Undefined 🔗

У undefined — особый тип в JavaScript:

typeof undefined === "undefined"

Преобразование типов 🔗

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

В JavaScript существует лишь 3 типа конвертации: в строку, в число или в логическое значение.

Чтобы конвертировать значение в эти типы, можно воспользоваться одноимёнными функциями:

String(42) // Приводит к строке.
Number("42") // Приводит к числу.
Boolean(42) // Приводит к логическому значению.

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

// К строке:
String(123) // "123"
String(-12.3) // "-12.3"
String(null) // "null"
String(undefined) // "undefined"
String(true) // "true"
String(false) // "false"
String(function () {}) // "function () {}"
String({}) // "[object Object]"
String({ key: 42 }) // "[object Object]"
String([]) // ""
String([1, 2]) // "1,2"

К числу также можно пытаться приводить любые значения. Если JavaScript не сможет привести какое-то значение к числу, мы получим NaNособое значение, представляющее не-число (Not-a-Number).

// К числу:
Number("123") // 123
Number("123.4") // 123.4
Number("123,4") // NaN
Number("") // 0
Number(null) // 0
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
Number(function () {}) // NaN
Number({}) // NaN
Number([]) // 0
Number([1]) // 1
Number([1, 2]) // NaN

// Обратите внимание, что Number от пустого массива — 0,
// от массива с одним числом — это число
// и от массива с несколькими числами — NaN.
// Почему так происходит, мы поймём чуть ниже.

К логическому также можно приводить любые значения:

Boolean("") // false
Boolean("string") // true
Boolean("false") // true
Boolean(0) // false
Boolean(42) // true
Boolean(-42) // true
Boolean(null) // false
Boolean(undefined) // false
Boolean(function () {}) // true
Boolean({}) // true
Boolean({ key: 42 }) // true
Boolean([]) // true
Boolean([1, 2]) // true

// Грубо говоря, всё, кроме пустой строки, нуля,
// null и undefined — true.

Неявное преобразование типов 🔗

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

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

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

5 + "3" === "53"
5 - "3" === 2
5 + "-3" === "5-3"
5 - +3 === 2
5 + -3 === 2

// Из-за этого же появилась и такая шутка:
Array(16).join("wat" - 1) + " Batman!"
// "NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!"

Дело в том, как JavaScript пробует эти два типа «сопоставить» друг с другом, чтобы с ними работать.

Вначале посмотрим на примитивы.

  1. Интерпретатор приведёт примитивные значения к логическим, если мы используем && или ||.
  2. К строке, если мы используем +, когда один из операндов — строка.
  3. К числу, если:
    1. мы используем операторы сравнения <, <=, >, >=;
    2. используем арифметические операции -, + (за исключением пункта 2), /, *.
    3. используем унарный плюс: +'2' === 2;
    4. используем оператор нестрогого сравнения ==.

Но примитивами дело не заканчивается, JavaScript также неявно приводит и непримитивные значения.

Интерпретатор приводит их к логическому, если мы используем && или ||. (Объекты — всегда true).

С числом и строкой всё немного интереснее. Чтобы определить, к строке приводить значение или к числу, JavaScript смотрит, какой из двух методов (valueOf и toString) в текущем объекте объявлен.

  1. Если перед нами не объект Date, то метод valueOf вызывается, обычно, первым (если не сильно углубляться в детали спецификации).
  2. Если возвращённое после этого значение — это примитив, то возвращается оно.
  3. Если нет, то вызывается другой метод (если valueOf не вернул примитив, то вызывается toString и наоборот).
  4. Если после этого вернулся примитив, возвращается он.
  5. Если даже после этого не вернулся примитив, то будет ошибка Uncaught TypeError: Cannot convert object to primitive value.

На примерах 🔗

// 1. Простой объект
const obj1 = {}
obj1.valueOf() // {}
obj1.toString() // "[object Object]"

// Чтобы «сложить» число с объектом,
// вначале будет вызыван obj1.valueOf().
// Он вернёт объект (непримитив),
// после чего будет вызыван obj1.toString().

1 + obj1
// 1 + "[object Object]"
// "1" + "[object Object]"
// "1[object Object]"

// 2. Объект с указанным .valueOf()
const obj2 = {}
obj2.valueOf = () => "obj2"
obj2.valueOf() // "obj2";
obj2.toString() // "[object Object]"

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

1 + obj2
// 1 + "obj2"
// "1" + "obj2"
// "1obj2"

// 2.1. Если же мы будем возвращать число
const obj2 = {}
obj2.valueOf = () => 42
obj2.valueOf() // 42
obj2.toString() // "[object Object]"

1 + obj2
// 1 + 42
// 43

// 3. Даты
const date = new Date()
date.valueOf() // 1467864738527
date.toString() // "Sun Sep 15 2019..."

// У дат приоритет методов обратный:
// то есть вначале будет вызываться .toString(),
// и только после него — .valueOf().

1 + date
// 1 + "Sun Sep 15 2019..."
// "1" + "Sun Sep 15 2019..."
// "1Sun Sep 15 2019..."

Строгое и нестрогое равенство 🔗

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

В отличие от строгого равенства (===), в нём интерпретатор пробует привести типы к одному, чтобы сравнить.

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

Вот таблица нестрогого равенства (зелёным отмечены значения, которые «равны»):

Таблица нестрогого равенства

А вот — для строгого:

Таблица строгого равенства

Хорошей практикой считается использовать только строгое сравнение, чтобы избежать неявного преобразования типов при сравнении.

В работе 🔗

Всегда используйте строгое равенство при сравнении значений.

🛠 Для удобства проверку на существование объекта можно проводить через if (object), потому что объекты всегда приводятся к true.

const exists = {}
if (exists) {
/* эта ветка выполнится */
}

const doesntExist = undefined
if (doesntExist) {
/* эта ветка не выполнится */
}

🛠 Если хочется описать сложную структуру, которая бы умела «вести себя», как число или строка, можно описать методы .valueOf() или .toString().

const ticketPrice = {
amount: 20,
currency: "USD",
valueOf: () => 20,
toString: () => "$20",
}

1 + ticketPrice // 1 + 20 -> 21
console.log(ticketPrice) // $20

Автор: Саша,