Битовые манипуляции в JavaScript
Вся информация на компьютерах хранится в двоичном формате. Буквы, цифры, массивы, хэшмапы, все.
Цифры в свою очередь эффективней хранить в двоичной системе счисления. Любое число в десятичной системе можно перевести в двоичную.
По сути все числа хранятся в таком формате:
Мы можем легко посмотреть репрезентацию десятичного числа в двоичной системе с помощью toString:
10.tostring(2); // 1010
4.toString(2); // 100
2.toString(2); // 10
1.toString(2); // 1
Парсить данные числа в свою очередь удобно через parseInt:
parseInt('1010', 2); // 10
parseInt('100', 2); // 4
parseInt('10', 2); // 2
parseInt('1', 2); // 1
Мы также можем записывать двоичные числа с помощью 0b:
console.log(0b1010); // 10
console.log(0b100); // 4
console.log(0b10); // 2
console.log(0b1); // 1
Логические Операторы
Для битовых чисел есть специальные логические операторы. Цель их существования - манипуляция с битовыми значениями.
«Битовый И»: &
Оператор & сравнивает каждый разряд двоичного числа и отдает 1 только в том случае, если разряды в двух числах являются единицей:
console.log(0b1001 & 0b0110) // 0000 => 0
console.log(0b1000 & 0b1100) // 1000 => 8
console.log(0b1111 & 0b1111) // 1111 => 1 + 2 + 4 + 8 => 15
«Битовый ИЛИ»: |
Оператор | сравнивает каждый разряд двоичного числа, если один из разрядов равен 1, то данный оператор вернет 1:
console.log(0b1001 | 0b0110) // 1111 => 15
console.log(0b1000 | 0b1100) // 1100 => 12
console.log(0b1111 | 0b1111) // 1111 => 1 + 2 + 4 + 8 => 15
«Битовый НЕ»: ~
Для того чтобы инверсировать все разряды в числе используется оператор ~:
console.group("0b0 (0)")
console.log(0b0); // 0
console.log(~0b0); // -1 (0b111111...111)
console.groupEnd();

console.group("0b1 (1)")
console.log(0b1); // 1
console.log(~0b1); // -2 (0b11111111...1110)
console.groupEnd();

console.group("0b100 (4)")
console.log(0b100); // 4
console.log(~0b100); // -5 (0b11111...11011)
console.groupEnd();
Вот как происходит изменение битов:
По сути мы просто меняем каждую цифру из числа по логике («там где был 0 - ставим 1, а там где была 1 - ставим 0»)
Может возникнуть резонный вопрос: «Почему при ~0 мы получаем -1 (в десятичной), а не максимальное число (где все разряды - 1)?»
При инвертировании 00000000, мы должны получить 11111111, данное число мы и получаем. Все дело в том, как устроены отрицательные двоичные числа.
Вот хорошее видео на эту тему: Binary: Plusses & Minuses (Why We Use Two's Complement) - Computerphile
«Битовый Сдвиг влево»: <<
Битовый сдвиг влево двигает все цифры двоичного числа влево и добавляет в конец 0. Справа от данного оператора можно указать сколько именно битов нам нужно сдвинуть.
console.log(0b101); // 5
console.log(0b101 << 1); // 0b1010 -> 10

console.log(0b101); // 5
console.log(0b101 << 2); // 0b10100 -> 20
«Битовый Сдвиг вправо»: >>
Побитовый сдвиг вправо сдвигает все цифры в числе на 1 ячейку вправо. Данный оператор дублирует первую цифру с левой части числа для того чтобы вставить ее в начало.
let num;

console.group('0b00000000000000000000000000000101')
num = 0b00000000000000000000000000000101;
console.log(num); // 5
console.log(num >> 1); // 2 => 0b00000000000000000000000000000010
console.log(num >> 2); // 1 => 0b00000000000000000000000000000001
console.groupEnd();

console.group('0b11111111111111111111111111111010')
num = -0b00000000000000000000000000000101; // 0b11111111111111111111111111111010 => -5
console.log(num); // -5
console.log(num >> 1); // -3 => 0b11111111111111111111111111111101
console.log(num >> 2); // -2 => 0b11111111111111111111111111111110
console.log(num >> 3); // -1 => 0b11111111111111111111111111111111
console.groupEnd();
«Беззнаковый битовый сдвиг вправо»: >>>
Данный оператор применяют когда нам нужно просто добавить 0 в начало числа, неважно какой до этого был знак у двоичного числа. Отрицательные числа всегда становятся положительными после использования данного оператора:
// Для положительных чисел ничего не поменялось
let num;

console.group('0b00000000000000000000000000000101')
num = 0b00000000000000000000000000000101;
console.log(num); // 5
console.log(num >> 1); // 2 => 0b00000000000000000000000000000010
console.log(num >> 2); // 1 => 0b00000000000000000000000000000001
console.groupEnd();

// Однако, для отрицательных чисел кое-что поменялось
console.group('0b11111111111111111111111111111010')
num = -0b00000000000000000000000000000101; // 0b11111111111111111111111111111010 => -5
console.log(num); // -5
console.log(num >>> 1); // 2147483645 => 0b01111111111111111111111111111101
console.log(num >>> 2); // 1073741822 => 0b00111111111111111111111111111110
console.log(num >>> 3); // 536870911 =>  0b00011111111111111111111111111111
console.groupEnd();
Референсы