JavaScript может быть не первым языком, который приходит на ум, когда вы думаете о компьютерной инженерии, криптографии или вычислительной математике. Но в соответствии со своей природой языка программирования общего назначения, он вполне способен выполнять эти задачи, а также побитовые операции — задачи программирования, выполняемые на уровне отдельных битов.

Побитовые операции в основном полезны на системном уровне, где они описывают логику архитектуры компьютера и компилятора.

Конечно, это не совсем та область, где блистает JavaScript. Но возможность манипулировать данными на уровне битов имеет и другие интересные варианты использования, и JavaScript предлагает разработчикам эту функциональность.

В этой серии из двух частей я сделаю следующее:

  • Общий обзор побитовых операций (часть 1)
  • Объясните, как работают побитовые операции в JavaScript (часть 1)
  • Знакомство с побитовыми операторами в JavaScript (часть 1)
  • Пройдитесь по общему алгоритму, который манипулирует битами (часть 2)
  • Покажите другой (более практичный) пример побитовых операций в JavaScript (часть 2)

Что такое побитовые операции?

Побитовая операция в информатике — это просто операция, выполняемая над значением на уровне отдельных битов (двоичная цифра, т. е. 1 или 0) или двоичные строки.

В нашем мире мы используем десятичные числа. Десятичный (от латинского decem, что означает десять) — это еще одно слово для обозначения десятичной системы счисления. В десятичной системе счисления у нас есть 10 цифр для представления значений: 0, 1, 2, 3, 4, 5, 6, 7, 8 и 9. Эти цифры представляют разные значения в зависимости от их положения в числе. Каждая позиция основана на степени десяти. Первая позиция — 10⁰, следующая слева — 10¹, следующая — 10² и так далее. Например, возьмем десятичное число 248:

   8         4       2   
10^2=100  10^1=10  10^0=1 

8*1 + 4*10 + 2*100 = 248

В мире компьютеров используются двоичные значения или значения с основанием 2. Этот числовой формат использует ту же логику, что и основание 10, но только с двумя цифрами для представления всех значений: 0 и 1. Как и в случае с основанием 10, цифры представляют разные значения в зависимости от их положения. Первая позиция — 2⁰, следующая слева — 2¹, следующая — 2² и так далее. Число 248 в двоичном формате будет таким:

   1        1      1       1       1      0      0      0
2^7=128  2^6=64  2^5=32  2^4=16  2^3=8  2^2=4  2^1=2  2^0=1

8*1 + 16*1 + 32*1 + 64*1 + 128*1 = 248

Хотя компьютерные программы могут выполнять вычисления с десятичными (или даже шестнадцатеричными, или любыми другими кодировками чисел) числами, компьютеры могут интерпретировать только строки двоичного кода. Любое вычисление, инструкция или блок удобочитаемого кода преобразуются на самом низком уровне в двоичный электрический сигнал, где 1 и 0 обозначают «включено» или «выключено».

Таким образом, бинарные операции работают непосредственно с бинарными строками, типом данных, который имеет много преимуществ в зависимости от варианта использования по сравнению с другими типами данных. Во-первых, вы работаете намного быстрее и эффективнее, когда избавляетесь от необходимости конвертировать двоичные данные в другой тип данных. Еще одним преимуществом бинарных операций является возможность кодировать/декодировать данные в более компактной форме. Наконец, в приложениях системного программирования и вычислительной техники побитовые операции могут использоваться для непосредственного взаимодействия с кодом машинного уровня или даже компьютерным оборудованием.

Итак… действительно ли это важно для JavaScript? Нет, не совсем. Но, как я уже сказал, это возможно! Кроме того, я думаю, что разработчикам JavaScript важно знать и узнавать о широте возможностей, которые им предоставляет выбранный ими инструмент.

Побитовые операции в JavaScript

Прежде всего, когда речь заходит о побитовых операциях в JavaScript, важно отметить, что JavaScript выполняет числовые операции с использованием числового типа, который является двойной точностью 64-битным значение формата с плавающей запятой, также известное как binary64.

Что именно это значит?

Binary64 — это значение IEEE (Институт инженеров по электротехнике и электронике) 754. Этот стандарт IEEE определяет значение как занимающее 64 бита в памяти компьютера.

Лучший способ кратко объяснить термин двойной точности — сравнить двоичное число 64 с числом одинарной точности (или двоичным числом 32). Значение с одинарной точностью отводит 23 из 32 бит для мантиссы, которая представляет собой часть числа, представляющую фактическое значение, т. е. не знак или показатель степени. Между тем, двойная точность выделяет 52 бита из 64 под мантиссу. Проще говоря, это более точный числовой формат, представляющий широкий диапазон значений.

