Вычислительно все представлено в виде бита. Бит - это 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
Краткая разбивка:
n >>> 0
- это право с заполнением нулями, которое (если я правильно понял) отбрасывает знак целого числа, что приводит к большому десятичному значению (т.е. -1 в двоичном формате - это все1
значений, включая крайний левый бит знака).Number.toString(2)
устанавливает систему счисления с основанием 2 (двоичную) и возвращает строку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 Реми Шарпа