На первый взгляд тема типов данных и преобразований может показаться легкой.
Обычно она изучается в самом начале погружения в JavaScript. Однако в этих темах есть неочевидные подводные камни, которые знает далеко не каждый разработчик.
В этой статье мы рассмотрим особенности типов данных и преобразований, которые многие пропустили.
typeof
JavaScript имеет 8 встроенных типов данных:
-
null -
undefined -
boolean -
number -
string -
object -
symbol -
BigInt
Подробности о каждом типе данных вы можете причитать в любой документации.
При этом в JavaScript 7 значений typeof и их определения неоднозначны:
-
”undefined” -
”boolean” -
”number” -
”string” -
object” -
”symbol” -
”function”
Оператор 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") // truewindow.isNaNпреобразует аргумент вnumberи возвращаетtrue, если результат будет равенNaN -
Number.isNaNбыл добавлен в ES6.Number.isNaNвернетtrueтолько для тех значений, которые не являются числами, например, применимо к строке, будет возвращеноfalse.Number.isNaN(2 / "Dave") // true Number.isNaN("Dave") // falseNumber.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
Если оба операнда истины, тогда будет возвращен последний операнд.
Строгое сравнение и сравнение с приведением типов
Обычно считается, что === использует “строгое” сравнение, и сравнивает типы, а == нет.
Если говорить более корректно, == позволяет делать приведение типов, тогда как === не разрешает.
Таблицы ниже показывают в результатах между == и ===
==

===

Неявное приведение между строкой и числом
Можно неявно привести строку к числу, используя оператор +.
В 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. Ни какой воды, рекламы и пустых рассуждений. Канал только начал свое развитие. Буду рад поддержке!
Спасибо за внимание!
ссылка на оригинал статьи https://habr.com/ru/post/709048/
Добавить комментарий