Руководство по преобразованию значений в строки в 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)
Какие из них следует использовать? Давайте посмотрим, как они справятся со следующими сложными значениями:
undefinednullSymbol(){__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(), а потому что метод не найден, если использовать эти значения в качестве приёмников вызовов методов:
undefinednull{__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(() => {})
undefinedBigInt вызывают исключения:
> 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, но позволяет интерактивно и инкрементально углубляться в объекты.