Руководство по преобразованию значений в строки в JavaScript
JSON.stringify()
, практические примеры и лучшие практики.Преобразование значений в строки в JavaScript сложнее, чем кажется:
- В большинстве подходов есть значения, которые они не могут обработать.
- Мы не всегда видим все данные.
Преобразование значений в строки
Пример: проблемный код
Можете найти проблему в следующем коде?
class UnexpectedValueError extends Error {
constructor(value) {
super('Unexpected value: ' + value); // (A)
}
}
Для некоторых значений этот код выбрасывает исключение в строке A
:
> new UnexpectedValueError(Symbol())
TypeError: Cannot convert a Symbol value to a string
> new UnexpectedValueError({__proto__:null})
TypeError: Cannot convert object to primitive value
Читайте дальше, чтобы узнать больше.
Пять способов преобразования значений в строки
Существует пять распространённых способов преобразования значения v
в строку:
String(v)
'' + v
`${v}`
v.toString()
{}.toString.call(v)
Какие из них следует использовать? Давайте посмотрим, как они справятся со следующими сложными значениями:
undefined
null
Symbol()
{__proto__: null}
Результаты таковы:
undefined | null | Symbol() | {__proto__:null} | |
---|---|---|---|---|
String(v) | ✔ | ✔ | ✔ | TypeError |
'' + v | ✔ | ✔ | TypeError | TypeError |
`${v}` | ✔ | ✔ | TypeError | TypeError |
v.toString() | TypeError | TypeError | ✔ | TypeError |
{}.toString.call(v) | ✔ | ✔ | ✔ | ✔ |
Заключение:
- Из пяти перечисленных в таблице подходов только
{}.toString.call(v)
работает для всех сложных значений. - Если не требуется стопроцентная безопасность и хочется быть менее многословным, то
String(v)
также будет хорошим решением.
Что означает {}.toString.call(v)
Следующие два выражения эквивалентны:
{}.toString.call(v)
Object.prototype.toString.call(v)
Мы вызываем .toString()
, но не используем приёмник для поиска этого метода, а вызываем его напрямую (подробнее). Это поможет справиться с тремя случаями, потерпевшими неудачу не из-за того, как работает .toString()
, а потому что метод не найден, если использовать эти значения в качестве приёмников вызовов методов:
undefined
null
{__proto__: null}
Почему объект с null
-прототипом считается сложным
Очевидно, почему v.toString()
не работает, если не существует метода .toString()
, но первые три метода преобразования требуют, чтобы один из трёх методов существовал и возвращал примитивное значение.
> String({__proto__: null}) // метод недоступен
TypeError: Cannot convert object to primitive value
> String({__proto__: null, [Symbol.toPrimitive]() {return 'YES'}})
'YES'
> String({__proto__: null, toString() {return 'YES'}})
'YES'
> String({__proto__: null, valueOf() {return 'YES'}})
'YES'
Интересно, что методы, возвращающие undefined
или null
, подходят, а объект — нет:
> String({__proto__: null, toString() { return undefined }})
'undefined'
> String({__proto__: null, toString() { return null }})
'null'
> String({__proto__: null, toString() { return {} }})
TypeError: Cannot convert object to primitive value
Исправление проблемного примера
class UnexpectedValueError extends Error {
constructor(value) {
super('Unexpected value: ' + {}.toString.call(value));
}
}
Теперь код может обрабатывать все сложные значения:
> new UnexpectedValueError(undefined).message
'Unexpected value: [object Undefined]'
> new UnexpectedValueError(null).message
'Unexpected value: [object Null]'
> new UnexpectedValueError(Symbol()).message
'Unexpected value: [object Symbol]'
> new UnexpectedValueError({__proto__:null}).message
'Unexpected value: [object Object]'
Преобразование объектов в строки
Обычные объекты имеют стандартные строковые представления, которые не слишком полезны:
> String({a: 1})
'[object Object]'
У массивов лучшее строковое представление, но они всё равно скрывают много информации:
> String(['a', 'b'])
'a,b'
> String(['a', ['b']])
'a,b'
> String([1, 2])
'1,2'
> String(['1', '2'])
'1,2'
> String([true])
'true'
> String(['true'])
'true'
> String(true)
'true'
Преобразование функций в строку возвращает их исходный код:
> String(function f() {return 4})
'function f() {return 4}'
Кастомизация преобразования объектов в строку
Можно переопределить встроенный способ преобразования объектов в строку, реализовав метод toString()
:
const obj = {
toString() {
return 'hello';
}
};
assert.equal(String(obj), 'hello');
Использование JSON.stringify()
для преобразования значений в строки
Формат данных JSON представляет собой текстовое представление значений JavaScript. Поэтому JSON.stringify()
можно также использовать для преобразования значений в строки. Это решение хорошо подходит для объектов и массивов, где обычное преобразование в строку имеет существенные недостатки:
> JSON.stringify({a: 1})
'{"a":1}'
> JSON.stringify(['a', ['b']])
'["a",["b"]]'
JSON.stringify()
нормально работает с объектами, чьи прототипы равны null
:
> JSON.stringify({__proto__: null, a: 1})
'{"a":1}'
Основной недостаток заключается в том, что JSON.stringify()
поддерживает только следующие значения:
- Примитивные значения:
null
- Логические значения
- Числа (кроме
NaN
иInfinity
) - Строки
- Не примитивные значения:
- Массивы
- Объекты (кроме функций)
Для большинства других значений в результате получаем undefined
(а не строку):
> JSON.stringify(undefined)
undefined
> JSON.stringify(Symbol())
undefined
> JSON.stringify(() => {})
undefined
BigInt
вызывают исключения:
> JSON.stringify(123n)
TypeError: Do not know how to serialize a BigInt
Свойства со значениями undefined
-производных опускаются:
> JSON.stringify({a: Symbol(), b: 2})
'{"b":2}'
Элементы массива, значения которых выдают undefined
преобразуются в null
:
> JSON.stringify(['a', Symbol(), 'b'])
'["a",null,"b"]'
Многострочный вывод
По умолчанию JSON.stringify()
возвращает одну текстовую строку. Однако необязательный третий параметр позволяет вывести многострочный текст и указать, сколько отступов делать — например:
assert.equal(
JSON.stringify({first: 'Robin', last: 'Doe'}, null, 2),
`{
"first": "Robin",
"last": "Doe"
}`
);
Отображение строк через JSON.stringify()
Функция JSON.stringify()
удобна для отображения произвольных строк:
- Результат всегда вписывается в одну строку.
- Невидимые символы, такие как новые строки и табуляции, становятся видимыми.
Например:
const strWithNewlinesAndTabs = `
<-TAB
Second line
`;
console.log(JSON.stringify(strWithNewlinesAndTabs));
Выводит:
"\n\t<-TAB\nSecond line \n"
Вывод данных в консоль
Консольные методы, такие как console.log()
, обычно дают хороший результат и обладают небольшими ограничениями:
console.log({__proto__: null, prop: Symbol()});
Выводит:
[Object: null prototype] { prop: Symbol() }
Однако по умолчанию он отображает объекты только до определённой глубины:
console.log({a: {b: {c: {d: true}}}});
Выводит:
{ a: { b: { c: [Object] } } }
Node.js позволяет указывать глубину для console.dir()
— при этом null
означает бесконечность:
console.dir({a: {b: {c: {d: true}}}}, {depth: null});
Выводит:
{
a: { b: { c: { d: true } } }
}
В браузерах console.dir()
не имеет опции object
, но позволяет интерактивно и инкрементально углубляться в объекты.