На первый взгляд тема типов данных и преобразований может показаться легкой.
Обычно она изучается в самом начале погружения в JavaScript. Однако в этих темах есть неочевидные подводные камни, которые знает далеко не каждый разработчик.
В этой статье мы рассмотрим особенности типов данных и преобразований, которые многие пропустили.
typeof
JavaScript имеет 8 встроенных типов данных:
null
undefined
boolean
number
string
object
symbol
BigInt
Подробности о каждом типе данных вы можете причитать в любой документации.
Значения typeof
отличаются:
”undefined”
”boolean”
”number”
”string”
object”
”symbol”
”function”
"bigint"
Оператор typeof
возвращает строку, указывающую тип операнда.
Операнд – то, к чему применяется оператор. Например, в умножении
5 * 2
есть два операнда: левый операнд равен5
, а правый операнд равен2
.
Оператор typeof
напрямую не коррелирует со встроенными типами!
typeof null
Давайте рассмотрим пример:
const a = null;
console.log(!a && typeof a === "object");
В консоль будет выведено значение true
.
Такой результат будет из-за того, что JavaScript имеет старый баг.
typeof null
возвращает "object"
.
Этот баг существует уже много лет и вероятнее всего уже никогда не будет исправлен. Это связано с тем, что написано слишком много кода, который полагается на это ошибочное поведение.
typeof function
Посмотрим пример:
Что будет выведено в консоль?
const x = function() {}
console.log(x.length) // 0
const y = function(a, b, c) {}
console.log(y.length) // 3
У многих людей, которые не встречали такого вопроса ранее, может возникнуть недоумение. Учитывая, что typeof y
и typeof x
возвращает “function”
, кто-то может ожидать, что функция является одним из встроенных типов в JS. На самом деле, согласно спецификации, функция - это подтип объекта. Благодаря этому можно проверить количество аргументов у функции через .length
typeof NaN
Важно запомнить особенности NaN
:
NaN никогда не равен сам себе независимо от того используем мы
==
или===
.NaN === NaN // false NaN == NaN // false
typeof NaN
всегда возвращает“number”
. Это может показаться странным из-за того, что NaN - не число, которое является числом.NaN
все еще числовой тип несмотря на этот факт.window.isNaN
вернетtrue
только для фактических значенийNaN
, когда результат просто не число.window.isNaN(2 / "Dave") // true window.isNaN("Dave") // true
window.isNaN
преобразует аргумент вnumber
и возвращаетtrue
, если результат будет равенNaN
Number.isNaN
был добавлен в ES6.Number.isNaN
вернетtrue
только для тех значений, которые не являются числами, например, применимо к строке, будет возвращеноfalse
.Number.isNaN(2 / "Dave") // true Number.isNaN("Dave") // false
Number.isNaN
производит приведение типов, в то время какwindow.isNaN
не делает приведение.
Значение vs Ссылка
Вспомним простые значения в JS:
null
string
boolean
number
symbol
Комплексные значения:
Массивы
Объекты
Функции
Простые значения в JS имутабельные. Комплексные значения мутабельные.
Сначала повторим разницу между мутабельными и имутабельными данными.
// Пример имутабельности чисел:
let a = 1
let b = a
b++
console.log(a) // 1
console.log(b) // 2
// Пример мутабельности:
let x = [1, 2, 3]
let y = x
y.push(4)
console.log(x) // [1, 2, 3, 4]
console.log(y) // [1, 2, 3, 4]
x.push(5)
console.log(x) // [1, 2, 3, 4, 5]
console.log(y) // [1, 2, 3, 4, 5]
В примере мутабельности мы определили массив x
. Константе y
мы присвоили ссылку на х
. Когда мы модифицируем массив x
, мы также модифицируем и y
.
При работе с имутабельными данными такого эффекта не происходит. Это важно запомнить!
Мы рассмотрели пример с числами. Давайте взглянем на пример со строками:
// Имутабельные строки:
let a = "hello"
a[2] = "Z"
console.log(a) // "Hello"
В данном примере не произойдет изменение строки!
Если вы хотите изменить строку, то вам придется создать новую переменную.
С использованием метода .toUpperCase()
ситуация будет отличаться.
a.toUpperCase()
console.log(a) // "HELLO"
Метод .toUpperCase()
возвращает новую строку и присваивает переменной a
.
Взглянем, как ведут себя массивы со строками:
// Мутабельные массивы:
let b = ["h", "e", "l", "l", "o"]
b[2] = "Z"
console.log(b) // ["h", "e", "Z", "l", "o"]
Тут мы получили модифицированный массив b
.
После повторения разницы между мутабельными и имутабельными данными может возникнуть вопрос: “Откуда у примитивных данных есть полезные методы вроде .toUpperCase()
?”
Если движок JavaScript встречает запись подобную "hello".toUpperCase()
и у нас есть примитив, то мы вызываем у него метод. В таком случае вокруг примитива создается обертка в виде объекта, у которого как раз есть методы. После выполнения инструкции обертка удаляется и у нас снова остается примитивное значение.
Давайте рассмотрим легкий пример:
let a = [0, 1]
let b = a
b[0] = "a"
console.log(a) // ["a ", 1]
После повторения теории результат выполнения будет очевидным.
Но существует особенность в другом похожем примере:
let a = [0, 1]
let b = a
b = ["a", "b"]
console.log(a[0]) // 0
Переменной a
мы присвоили массив.
Переменной b
мы присвоили ссылку на переменную а
, а затем переменной b
присвоили новый массив.
В момент последнего присвоения старая ссылка была удалена!
Если мы создали новую ссылку в b
, то мы уже не можем, модифицируя b
, изменять и a
.
Еще раз:
let a = [0, 1]
let b = a // Создается ссылка
b = ["a", "b"] // Создается НОВАЯ ссылка на массив
Сравнение типов
Преобразование может быть явным, когда мы целенаправленно приводим один тип к другому, либо неявным, когда приведение типа происходит автоматически без явных команд.
String("123") // Явное преобразование
123 + "" // неявное преобразование
В JavaScript преобразование всегда приводит к 3м типам:
к строке
к числу
к логическому значению (
true
/false
)
Приведение к строке
String(null) // "null"
String(undefined) // "undefined"
String(true) // "true"
String(false) // "false"
String(1) // "1"
String(NaN) // "NaN"
String(10000000000 * 900000000000) // "9e+21"
String({}) // "[object Object]"
String({ name: "Ivan" }) // "[object Object]"
String([]) // ""
String([1, 2, 3]) // "1,2,3"
В данном примере преобразование происходит очевидным образом.
Приведение к числу
Number(null) // 0
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
Number(1) // 1
Number(NaN) // NaN
Number(10000000000 * 900000000000) // 9e+21
Number({}) // NaN
Number({ name: "Ivan" }) // NaN
Number([]) // 0
Number([1, 2, 3]) // NaN
Number("Ivan") // NaN
Number("0") // 0
Number("123") // 123
Тут есть исключения, которые нужно помнить:
Number(null)
приводится к0
Number(undefined)
приводится кNaN
Пустой массив
Number([])
приводится к0
Не пустой массив
Number([1, 2, 3])
приводится кNaN
Приведение к логическому типу
// Ложные значения
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
Boolean(-0) // false
Boolean(+0) // false
Boolean("") // false
// Истинные значения
Boolean(1) // true
Boolean(-1) // true
Boolean(10000000000 * 900000000000) // true
Boolean({}) // true
Boolean({ name: "Ivan" }) // true
Boolean([]) // true
Boolean([1, 2, 3]) // true
Boolean(() => {}) // true
Boolean("Ivan") // true
Boolean("0") // true
В этом примере стоит заострить внимание на объектах и массивах.
Пустая функция, объект или массив приведет к true
.
Приведение комплексных данных
Комплексные данные, такие как объекты и массивы, сначала будут преобразованы в их примитивные значения, а уже потом это значение будет преобразовано в число.
Разберем более подробно. Если у объекта доступен метод .valueOf
, который возвращает примитивное значение, то оно будет использоваться для приведения к числу, а если нет, то будет использоваться метод .toString()
.
Если ни одна операция не может предоставить примитивное значение, то выдается ошибка “Type Error”
let x = {}
x.valueOf = () => 22
console.log(Number(x)) // 22
let y = []
y.toString = () => '22'
console.log(Number(y)) // 22
let z = {}
z.valueOf() // {} (не примитив)
z.toString() // "[object Object]" (приводит объект к строке)
Number(z) // NaN
Давайте коснемся логических операторов прежде чем продолжить дальше:
Как вы думаете что будет выведено в консоль?
let obj = {
a: {
b: "c"
}
}
console.log(obj.a && obj.a.b)
Казалось бы простой вопрос, но не все ответят правильно.
В консоль будет выведено с
. Особенно для тех, кто пришел из других языков такой результат будет неочевиден.
Вспомним как работают логические операторы:
let a = 1
let b = "a"
let c = null
console.log(a && b) // "a"
console.log(a || b) // 1
console.log(b || c) // null
console.log(c || a) // 1
Если оба операнда истины, тогда будет возвращен последний операнд.
Строгое сравнение и сравнение с приведением типов
Обычно считается, что ===
использует “строгое” сравнение, и сравнивает типы, а ==
нет.
Если говорить более корректно, ==
позволяет делать приведение типов, тогда как ===
не разрешает.
Таблицы ниже показывают в результатах между ==
и ===
==
![](https://habrastorage.org/getpro/habr/upload_files/1cb/2ab/9ff/1cb2ab9ff774097c4c6cbdd0c4387545.png)
===
![](https://habrastorage.org/getpro/habr/upload_files/cd4/3e5/4f8/cd43e54f8b8863ab831c11211df630ca.png)
Неявное приведение между строкой и числом
Можно неявно привести строку к числу, используя оператор +
.
В JS оператор +
используется как для сложения чисел, так и для конкатенации строк.
Оператор +
выполняет операцию .toPrimitive
над значением левой и правой стороны.
Метод .toPrimitive
вызывает valueOf
у значения. Если одно из значений является строкой, то он их объединяет.
Также существует небольшая разница между неявным приведением числа к строке с помощью +
и явным с помощью String()
.
+
вызывает valueOf
, в то время как явный метод вызывает toString
Искусственный пример. Не берите его в свой код. Пример только для наглядности:
a = { valueof: () => 22, toString: () => 44 }
String(a) // 44
a + '" // 22
Алгоритмы сравнения
Нас интересует, что происходит, когда boolean
находится по обе стороны от ==
.
console.log("22" == true) // false
console.log("22" == 1) // false
console.log(22 == 1) // false
Вы могли ожидать, что "22" == true
вернет true
, т.к. “22”
является истинным значением, но фактически результат будет false
.
Это происходит из-за того, что значение true
приводится к числу. Результат выполнения будет 1
. Далее "22"
приводится к числу 22
. В конце идет сравнение 22 == 1
, где и возвращается false
.
Задачи на собеседованиях
Мы готовы рассмотреть интересные примеры, которые встречаются на собеседованиях.
console.log(false == "0") // true
// false приведен к 0
// "0" приведен к 0
// 0 === 0
console.log(false == 0) // true
// false приведен к 0
// 0 === 0
console.log(false == "") // true
// false приведен к 0
// "" приведен к 0
// 0 === 0
console.log(false == []) // true
// false приведен к 0
// [] это объект так что вызывается ToPrimitive
// valueOf() попробует получить примитивное значение
// [].valueOf() приведет к [], что не является примитивным значением
// При вызове [].toString() получим ""
// "" будет приведео к числу 0
// 0 === 0
console.log("" == 0) // true
// "" будет приведено к 0
// 0 === 0
console.log("" == []) // true
// [] это объект так что вызывается ToPrimitive
// valueOf() попробует получить примитивное значение
// [].valueOf() приведет к [], что не является примитивным значением
// При вызове [].toString() получим ""
// "" === ""
console.log(0 == []) // true
// [] это объект так что вызывается ToPrimitive
// valueOf() попробует получить примитивное значение
// [].valueOf() приведет к [], что не является примитивным значением
// При вызове [].toString() получим ""
// "" приведен к 0
// 0 === 0
Более сложные примеры
Вспомним терминологию.
Операнд - то к чему применяется оператор.
Бинарный оператор - оператор, который применяется к 2м операндам (1 + 3
)
Унарный оператор - оператор, который применяется к одному операнду (2++
)
console.log(true + false) // 1
// Бинарный оператор + вызывает численное преобразование для true и false
// 1 + 0 (вернет 1)
console.log(12 / "6") // 2
// Оператор деления вызывает численное преобразование
// 12 / 6 (вернет 2)
console.log("number" + 15 + 3) // "number153"
// Тут + выполняется слева направо
// "number" + 15 (вернет "number15")
// Поскольку один из операндов + это строка, то второе число будет преобразовано в строку
// "number15" + "3" (вернет "number153")
console.log(15 + 3 + "number") // "18number"
// 15 + 3 (вернет 18)
// 18 + "number" (вернет "18number")
console.log([1] > null) // true
// Оператор сравнения вызывает численное преобразование
// [1] будет преобразован в 1
// null будет преобразован в 0
// 1 > 0 (вернет true)
console.log("foo" + +"bar") // "fooNaN"
// Унарный оператор имеет более высокий приоритет, чем унарный оператор
// +"bar" выполнится первый
// Унарный плюс вызывает численное преобразование "bar" (вернет NaN)
// "foo" + NaN тут так же сработает конкатинация (вернет "fooNaN")
console.log("true" == true) // false
// Оператор сравнения вызывает численное преобразование
// Левый операнд "true" преобразуется в NaN
// Правый операнд true станет 1
// NaN === 1 (вернет false)
console.log("false" == false) // false
// Оператор сравнения вызывает численное преобразование
// Левый операнд "false" преобразуется в NaN
// Правый операнд true станет 0
// NaN === 0 (вернет false)
console.log(null == "") // false
// Оператор == обычно вызывает численное преобразование, но не в случае с null
// null == null и null == undefined возращает true, а все остальные случаи вернут false
console.log(!!"false" == !!"true") // true
// Оператор !! конвертирует строки "false" и "true" в булевые значения
// Получаем true == true, т.к. "false" не пустая строка (вернет true)
console.log(["x"] == "x") // true
// Оператор == вызывает численное преобразование у массива
// Метод массива valueOf возвращает сам массив. Этот результат игнориуется, т.к. не является примитивом
// Далее вызывается метод массива toString, который конвертирует ["x"] в "x"
// "x" == "x" (вернет true)
console.log([] + null + 1) // "null1"
// Оператор + вызывает численное преобразование массива
// Метод массива valueOf возвращает сам массив. Этот результат игнориуется, т.к. не является примитивом
// Далее вызывается метод массива toString, который конвертирует [] в ""
// "" + null (вернет "null")
// "null" + 1 (вернет "null1")
console.log([1, 2, 3] == [1, 2, 3]) // false
// В данном примере преобразование не происходит, т.к. оба массива одного типа
// Оператор == сравнивает объекты по ссылке, а не по значению
// Данные массивы являются двумя разными экземплярами
// Поэтому [1, 2, 3] == [1, 2, 3] вернет false
Пользуясь возможность возможностью хотелось бы рассказать о youtube канале Open JS на котором выкладываются обучающие ролики по JavaScript. Ни какой воды, рекламы и пустых рассуждений. Канал только начал свое развитие. Буду рад поддержке!
Спасибо за внимание!