Вычислительно все представлено в виде бита. Бит - это 0 или 1. Например, символ обычно состоит из 8 бит. Буква R имеет десятичное значение 82 (через "R".charCodeAt(0)), которое имеет шестнадцатеричное значение 0x52 и двоичное значение 0b1010010. Хотя двоичный файл отображается как 7 бит, более полезно думать о нем как о 8 битах AKA 1 байт (или, конечно, в моем случае).

Обратите внимание, что предыдущий 0x означает шестнадцатеричный, а 0b означает двоичный (не по теме, но восьмеричный - это 0o).

Итак, мое (голубое) значение 0x0FF может быть представлено как 4 0 и два набора из 4 0: 0b000011111111. Давайте посмотрим на побитовое игнорирование значения.

Побитовое не

Операция not инвертирует двоичное значение (пример взят из MDN):

| a | NOT a |
|---|-------|
| 0 | 1     |
| 1 | 0     |

В JavaScript это представлено как ~ 0b1 (где 1 - двоичное значение, которое я хочу инвертировать).

Вот тогда все становится непросто. Запуск ~ 0b1 в JavaScript дает -2:

console.log( ~ 0b1 )
// -2

Что такое число?

В приведенном выше примере я ожидал увидеть в результате 0, но я вижу -2. Это почему?

Происходит несколько вещей, которые изначально вводят в заблуждение. Прежде всего, результат будет десятичным, а не двоичным. Во всех встречающихся мне REPL (консоль разработчика, jsconsole и т. Д.) Результаты отображаются в виде Number типа. В JavaScript числа являются десятичными.

Однако знать, что числа являются десятичными, недостаточно. Числа в JavaScript представляют собой формат с плавающей запятой двойной точности и занимают 64 бита (или 8 байтов).

Если я преобразую результат в двоичный, мы сможем лучше понять, что происходит:

const n = -2; 
const sign = n < 0 ? 1 : 0; 
console.log( (n >>> 0).toString(2).padStart(64, sign) );
// 1111111111111111111111111111111111111111111111111111111111111110

Краткая разбивка:

  1. n >>> 0 - это право с заполнением нулями, которое (если я правильно понял) отбрасывает знак целого числа, что приводит к большому десятичному значению (т.е. -1 в двоичном формате - это все 1 значений, включая крайний левый бит знака).
  2. Number.toString(2) устанавливает систему счисления с основанием 2 (двоичную) и возвращает строку
  3. padStart(64, sign) использует ES6 padStart (также известный как pad left) со знаковым битом (если наш номер был отрицательным, знак равен 1, в противном случае 0) и заполняет левую часть строки до тех пор, пока длина не станет 64 символа (или для нас: 64 бита)

Почему 1 не равно минус 2?

Теперь, когда мы лучше понимаем 64-битные числа, мы можем видеть, что инвертирование 0b1 фактически инвертирует первые 63 нулевых бита, а затем последний один бит. Поскольку знаковый бит также переворачивается, результат составляет 63 единичных бита и один нулевой бит: -2.

Что нам действительно нужно, так это своего рода зажим на наших битах. Здесь мы можем использовать типизированные массивы, чтобы помочь.

Типизированные массивы для байтов без знака

Создание new Uint8Array даст нам массив, содержащий байты без знака в каждом элементе массива. Без знака - это ключ к тому, чтобы операция not работала так, как мы ожидаем. Таким образом, 0b1 будет выглядеть как 00000001, а при правильном переворачивании будет 11111110.

Для простоты я создам новый типизированный массив с одним элементом, содержащим значение 0b1, полученное ранее. Теперь, с этим беззнаковым байтом, выполнение операции not дает тот же результат в консоли, но сохраненный результат действительно такой, как ожидалось:

const a = new Uint8Array([0b1]);
console.log(a[0].toString(2).padStart(8, 0));
// 00000001
console.log(a[0] = ~a[0]); 
// -2
console.log(a[0].toString(2).padStart(8, 0));
// 11111110

Перелистывание # 0FF

Мои первоначальные требования также были очень ошибочными. Я надеялся переключить красный цвет на голубой, предполагая, что #0FF переключится на #F00. Но я заметил свою ошибку. Шестнадцатеричное число - 0xFF (или ведущие нули, как и в десятичном, не требуются), если мы введем левую часть, значение будет 0x0000FFFF.

Это значение совсем не красное. Фактически, в большинстве браузеров (на момент написания) не поддерживаются 8-значные шестнадцатеричные значения (хотя, очевидно, Firefox поддерживает, как и Chrome Canary). Этот цвет (в 8-значном шестнадцатеричном формате) синий!

Следующее задание - поместить это значение в типизированный массив, но теперь Unit8Array слишком мало. Каждый элемент массива в Uint8Array может быть 8-битным, значение в шестнадцатеричном формате - 32 бита (включая ведущие нули). Я мог переключиться на использование Uint32Array и повторить процесс из более ранней версии, и это сработало бы, но это кажется ... неуклюжим. До такой степени, что я понимаю, что вся предпосылка была ошибочной.

Но теперь я узнал все о побитовом не, ответ на мою первоначальную проблему заключается в использовании побитового XOR.

XOR… или: что я должен был сделать

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

Для a XOR b XOR дает двоичный 1, если a и b разные, и двоичный 0, если они одинаковы. Таким образом генерируется инвертированный результат. Таким образом, побитовая операция XOR против 0xFFF всегда будет отражать каждый отдельный бит, и, что важно, он оставит бит знака в покое (т.е. мы не получим внезапно отрицательные значения).

console.log( (0x0FF ^ 0xFFF).toString(16).padStart(3, '0') )
// f00

Что ж, было весело. Я вылез из кроличьей норы и вернулся к работе. Надеюсь, вам тоже было весело!

Изначально опубликовано в B: log Реми Шарпа