Прогноз погоды, где ведущий показывает на облака с разными типами
Иллюстрация: Кира Кустова

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

Брюки превращаются в элегантные шорты, а во что превращается строка «55ab» при преобразовании в число?

Время чтения: 10 мин

Кратко

Скопировано

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

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

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

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

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

        
          
          const x = '4'Number(x)const y = 4String(y)
          const x = '4'
Number(x)

const y = 4
String(y)

        
        
          
        
      

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

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

        
          
          console.log(5 == '5')// trueconsole.log(5 === '5')// false
          console.log(5 == '5')
// true
console.log(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 = 5console.log(a == b)// true
          const a = 5
const b = 5
console.log(a == b)
// true

        
        
          
        
      

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

        
          
          const a = [1, 2, 3]const b = [1, 2, 3]console.log(a == b)// false
          const a = [1, 2, 3]
const b = [1, 2, 3]
console.log(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', хотя это тоже примитив:

        
          
          console.log(typeof null === 'object')// true
          console.log(typeof null === 'object')
// true

        
        
          
        
      

Функции

Скопировано

У функций в JavaScript тоже тип — object, хотя typeof возвращает 'function':

        
          
          function simpleFunction() {}console.log(typeof simpleFunction === 'function')// trueconst assignedFunction = function () {}console.log(typeof assignedFunction === 'function')// trueconst arrowFunction = () => {}console.log(typeof arrowFunction === 'function')// trueconsole.log(typeof function () {} === 'function')// true
          function simpleFunction() {}
console.log(typeof simpleFunction === 'function')
// true

const assignedFunction = function () {}
console.log(typeof assignedFunction === 'function')
// true

const arrowFunction = () => {}
console.log(typeof arrowFunction === 'function')
// true

console.log(typeof function () {} === 'function')
// true

        
        
          
        
      

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

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 пробует эти два типа «сопоставить» друг с другом, чтобы с ними работать.

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

  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 = () => 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 (object), потому что объекты всегда приводятся к true.

        
          
          const exists = {}if (exists) {  /* эта ветка выполнится */}const doesntExist = undefinedif (doesntExist) {  /* эта ветка не выполнится */}
          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 -> 21console.log(ticketPrice)// $20
          const ticketPrice = {
  amount: 20,
  currency: 'USD',
  valueOf: () => 20,
  toString: () => '$20',
}

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

        
        
          
        
      

На собеседовании

Скопировано
Задать вопрос в рубрику
🤚 Я знаю ответ

Андрей Детенков  отвечает

Скопировано

Операторы == и === в JavaScript — это операторы сравнения, которые мы используем, чтобы определить, равны или нет два значения.

Двойное равно == часто называют «нестрогим равенством», потому что этот оператор перед сравнением величин выполняет приведение типов:

        
          
          const foo = 100;const bar = '100';console.log(foo == bar) // true
          const foo = 100;
const bar = '100';
console.log(foo == bar) // true

        
        
          
        
      

Оператор === выполняет строгое сравнение, без приведения типов. При использовании тройного равно сравниваются и типы, и значения.

При сравнении переменных сначала проверяется, отличаются ли их типы.
Если типы совпадают, то проверяется значение. Если значения одинаковы, возвращается true.

        
          
          const foo = 100;const bar = '100';console.log(foo === bar); // falseconst foo = '100';const bar = '100';console.log(foo === bar); // true
          const foo = 100;
const bar = '100';
console.log(foo === bar); // false

const foo = '100';
const bar = '100';
console.log(foo === bar); // true

        
        
          
        
      

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

🤚 Я знаю ответ

Федя Петраков  отвечает

Скопировано

Значение переменной variable будет равно '115'. В переменной будет храниться строка, а не число.

        
          
          let variable = 1 + '15'console.log(variable) // '115'
          let variable = 1 + '15'
console.log(variable) // '115'

        
        
          
        
      

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