Руководство по преобразованию значений в строки в JavaScript

Руководство по преобразованию значений в строки в JavaScript: сравнение 5 методов, работа с объектами и 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 в строку:

Какие из них следует использовать? Давайте посмотрим, как они справятся со следующими сложными значениями:

Результаты таковы:

undefinednullSymbol(){__proto__:null}
String(v)TypeError
'' + vTypeErrorTypeError
`${v}`TypeErrorTypeError
v.toString()TypeErrorTypeErrorTypeError
{}.toString.call(v)

Заключение:

Что означает {}.toString.call(v)

Следующие два выражения эквивалентны:

{}.toString.call(v)
Object.prototype.toString.call(v)

Мы вызываем .toString(), но не используем приёмник для поиска этого метода, а вызываем его напрямую (подробнее). Это поможет справиться с тремя случаями, потерпевшими неудачу не из-за того, как работает .toString(), а потому что метод не найден, если использовать эти значения в качестве приёмников вызовов методов:

Почему объект с 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() поддерживает только следующие значения:

Для большинства других значений в результате получаем 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, но позволяет интерактивно и инкрементально углубляться в объекты.

Комментарии


Дополнительные материалы

Предыдущая Статья

Когда определяется this в JavaScript

Следующая Статья

React: Какой useEffect запускается первым?