Кратко
Секция статьи "Кратко"Представим ситуацию: у нас есть форма с полем, в которое пользователь вписывает свой возраст в годах.
По умолчанию любой ввод в полях — это строка. Если мы хотим работать с этим значением, как с числом, то нам нужно привести его к числу.
Приведение (или преобразование) типов — это процесс конвертации значения из одного типа в другой.
В JavaScript типы можно преобразовывать явно и неявно.
Когда мы вызываем функцию, чтобы получить конкретный тип — это явное преобразование:
const x = "4"Number(x)const y = 4String(y)
const x = "4" Number(x) const y = 4 String(y)
Сравнение бывает строгим и нестрогим. При строгом сравнении (
) интерпретатор учитывает типы сравниваемых значений.
Когда же мы сравниваем значения нестрого между собой с помощью
, JavaScript приводит типы самостоятельно:
5 == "5" // true5 === "5" // false
5 == "5" // true 5 === "5" // false
Чтобы понять, почему так, нам надо сперва разобраться, какие типы в JS есть.
Сперва проведём границу между примитивными типами, объектами и другими.
Примитивные типы
Секция статьи "Примитивные типы"В JavaScript примитивные типы следующие:
// 1. Undefinedtypeof 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, символ, ES6typeof Symbol() === "symbol"// 6. BigInt, большое число, ES6typeof 9007199254740991n === "bigint"typeof BigInt(9007199254740991) === "bigint"// 7. Nulltypeof null === 'object'// О том, почему здесь “object” — чуть позже.
// 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" // 7. Null typeof null === 'object' // О том, почему здесь “object” — чуть позже.
Примитивные типы — это такие типы, значения которых можно только перезаписать, но нельзя изменить.
Например, если мы создали переменную со значением 42
, изменить это значение будет нельзя. Мы сможем его только полностью перезаписать:
let theAnswerToUltimateQuestion = 42theAnswerToUltimateQuestion = 43// Новое значение полностью перезаписало старое;// старое собрано сборщиком мусора и забыто.let theAnswers = [42, 43, 44]theAnswers[0] = 142// Теперь значение переменной [142, 43, 44];// мы не перезаписали его полностью, а лишь изменили часть.
let theAnswerToUltimateQuestion = 42 theAnswerToUltimateQuestion = 43 // Новое значение полностью перезаписало старое; // старое собрано сборщиком мусора и забыто. let theAnswers = [42, 43, 44] theAnswers[0] = 142 // Теперь значение переменной [142, 43, 44]; // мы не перезаписали его полностью, а лишь изменили часть.
Этот механизм связан с тем, как значения переменных хранятся в памяти. Мы не пойдём слишком глубоко в эту тему, но, грубо говоря, примитивные типы «ссылаются на одно и то же значение в памяти», а не примитивные — на разные.
Из-за этого, например, примитивы можно сравнивать по значению:
const a = 5const b = 5a == b // true
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, он грубо говоря// «сравнивает места в памяти, на которые ссылаются эти переменные».// У не примитивов, эти места — разные, из-за чего они считаются неодинаковыми.
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"
const keyValueCollection = { key: "value" } typeof keyValueCollection === "object" const listCollection = [1, 2, 3] typeof listCollection === "object"
У null
оператор typeof
возвращает object
, хотя это тоже примитив:
typeof null === "object"
typeof null === "object"
Функции
Секция статьи "Функции"У функций в JavaScript тоже тип — object
, хотя typeof
возвращает function
:
function simpleFunction() {}typeof simpleFunction === "function"const assignedFunction = function () {}typeof assignedFunction === "function"const arrowFunction = () => {}typeof arrowFunction === "function"typeof function () {} === "function"
function simpleFunction() {} typeof simpleFunction === "function" const assignedFunction = function () {} typeof assignedFunction === "function" const arrowFunction = () => {} typeof arrowFunction === "function" typeof function () {} === "function"
Разницу между разными видами функций мы описали в статье о функциях Функции.
typeof
Секция статьи "typeof" Оператор typeof
возвращает не непосредственно «тип», а строку. Для всех примитивов, кроме null
, этой строкой будет название этого примитива.
Для объектов он сначала проверит, можно ли его «вызвать». Функции — это как раз такие объекты, поэтому оператор возвращает function
.
Несмотря на то, что typeof
не всегда возвращает то, что мы бы могли ожидать, им удобно пользоваться в некоторых случаях в коде, например, для определения функций.
Преобразование типов
Секция статьи "Преобразование типов"Теперь, когда мы разобрались с типами, посмотрим, как мы можем преобразовывать значения одного типа в значения другого.
В JavaScript существует лишь 3 типа конвертации: в строку, в число или в логическое значение.
Чтобы конвертировать значение в эти типы, можно воспользоваться одноимёнными функциями:
String(42) // Приводит к строке.Number("42") // Приводит к числу.Boolean(42) // Приводит к логическому значению.
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"
// К строке: 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") // 123Number("123.4") // 123.4Number("123,4") // NaNNumber("") // 0Number(null) // 0Number(undefined) // NaNNumber(true) // 1Number(false) // 0Number(function () {}) // NaNNumber({}) // NaNNumber([]) // 0Number([1]) // 1Number([1, 2]) // NaN// Обратите внимание, что Number от пустого массива — 0,// от массива с одним числом — это число// и от массива с несколькими числами — NaN.// Почему так происходит, мы поймём чуть ниже.
// К числу: 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("") // falseBoolean("string") // trueBoolean("false") // trueBoolean(0) // falseBoolean(42) // trueBoolean(-42) // trueBoolean(NaN) // falseBoolean(null) // falseBoolean(undefined) // falseBoolean(function () {}) // trueBoolean({}) // trueBoolean({ key: 42 }) // trueBoolean([]) // trueBoolean([1, 2]) // true// Грубо говоря, всё, кроме пустой строки, нуля,// NaN, null и undefined — true.
Boolean("") // false Boolean("string") // true Boolean("false") // true Boolean(0) // false Boolean(42) // true Boolean(-42) // true Boolean(NaN) // false Boolean(null) // false Boolean(undefined) // false Boolean(function () {}) // true Boolean({}) // true Boolean({ key: 42 }) // true Boolean([]) // true Boolean([1, 2]) // true // Грубо говоря, всё, кроме пустой строки, нуля, // NaN, null и undefined — true.
Неявное преобразование типов
Секция статьи "Неявное преобразование типов"В секции выше мы преобразовывали типы «руками», с помощью функций. Но JavaScript может делать такие преобразования за нас самостоятельно. (Из-за чего в языке появляется много странностей, за которые его не очень сильно любят.)
Такая типизация, при которой тип значения определяется во время присвоения, а по ходу программы может меняться, — называется динамической.
Неявное преобразование происходит, когда мы заставляем JavaScript работать со значениями разных типов. Например, если мы хотим «сложить» число и строку:
5 + "3" === "53"5 - "3" === 25 + "-3" === "5-3"5 - +3 === 25 + -3 === 2// Из-за этого же появилась и такая шутка:Array(16).join("wat" - 1) + " Batman!"// "NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!"
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 пробует эти два типа «сопоставить» друг с другом, чтобы с ними работать.
Вначале посмотрим на примитивы.
- Интерпретатор приведёт примитивные значения к логическим, если мы используем
&&
или||
. - К строке, если мы используем
+
, когда один из операндов — строка. - К числу, если:
- мы используем операторы сравнения
<
,<
,= >
,>
;= - используем арифметические операции
,- +
(за исключением пункта 2),
,/ *
. - используем унарный плюс:
+'2'
;= = = 2 - используем оператор нестрогого сравнения
.= =
- мы используем операторы сравнения
Но примитивами дело не заканчивается, JavaScript также неявно приводит и не примитивные значения.
Интерпретатор приводит их к логическому, если мы используем &&
или ||
. (Объекты — всегда true
).
С числом и строкой всё немного интереснее. Чтобы определить, к строке приводить значение или к числу, JavaScript смотрит, какой из двух методов (valueOf
и toString
) в текущем объекте объявлен.
- Если перед нами не объект
Date
, то методvalueOf
вызывается, обычно, первым (если не сильно углубляться в детали спецификации). - Если возвращённое после этого значение — это примитив, то возвращается оно.
- Если нет, то вызывается другой метод (если
valueOf
не вернул примитив, то вызываетсяtoString
и наоборот). - Если после этого вернулся примитив, возвращается он.
- Если даже после этого не вернулся примитив, то будет ошибка
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 = () => 42obj2.valueOf() // 42obj2.toString() // "[object Object]"1 + obj2// 1 + 42// 43// 3. Датыconst date = new Date()date.valueOf() // 1467864738527date.toString() // "Sun Sep 15 2019..."// У дат приоритет методов обратный:// то есть вначале будет вызываться .toString(),// и только после него — .valueOf().1 + date// 1 + "Sun Sep 15 2019..."// "1" + "Sun Sep 15 2019..."// "1Sun Sep 15 2019..."
// 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
, потому что объекты всегда приводятся к true
.
const exists = {}if (exists) { /* эта ветка выполнится */}const doesntExist = undefinedif (doesntExist) { /* эта ветка не выполнится */}
const exists = {} if (exists) { /* эта ветка выполнится */ } const doesntExist = undefined if (doesntExist) { /* эта ветка не выполнится */ }
🛠 Если хочется описать сложную структуру, которая бы умела «вести себя», как число или строка, можно описать методы
или
.
const ticketPrice = { amount: 20, currency: "USD", valueOf: () => 20, toString: () => "$20",}1 + ticketPrice // 1 + 20 -> 21console.log(ticketPrice) // $20
const ticketPrice = { amount: 20, currency: "USD", valueOf: () => 20, toString: () => "$20", } 1 + ticketPrice // 1 + 20 -> 21 console.log(ticketPrice) // $20