Важно отметить кодировку типа числа JavaScript, поскольку побитовые операции выполняются над 32-битными двоичными числами. Таким образом, перед выполнением побитовой операции JavaScript изначально преобразует операнды в 32-битные целые числа. После операции он преобразует число обратно в двоичный64. Если операнд имеет длину более 32 бит, его «самые значащие» биты (крайние левые биты) будут обрезаны и отброшены.

Операторы

Это, конечно, подводит нас к тому, как побитовые операции обозначаются в JavaScript (так же, как и в большинстве языков). Вот операторы:

Давайте рассмотрим их один за другим, используя определения из веб-документов MDN.

& И

Этот оператор «возвращает число или BigInt, чье двоичное представление имеет 1 в каждой битовой позиции, для которой соответствующие биты обоих операндов равны 1».

const a = 3; // 00000000000000000000000000000011
const b = 7; // 00000000000000000000000000000111

console.log(a & b);
// Expected: 3, or 00000000000000000000000000000011

| OR

Этот оператор «возвращает число или BigInt, чье двоичное представление имеет 1 в каждой битовой позиции, для которой соответствующие биты одного или обоих операндов равны 1».

const a = 3; // 00000000000000000000000000000011
const b = 7; // 00000000000000000000000000000111

console.log(a | b);
// Expected: 7, or 00000000000000000000000000000111

^ Исключающее ИЛИ

XOR означает «исключающее или». Этот оператор «возвращает число или BigInt, чье двоичное представление имеет 1 в каждой битовой позиции, для которой соответствующие биты одного, но не обоих операндов равны 1».

const a = 3; // 00000000000000000000000000000011
const b = 7; // 00000000000000000000000000000111

console.log(a^b);
// Expected: 4, or 00000000000000000000000000000100

~ НЕ

Этот оператор «возвращает число или BigInt, чье двоичное представление имеет 1 в каждой битовой позиции, для которой соответствующий бит операнда равен 0, и 0 в противном случае».

const a = 3; // 00000000000000000000000000000011
const b = -7; // 11111111111111111111111111111001

console.log(~a);
// Expected: -4, or 11111111111111111111111111111100

console.log(~b);
// Expected: 6, or 00000000000000000000000000000110

Этот оператор «возвращает число или BigInt, двоичное представление которого является первым операндом, сдвинутым на указанное количество битов влево. Лишние биты, смещенные влево, отбрасываются, а нулевые биты сдвигаются справа».

const a = 7; // 00000000000000000000000000000111
const b = 3; // 00000000000000000000000000000011

console.log(a << b);
// Expected: 56, or 00000000000000000000000000111000

›› Подписанный правый сдвиг

Этот операнд «возвращает число или BigInt, двоичное представление которого является первым операндом, сдвинутым на указанное число битов вправо. Лишние биты, сдвинутые вправо, отбрасываются, а копии самого левого бита сдвигаются слева. Эта операция также называется «сдвиг вправо с распространением знака» или «арифметический сдвиг вправо», потому что знак полученного числа совпадает со знаком первого операнда».

const a = 7; // 00000000000000000000000000000111
const b = 2; // 00000000000000000000000000000011
const c = -7; // 11111111111111111111111111111001

console.log(a >> b);
// Expected: 1, or 00000000000000000000000000000001

console.log(c >> b);
// Expected: -2, or 11111111111111111111111111111110

››› Нулевое заполнение правым сдвигом

Этот операнд возвращает число, двоичное представление которого является первым операндом, сдвинутым на указанное число битов вправо. Лишние биты, сдвинутые вправо, отбрасываются, а нулевые биты сдвигаются слева. Эта операция также называется «заполнение нулями вправо, потому что бит знака становится 0, поэтому результирующее число всегда положительное. Беззнаковый сдвиг вправо не принимает значения BigInt.

const a = 7; // 00000000000000000000000000000111
const b = 2; // 00000000000000000000000000000011
const c = -7; // 11111111111111111111111111111001

console.log(a >>> b);
// Expected: 1, or 00000000000000000000000000000001
console.log(c >>> b);
// Expected: 1073741822, or 00111111111111111111111111111110

ЗАМЕЧАНИЕ ПО ПРИСВОЕНИЮ: Каждый из этих операторов, как и многие операторы в JavaScript, имеет сокращенную версию var1 = var1 [оператор] var2, где результат операции присваивается левому операнду. Например:

let a = 7; // 00000000000000000000000000000111

a &= 2;
console.log(a);
// Expected: 2, or 00000000000000000000000000000010

Спасибо за прочтение. Во второй части этой серии я покажу вам несколько интересных приложений побитовых операций.