← JavaScript/TextDecoder и TextEncoder#158 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

TextDecoder и TextEncoder

Ты интегрируешь устаревший корпоративный API, который отдаёт данные в кодировке Windows-1251 (не UTF-8). Fetch возвращает ArrayBuffer, а не строку. Без TextDecoder прочитать кириллицу невозможно — получишь кракозябры. В другом проекте нужно отправить JSON через WebSocket в бинарном режиме — TextEncoder превращает строку в байты.

Какую проблему решает

Сеть и файлы работают с байтами. Строки в JavaScript — Unicode. TextEncoder/TextDecoder — мост между ними. Это единственный стандартный способ конвертировать строки в бинарные данные и обратно с поддержкой разных кодировок.

На основе предыдущих уроков

  • ArrayBuffer: TextEncoder возвращает Uint8Array, TextDecoder принимает ArrayBuffer/TypedArray
  • Fetch: response.arrayBuffer() — когда сервер не UTF-8, нужен TextDecoder
  • Строки: строки в JS — UTF-16, но TextEncoder всегда кодирует в UTF-8
  • TextEncoder — строка в байты

    TextEncoder всегда кодирует в UTF-8:

    const encoder = new TextEncoder()
    
    const bytes = encoder.encode('Привет!')
    console.log(bytes instanceof Uint8Array)  // true
    console.log(bytes.byteLength)            // 13 (кириллица = 2 байта/символ)
    
    // encodeInto() — пишет в существующий буфер (без аллокации)
    const buf = new Uint8Array(100)
    const { read, written } = encoder.encodeInto('Hello', buf)
    console.log(written)  // 5

    Почему кириллица занимает 2 байта

    UTF-8 — переменная ширина. ASCII (U+0000–U+007F) — 1 байт. Кириллица (U+0400–U+04FF) — 2 байта. Китайские символы — 3 байта. Эмодзи — 4 байта:

    const enc = new TextEncoder()
    const sizes = [
      ['A', 1],   // ASCII
      ['Я', 2],   // Кириллица
      ['€', 3],   // Европейский символ
      ['😀', 4],  // Эмодзи
    ]
    
    for (const [char] of sizes) {
      console.log(char, '->', enc.encode(char).byteLength, 'байт')
    }

    TextDecoder — байты в строку

    Поддерживает множество кодировок:

    const utf8   = new TextDecoder()                // utf-8 по умолчанию
    const win    = new TextDecoder('windows-1251')  // кириллица Windows
    const latin  = new TextDecoder('iso-8859-1')    // Western European
    
    // Декодируем байты
    const bytes = Uint8Array.from([72, 101, 108, 108, 111])
    console.log(utf8.decode(bytes))  // 'Hello'

    stream: true — потоковая декодировка

    При получении данных чанками (streaming) многобайтовый символ может прийти разорванным. Опция stream: true сохраняет состояние между вызовами:

    const decoder = new TextDecoder('utf-8')
    
    // Буква 'П' в UTF-8 = [0xD0, 0x9F]
    // Чанк 1 содержит только первый байт 'П'
    const chunk1 = new Uint8Array([0xD0])
    const chunk2 = new Uint8Array([0x9F, 0x20, 0x41])  // '...' + ' A'
    
    const part1 = decoder.decode(chunk1, { stream: true })  // ''  — ждёт следующий байт
    const part2 = decoder.decode(chunk2)                     // 'П A'
    console.log(part1 + part2)  // 'П A'

    Типичные ошибки

    Ошибка 1: Не учтено, что .length строки !== байты

    const str = 'Привет'
    console.log(str.length)  // 6 символов
    
    const bytes = new TextEncoder().encode(str)
    console.log(bytes.byteLength)  // 12 байт (кириллица = 2 байта!)
    
    // В HTTP Content-Length нужно указывать БАЙТЫ, не символы

    Ошибка 2: Отсутствие stream:true при потоковом чтении

    // НЕВЕРНО — разрывает многобайтовые символы
    const decoder = new TextDecoder()
    chunks.forEach(chunk => {
      result += decoder.decode(chunk)  // символ может расколоться!
    })
    
    // ВЕРНО
    const decoder = new TextDecoder()
    chunks.forEach((chunk, i) => {
      const isLast = i === chunks.length - 1
      result += decoder.decode(chunk, { stream: !isLast })
    })

    Ошибка 3: Неправильная кодировка для legacy API

    // Если сервер отдаёт windows-1251, а ты декодируешь как utf-8:
    const decoder = new TextDecoder('utf-8')  // Кракозябры!
    
    // Нужно явно указать кодировку:
    const decoder2 = new TextDecoder('windows-1251')  // Правильно

    В реальных проектах

  • Fetch binaryMode: response.arrayBuffer() + TextDecoder для legacy API с windows-1251
  • WebSocket binary: отправка JSON/текста в бинарном режиме через TextEncoder
  • FileReader: TextDecoder как замена FileReader для чтения текстовых файлов
  • Web Crypto: TextEncoder конвертирует пароль в байты для crypto.subtle.importKey
  • gRPC-web: декодирование protobuf-ответов в строки
  • Примеры

    WebSocket бинарный протокол: кодирование JSON сообщений и декодирование ответов с поддержкой кодировок

    // ===== Симуляция бинарного WebSocket протокола =====
    // Формат сообщения: [type: u8][bodyLen: u32 LE][body: UTF-8 bytes]
    
    const MSG_TYPES = { JSON: 0x01, TEXT: 0x02, BINARY: 0x03 }
    
    const encoder = new TextEncoder()
    const decoder = new TextDecoder('utf-8')
    
    function encodeMessage(type, data) {
      // Сериализуем данные в UTF-8 байты
      const bodyStr  = typeof data === 'string' ? data : JSON.stringify(data)
      const bodyBytes = encoder.encode(bodyStr)
    
      // Заголовок: 1 байт тип + 4 байта длина = 5 байт
      const header = new ArrayBuffer(5)
      const view   = new DataView(header)
      view.setUint8(0, type)
      view.setUint32(1, bodyBytes.byteLength, true)
    
      // Собираем итоговый буфер
      const packet = new Uint8Array(5 + bodyBytes.byteLength)
      packet.set(new Uint8Array(header), 0)
      packet.set(bodyBytes, 5)
    
      return packet
    }
    
    function decodeMessage(packet) {
      const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength)
      const type    = view.getUint8(0)
      const bodyLen = view.getUint32(1, true)
      const body    = packet.slice(5, 5 + bodyLen)
      const bodyStr = decoder.decode(body)
    
      return {
        type,
        bodyLen,
        data: type === MSG_TYPES.JSON ? JSON.parse(bodyStr) : bodyStr,
      }
    }
    
    // Тест JSON-сообщения
    console.log('=== Бинарный WebSocket протокол ===')
    const userData = { id: 42, name: 'Иван', action: 'login' }
    const packet = encodeMessage(MSG_TYPES.JSON, userData)
    
    console.log('Размер пакета:', packet.byteLength, 'байт')
    console.log('Заголовок (hex):', Array.from(packet.slice(0, 5))
      .map(b => b.toString(16).padStart(2, '0')).join(' '))
    
    const decoded = decodeMessage(packet)
    console.log('Тип:', decoded.type.toString(16))   // 1
    console.log('bodyLen:', decoded.bodyLen)
    console.log('data.name:', decoded.data.name)     // Иван
    console.log('data.action:', decoded.data.action) // login
    
    // ===== UTF-8 размеры символов =====
    console.log('\n=== Размер символов в UTF-8 ===')
    const chars = ['A', 'Я', '€', '😀', '中', '한']
    for (const ch of chars) {
      const bytes = encoder.encode(ch)
      const hex   = Array.from(bytes).map(b => '0x' + b.toString(16)).join(', ')
      console.log(`'${ch}' → ${bytes.byteLength} байт [${hex}]`)
    }
    
    // ===== Измерение байтового размера строк =====
    console.log('\n=== Размер строк в байтах ===')
    function getByteSize(str) {
      return encoder.encode(str).byteLength
    }
    
    const strings = [
      'Hello',
      'Привет',
      '你好',
      'Hello, Мир! 🌍',
    ]
    for (const s of strings) {
      const chars = s.length
      const bytes = getByteSize(s)
      console.log(`"${s}": ${chars} символов, ${bytes} байт, ${(bytes/chars).toFixed(1)} байт/символ`)
    }
    
    // ===== Потоковое декодирование =====
    console.log('\n=== stream: true — потоковое декодирование ===')
    
    // Симулируем получение текста чанками
    const fullText = 'Привет, мир! Hello World!'
    const allBytes = encoder.encode(fullText)
    
    // Разбиваем на чанки по 7 байт (режем посередине кириллических символов)
    const chunkSize = 7
    const chunks = []
    for (let i = 0; i < allBytes.length; i += chunkSize) {
      chunks.push(allBytes.slice(i, i + chunkSize))
    }
    
    console.log('Исходный текст:', fullText)
    console.log('Чанков:', chunks.length, '(по ~7 байт)')
    
    // Без stream: true — ошибки декодирования
    const badDecoder = new TextDecoder()
    let badResult = ''
    for (const chunk of chunks) {
      badResult += badDecoder.decode(chunk)  // символы могут испортиться
    }
    console.log('Без stream:true:', badResult.length === fullText.length ? 'OK' : 'Есть ошибки!')
    
    // Со stream: true — корректно
    const goodDecoder = new TextDecoder()
    let goodResult = ''
    chunks.forEach((chunk, i) => {
      goodResult += goodDecoder.decode(chunk, { stream: i < chunks.length - 1 })
    })
    console.log('С stream:true:', goodResult)
    console.log('Совпадает?', goodResult === fullText)  // true

    TextDecoder и TextEncoder

    Ты интегрируешь устаревший корпоративный API, который отдаёт данные в кодировке Windows-1251 (не UTF-8). Fetch возвращает ArrayBuffer, а не строку. Без TextDecoder прочитать кириллицу невозможно — получишь кракозябры. В другом проекте нужно отправить JSON через WebSocket в бинарном режиме — TextEncoder превращает строку в байты.

    Какую проблему решает

    Сеть и файлы работают с байтами. Строки в JavaScript — Unicode. TextEncoder/TextDecoder — мост между ними. Это единственный стандартный способ конвертировать строки в бинарные данные и обратно с поддержкой разных кодировок.

    На основе предыдущих уроков

  • ArrayBuffer: TextEncoder возвращает Uint8Array, TextDecoder принимает ArrayBuffer/TypedArray
  • Fetch: response.arrayBuffer() — когда сервер не UTF-8, нужен TextDecoder
  • Строки: строки в JS — UTF-16, но TextEncoder всегда кодирует в UTF-8
  • TextEncoder — строка в байты

    TextEncoder всегда кодирует в UTF-8:

    const encoder = new TextEncoder()
    
    const bytes = encoder.encode('Привет!')
    console.log(bytes instanceof Uint8Array)  // true
    console.log(bytes.byteLength)            // 13 (кириллица = 2 байта/символ)
    
    // encodeInto() — пишет в существующий буфер (без аллокации)
    const buf = new Uint8Array(100)
    const { read, written } = encoder.encodeInto('Hello', buf)
    console.log(written)  // 5

    Почему кириллица занимает 2 байта

    UTF-8 — переменная ширина. ASCII (U+0000–U+007F) — 1 байт. Кириллица (U+0400–U+04FF) — 2 байта. Китайские символы — 3 байта. Эмодзи — 4 байта:

    const enc = new TextEncoder()
    const sizes = [
      ['A', 1],   // ASCII
      ['Я', 2],   // Кириллица
      ['€', 3],   // Европейский символ
      ['😀', 4],  // Эмодзи
    ]
    
    for (const [char] of sizes) {
      console.log(char, '->', enc.encode(char).byteLength, 'байт')
    }

    TextDecoder — байты в строку

    Поддерживает множество кодировок:

    const utf8   = new TextDecoder()                // utf-8 по умолчанию
    const win    = new TextDecoder('windows-1251')  // кириллица Windows
    const latin  = new TextDecoder('iso-8859-1')    // Western European
    
    // Декодируем байты
    const bytes = Uint8Array.from([72, 101, 108, 108, 111])
    console.log(utf8.decode(bytes))  // 'Hello'

    stream: true — потоковая декодировка

    При получении данных чанками (streaming) многобайтовый символ может прийти разорванным. Опция stream: true сохраняет состояние между вызовами:

    const decoder = new TextDecoder('utf-8')
    
    // Буква 'П' в UTF-8 = [0xD0, 0x9F]
    // Чанк 1 содержит только первый байт 'П'
    const chunk1 = new Uint8Array([0xD0])
    const chunk2 = new Uint8Array([0x9F, 0x20, 0x41])  // '...' + ' A'
    
    const part1 = decoder.decode(chunk1, { stream: true })  // ''  — ждёт следующий байт
    const part2 = decoder.decode(chunk2)                     // 'П A'
    console.log(part1 + part2)  // 'П A'

    Типичные ошибки

    Ошибка 1: Не учтено, что .length строки !== байты

    const str = 'Привет'
    console.log(str.length)  // 6 символов
    
    const bytes = new TextEncoder().encode(str)
    console.log(bytes.byteLength)  // 12 байт (кириллица = 2 байта!)
    
    // В HTTP Content-Length нужно указывать БАЙТЫ, не символы

    Ошибка 2: Отсутствие stream:true при потоковом чтении

    // НЕВЕРНО — разрывает многобайтовые символы
    const decoder = new TextDecoder()
    chunks.forEach(chunk => {
      result += decoder.decode(chunk)  // символ может расколоться!
    })
    
    // ВЕРНО
    const decoder = new TextDecoder()
    chunks.forEach((chunk, i) => {
      const isLast = i === chunks.length - 1
      result += decoder.decode(chunk, { stream: !isLast })
    })

    Ошибка 3: Неправильная кодировка для legacy API

    // Если сервер отдаёт windows-1251, а ты декодируешь как utf-8:
    const decoder = new TextDecoder('utf-8')  // Кракозябры!
    
    // Нужно явно указать кодировку:
    const decoder2 = new TextDecoder('windows-1251')  // Правильно

    В реальных проектах

  • Fetch binaryMode: response.arrayBuffer() + TextDecoder для legacy API с windows-1251
  • WebSocket binary: отправка JSON/текста в бинарном режиме через TextEncoder
  • FileReader: TextDecoder как замена FileReader для чтения текстовых файлов
  • Web Crypto: TextEncoder конвертирует пароль в байты для crypto.subtle.importKey
  • gRPC-web: декодирование protobuf-ответов в строки
  • Примеры

    WebSocket бинарный протокол: кодирование JSON сообщений и декодирование ответов с поддержкой кодировок

    // ===== Симуляция бинарного WebSocket протокола =====
    // Формат сообщения: [type: u8][bodyLen: u32 LE][body: UTF-8 bytes]
    
    const MSG_TYPES = { JSON: 0x01, TEXT: 0x02, BINARY: 0x03 }
    
    const encoder = new TextEncoder()
    const decoder = new TextDecoder('utf-8')
    
    function encodeMessage(type, data) {
      // Сериализуем данные в UTF-8 байты
      const bodyStr  = typeof data === 'string' ? data : JSON.stringify(data)
      const bodyBytes = encoder.encode(bodyStr)
    
      // Заголовок: 1 байт тип + 4 байта длина = 5 байт
      const header = new ArrayBuffer(5)
      const view   = new DataView(header)
      view.setUint8(0, type)
      view.setUint32(1, bodyBytes.byteLength, true)
    
      // Собираем итоговый буфер
      const packet = new Uint8Array(5 + bodyBytes.byteLength)
      packet.set(new Uint8Array(header), 0)
      packet.set(bodyBytes, 5)
    
      return packet
    }
    
    function decodeMessage(packet) {
      const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength)
      const type    = view.getUint8(0)
      const bodyLen = view.getUint32(1, true)
      const body    = packet.slice(5, 5 + bodyLen)
      const bodyStr = decoder.decode(body)
    
      return {
        type,
        bodyLen,
        data: type === MSG_TYPES.JSON ? JSON.parse(bodyStr) : bodyStr,
      }
    }
    
    // Тест JSON-сообщения
    console.log('=== Бинарный WebSocket протокол ===')
    const userData = { id: 42, name: 'Иван', action: 'login' }
    const packet = encodeMessage(MSG_TYPES.JSON, userData)
    
    console.log('Размер пакета:', packet.byteLength, 'байт')
    console.log('Заголовок (hex):', Array.from(packet.slice(0, 5))
      .map(b => b.toString(16).padStart(2, '0')).join(' '))
    
    const decoded = decodeMessage(packet)
    console.log('Тип:', decoded.type.toString(16))   // 1
    console.log('bodyLen:', decoded.bodyLen)
    console.log('data.name:', decoded.data.name)     // Иван
    console.log('data.action:', decoded.data.action) // login
    
    // ===== UTF-8 размеры символов =====
    console.log('\n=== Размер символов в UTF-8 ===')
    const chars = ['A', 'Я', '€', '😀', '中', '한']
    for (const ch of chars) {
      const bytes = encoder.encode(ch)
      const hex   = Array.from(bytes).map(b => '0x' + b.toString(16)).join(', ')
      console.log(`'${ch}' → ${bytes.byteLength} байт [${hex}]`)
    }
    
    // ===== Измерение байтового размера строк =====
    console.log('\n=== Размер строк в байтах ===')
    function getByteSize(str) {
      return encoder.encode(str).byteLength
    }
    
    const strings = [
      'Hello',
      'Привет',
      '你好',
      'Hello, Мир! 🌍',
    ]
    for (const s of strings) {
      const chars = s.length
      const bytes = getByteSize(s)
      console.log(`"${s}": ${chars} символов, ${bytes} байт, ${(bytes/chars).toFixed(1)} байт/символ`)
    }
    
    // ===== Потоковое декодирование =====
    console.log('\n=== stream: true — потоковое декодирование ===')
    
    // Симулируем получение текста чанками
    const fullText = 'Привет, мир! Hello World!'
    const allBytes = encoder.encode(fullText)
    
    // Разбиваем на чанки по 7 байт (режем посередине кириллических символов)
    const chunkSize = 7
    const chunks = []
    for (let i = 0; i < allBytes.length; i += chunkSize) {
      chunks.push(allBytes.slice(i, i + chunkSize))
    }
    
    console.log('Исходный текст:', fullText)
    console.log('Чанков:', chunks.length, '(по ~7 байт)')
    
    // Без stream: true — ошибки декодирования
    const badDecoder = new TextDecoder()
    let badResult = ''
    for (const chunk of chunks) {
      badResult += badDecoder.decode(chunk)  // символы могут испортиться
    }
    console.log('Без stream:true:', badResult.length === fullText.length ? 'OK' : 'Есть ошибки!')
    
    // Со stream: true — корректно
    const goodDecoder = new TextDecoder()
    let goodResult = ''
    chunks.forEach((chunk, i) => {
      goodResult += goodDecoder.decode(chunk, { stream: i < chunks.length - 1 })
    })
    console.log('С stream:true:', goodResult)
    console.log('Совпадает?', goodResult === fullText)  // true

    Задание

    Ты получаешь данные от legacy API в формате CSV, закодированном в UTF-8 байтах (ArrayBuffer). Нужно декодировать данные и обработать их. Реализуй: - `encodeTo(data)` — принимает JS-объект, сериализует в JSON, возвращает Uint8Array - `decodeFrom(bytes)` — принимает Uint8Array, декодирует UTF-8, парсит JSON - `getByteSize(str)` — возвращает размер строки в байтах при кодировке UTF-8 - `parseCSVBytes(bytes)` — принимает Uint8Array с CSV-данными (первая строка — заголовки), возвращает массив объектов

    Подсказка

    encodeTo: JSON.stringify + new TextEncoder().encode(). decodeFrom: new TextDecoder().decode() + JSON.parse(). getByteSize: encoder.encode(str).byteLength. parseCSVBytes: decoder.decode(bytes) затем split и map

    Загружаем среду выполнения...
    Загружаем AI-помощника...