Лекция
Привет, сегодня поговорим про побитовые операторы, обещаю рассказать все что знаю. Для того чтобы лучше понимать что такое побитовые операторы , настоятельно рекомендую прочитать все из категории Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend).
<<
(Левый сдвиг)>>
(Правый сдвиг, переносящий знак)>>>
(Правый сдвиг с заполнением нулями)-1
2
побитовые операторы интерпретируют операнды как последовательность из 32 битов (нулей и единиц). Они производят операции, используя двоичное представление числа, и возвращают новую последовательность из 32 бит (число) в качестве результата.
Эта глава сложная, требует дополнительных знаний в программировании и не очень важная, вы можете пропустить ее.
Побитовые операторы в JavaScript работают с 32-битными целыми числами в их двоичном представлении.
Это представление называется «32-битное целое со знаком, старшим битом слева и дополнением до двойки».
Разберем, как устроены числа внутри подробнее, это необходимо знать для битовых операций с ними.
Примеры представления чисел в двоичной системе:
1 |
a = 0; // 00000000000000000000000000000000 |
2 |
a = 1; // 00000000000000000000000000000001 |
3 |
a = 2; // 00000000000000000000000000000010 |
4 |
a = 3; // 00000000000000000000000000000011 |
5 |
a = 255; // 00000000000000000000000011111111 |
Обратите внимание, каждое число состоит ровно из 32-битов.
Несмотря на то, что нам такой способ записи чисел кажется не совсем обычным, бывают языки и технологии, использующие способ записи «младший бит слева», когда биты пишутся наоборот, от меньшего разряда к большему.
Именно поэтому спецификация EcmaScript явно говорит «старший бит слева».
Двоичный вид числа, обратного данному (например, 5
и -5
) получается путем обращения всех битов с прибавлением 1.
То есть, нули заменяются на единицы, единицы — на нули и к числу прибавляется 1
. Получается внутреннее представление того же числа, но со знаком минус.
Например, вот число 314
:
00000000000000000000000100111010 |
Чтобы получить -314
, первый шаг — обратить биты числа: заменить 0
на 1
, а 1
на 0
:
11111111111111111111111011000101 |
Второй шаг — к полученному двоичному числу приплюсовать единицу, обычным двоичным сложением: 11111111111111111111111011000101 + 1 = 11111111111111111111111011000110
.
Итак, мы получили:
-314 = 11111111111111111111111011000110 |
Принцип дополнения до двойки делит все двоичные представления на два множества: если крайний-левый бит равен 0
— число положительное, если 1
— число отрицательное. Поэтому этот бит называется знаковым битом.
В следующей таблице перечислены все побитовые операторы.
Далее операторы разобраны более подробно.
Оператор | Использование | Описание |
---|---|---|
Побитовое И (AND) | a & b |
Ставит 1 на бит результата, для которого соответствующие биты операндов равны 1. |
Побитовое ИЛИ (OR) | a | b |
Ставит 1 на бит результата, для которого хотя бы один из соответствующих битов операндов равен 1. |
Побитовое исключающее ИЛИ (XOR) | a ^ b |
Ставит 1 на бит результата, для которого только один из соответствующих битов операндов равен 1 (но не оба). |
Побитовое НЕ (NOT) | ~a |
Заменяет каждый бит операнда на противоположный. |
Левый сдвиг | a << b |
Сдвигает двоичное представление a на b битов влево, добавляя справа нули. |
Правый сдвиг, переносящий знак | a >> b |
Сдвигает двоичное представление a на b битов вправо, отбрасывая сдвигаемые биты. |
Правый сдвиг с заполнением нулями | a >>> b |
Сдвигает двоичное представление a на b битов вправо, отбрасывая сдвигаемые биты и добавляя нули слева. |
Побитовые операторы работают следующим образом:
Посмотрим, как работают операторы, на примерах.
Выполняет операцию И над каждой парой бит.
Результат a & b
равен единице только когда оба бита a
и b
равны единице.
Таблица истинности для &
:
a | b | a & b |
---|---|---|
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
Пример:
1 |
9 (по осн. 10) |
2 |
= 00000000000000000000000000001001 (по осн. 2) |
3 |
14 (по осн. 10) |
4 |
= 00000000000000000000000000001110 (по осн. 2) |
5 |
-------------------------------- |
6 |
14 & 9 (по осн. 10) |
7 |
= 00000000000000000000000000001000 (по осн. 2) |
8 |
= 8 (по осн. 10) |
Выполняет операцию ИЛИ над каждой парой бит. Результат a | b
равен 1, если хотя бы один бит изa,b
равен 1.
Таблица истинности для |
:
a | b | a | b |
---|---|---|
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
Пример:
1 |
9 (по осн. 10) |
2 |
= 00000000000000000000000000001001 (по осн. 2) |
3 |
14 (по осн. 10) |
4 |
= 00000000000000000000000000001110 (по осн. 2) |
5 |
-------------------------------- |
6 |
14 | 9 (по осн. 10) |
7 |
= 00000000000000000000000000001111 (по осн. 2) |
8 |
= 15 (по осн. 10) |
Выполняет операцию «Исключающее ИЛИ» над каждой парой бит.
a
Исключающее ИЛИ b
равно 1, если только a=1
или только b=1
, но не оба одновременно a=b=1
.
Таблица истинности для исключающего ИЛИ:
a | b | a ^ b |
---|---|---|
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
Как видно, оно дает 1, если ЛИБО слева 1
, ЛИБО справа 1
, но не одновременно. Поэтому его и называют «исключающее ИЛИ».
Пример:
1 |
9 (по осн. 10) |
2 |
= 00000000000000000000000000001001 (по осн. 2) |
3 |
14 (по осн. 10) |
4 |
= 00000000000000000000000000001110 (по осн. 2) |
5 |
-------------------------------- |
6 |
14 ^ 9 (по осн. 10) |
7 |
= 00000000000000000000000000000111 (по осн. 2) |
8 |
= 7 (по осн. 10) |
Исключающее или можно использовать для шифрования, так как эта операция полностью обратима. Двойное применение исключающего ИЛИ с тем же аргументом дает исходное число.
Иначе говоря, верна формула: a ^ b ^ b == a
.
Пускай Вася хочет передать Пете секретную информацию data
. Эта информация заранее превращена в число, например строка интерпретируется как последовательность кодов символов.
Вася и Петя заранее договариваются о числовом ключе шифрования key
.
Алгоритм:
data
и делает операцию data ^ key
. При необходимости data
бьется на части, равные по длине key
, чтобы можно было провести побитовое ИЛИ ^
по всей длине. JavaScript позволяет делать операцию ^
с 32-битными целыми числами, так что data
нужно разбить на последовательность таких чисел.data ^ key
отправляется Пете, это шифровка.Например, пусть в data
очередное число равно 9
, а ключ key
равен 1220461917
.
Данные: 9 в двоичном виде |
00000000000000000000000000001001 |
Ключ: 1220461917 в двоичном виде |
01001000101111101100010101011101 |
Результат операции 9 ^ key: |
01001000101111101100010101010100 |
Результат в 10-ной системе (шифровка): |
1220461908 |
1220461908
, применяет к нему такую же операцию ^ key
.data
.В нашем случае:
Полученная шифровка в двоичной системе: |
9 ^ key = 1220461908 |
01001000101111101100010101010100 |
Ключ: 1220461917 в двоичном виде: |
01001000101111101100010101011101 |
Результат операции 1220461917 ^ key: |
00000000000000000000000000001001 |
Результат в 10-ной системе (исходное сообщение): |
9 |
Конечно, такое шифрование поддается частотному анализу и другим методам дешифровки, поэтому современные алгоритмы используют XOR как одну из важных частей более сложной многоступенчатой схемы.
Производит операцию НЕ над каждым битом, заменяя его на обратный ему.
Таблица истинности для НЕ:
a | ~a |
---|---|
0 |
1 |
1 |
0 |
Пример:
1 |
9 (по осн. Об этом говорит сайт https://intellect.icu . 10) |
2 |
= 00000000000000000000000000001001 (по осн. 2) |
3 |
-------------------------------- |
4 |
~9 (по осн. 10) |
5 |
= 11111111111111111111111111110110 (по осн. 2) |
6 |
= -10 (по осн. 10) |
Из-за внутреннего представления отрицательных чисел получается так, что ~n == -(n+1)
.
Например:
1 |
alert(~3); // -4 |
2 |
alert(~-1); // 0 |
Операторы битового сдвига принимают два операнда. Первый — это число для сдвига, а второй — количество битов, которые нужно сдвинуть в первом операнде.
Направление сдвига — то же, что и направление стрелок в операторе.
<<
(Левый сдвиг)Этот оператор сдвигает первый операнд на указанное число битов влево. Лишние биты отбрасываются, справа добавляются нулевые биты.
Например, 9 << 2
даст 36
:
1 |
9 (по осн.10) |
2 |
= 00000000000000000000000000001001 (по осн.2) |
3 |
-------------------------------- |
4 |
9 << 2 (по осн.10) |
5 |
= 00000000000000000000000000100100 (по осн.2) |
6 |
= 36 (по осн.10) |
>>
(Правый сдвиг, переносящий знак)Этот оператор сдвигает биты вправо, отбрасывая лишние. Копии крайнего-левого бита добавляются слева. Так как итоговый крайний-левый бит имеет то же значение, что и исходный, знак числа (представленный крайним-левым битом) не изменяется.
Поэтому он назван «переносящим знак».
Например, 9 >> 2
даст 2
:
1 |
9 (по осн.10) |
2 |
= 00000000000000000000000000001001 (по осн.2) |
3 |
-------------------------------- |
4 |
9 >> 2 (по осн.10) |
5 |
= 00000000000000000000000000000010 (по осн.2) |
6 |
= 2 (по осн.10) |
Аналогично, -9 >> 2
даст -3
, так как знак сохранен:
1 |
-9 (по осн.10) |
2 |
= 11111111111111111111111111110111 (по осн.2) |
3 |
-------------------------------- |
4 |
-9 >> 2 (по осн.10) |
5 |
= 11111111111111111111111111111101 (по осн.2) = -3 (по осн.10) |
>>>
(Правый сдвиг с заполнением нулями)Этот оператор сдвигает биты первого операнда вправо. Лишние биты справа отбрасываются. Слева добавляются нулевые биты.
Знаковый бит становится равным 0, поэтому результат всегда положителен.
Для неотрицательных чисел правый сдвиг с заполнением нулями и правый сдвиг с переносом знака дадут одинаковый результат, т.к в обоих случаях слева добавятся нули.
Для отрицательных чисел — результат работы разный. Например, -9 >>> 2
даст 1073741821
, в отличие от -9 >> 2
(дает -3
):
1 |
-9 (по осн.10) |
2 |
= 11111111111111111111111111110111 (по осн.2) |
3 |
-------------------------------- |
4 |
-9 >>> 2 (по осн.10) |
5 |
= 00111111111111111111111111111101 (по осн.2) |
6 |
= 1073741821 (по осн.10) |
Побитовые операторы используются редко, но все же используются.
Случаи применения побитовых операторов, которые мы здесь разберем, составляют большинство всех использований в JavaScript.
В JavaScript побитовые операторы ^
, &
, |
выполняются после сравнений ==
.
Например, в сравнении a == b^0
будет сначала выполнено сравнение a == b
, а потом уже операция ^0
, как будто стоят скобки (a == b)^0
.
Обычно это не то, чего мы хотим. Чтобы гарантировать желаемый порядок, нужно ставить скобки: a == (b^0)
.
Для этого примера представим, что наш скрипт работает с пользователями:
Гость
Редактор
Админ
У каждого из них есть ряд доступов, которые можно свести в таблицу:
Пользователь | Просмотр статей | Изменение статей | Просмотр товаров | Изменение товаров | Общее администрирование |
---|---|---|---|---|---|
Гость | Да | Нет | Да | Нет | Нет |
Редактор | Да | Да | Да | Да | Нет |
Админ | Да | Да | Да | Да | Да |
Если вместо «Да» поставить 1
, а вместо «Нет» — 0
, то каждый набор доступов описывается числом:
Пользователь | Просмотр статей | Изменение статей | Просмотр товаров | Изменение товаров | Общее администрирование | В десятичной системе |
---|---|---|---|---|---|---|
Гость | 1 | 0 | 1 | 0 | 0 | = 20 |
Редактор | 1 | 1 | 1 | 1 | 0 | = 30 |
Админ | 1 | 1 | 1 | 1 | 1 | = 31 |
Мы «упаковали» много информации в одно число. Это экономит память. Но, кроме этого, по нему очень легко проверить, имеет ли посетитель заданную комбинацию доступов.
Для этого посмотрим, как в 2-ной системе представляется каждый доступ в отдельности.
00001 (=1)
(все нули кроме 1
на позиции, соответствующей этому доступу).00010 (=2)
.00100 (=4)
.01000 (=8)
.10000 (=16)
.Например, просматривать и изменять статьи позволит доступ access = 11000
.
Как правило, доступы задаются в виде констант:
1 |
var ACCESS_ADMIN = 1; // 00001 |
2 |
var ACCESS_GOODS_CHANGE = 2; // 00010 |
3 |
var ACCESS_GOODS_VIEW = 4; // 00100 |
4 |
var ACCESS_ARTICLE_CHANGE = 8; // 01000 |
5 |
var ACCESS_ARTICLE_VIEW = 16; // 10000 |
Из этих констант получить нужную комбинацию доступов можно при помощи операции |
.
var access = ACCESS_ARTICLE_VIEW | ACCESS_ARTICLE_CHANGE; // 11000 |
Для удобной работы с примерами в этой статье пригодятся две функции.
parseInt("11000", 2)
— переводит строку с двоичной записью числа в число.n.toString(2)
— получает для числа n
запись в 2-ной системе в виде строки.Например:
1 |
var access = parseInt( "11000" , 2); // получаем число из строки |
2 |
3 |
alert(access); // 24, число с таким 2-ным представлением |
4 |
5 |
var access2 = access.toString(2); // обратно двоичную строку из числа |
6 |
7 |
alert(access2); // 11000 |
Для того, чтобы понять, есть ли в доступе access
нужный доступ, например право администрирования — достаточно применить к нему побитовый оператор И (&
) с соответствующей маской.
Создадим для примера ряд доступов и проверим их:
1 |
var access = parseInt( "11111" , 2); // 31, все 1 означает, что доступ полный |
2 |
3 |
alert(access & ACCESS_ADMIN); // если результат не 0, то есть доступ ACCESS_ADMIN |
А теперь та же проверка для посетителя с другими правами:
1 |
var access = parseInt( "10100" ); // 20, нет 1 в конце |
2 |
3 |
alert(access & ACCESS_ADMIN); // 0, нет доступа к общему администрированию |
Такая проверка работает, потому что оператор И ставит 1
на те позиции результата, на которых в обоих операндах стоит 1
.
Так что access & 1
для любого числа access
поставит все биты в ноль, кроме самого правого. А самый правый станет 1
только если он равен 1
в access
.
Для полноты картины также проверим, дает ли доступ 11111
право на изменение товаров. Для этого нужно применить к доступу оператор И (&
) с 00010
(=2
в 10-ной системе).**
1 |
var adminAccess = 31; // 11111 |
2 |
3 |
alert(adminAccess & ACCESS_GOODS_CHANGE); // не 0, есть доступ к изменению товаров |
Можно проверить один из нескольких доступов.
Например, проверим, есть ли права на просмотр ИЛИ изменение товаров. Соответствующие права задаются битом 1
на втором и третьем месте с конца, что дает число 00110
(=6
в 10-ной системе).
1 |
var check = ACCESS_GOODS_VIEW | ACCESS_GOODS_CHANGE; // 6, 00110 |
2 |
3 |
var access = 30; // 11110; |
4 |
5 |
alert(access & check); // не 0, значит есть доступ к просмотру ИЛИ изменению |
6 |
7 |
access = parseInt( "11100" , 2); |
8 |
9 |
alert(access & check); // не 0, есть доступ к просмотру ИЛИ изменению |
Как видно из примера выше, если в аргументе check
стоит ИЛИ из нескольких доступов ACCESS_*
, то и результат проверки скажет, есть ли хотя бы один из них. А какой — нужно смотреть отдельной проверкой, если это важно.
Итак, маска дает возможность удобно «паковать» много битовых значений в одно число при помощи ИЛИ |
, а также, при помощи оператора И (&
), проверять маску на комбинацию установленных битов.
Зачастую маски используют в функциях, чтобы одним параметром передать несколько «флагов», т.е. однобитных значений.
Например:
// найти пользователей с правами на изменение товаров или администраторов |
findUsers(ACCESS_GOODS_CHANGE | ACCESS_ADMIN); |
Так как битовые операции отбрасывают десятичную часть, то их можно использовать для округления. Достаточно взять любую операцию, которая не меняет значение числа.
Например, двойное НЕ (~
):
1 |
alert( ~~12.345 ); // 12 |
Подойдет и Исключающее ИЛИ (^
) с нулем:
1 |
alert( 12.345^0 ); // 12 |
Последнее даже более удобно, поскольку отлично читается:
1 |
alert( 12.3 * 14.5 ^ 0); // (=178) "12.3 умножить на 14.5 и округлить" |
У побитовых операторов достаточно низкий приоритет, он меньше чем у остальной арифметики:
1 |
alert( 1.1 + 1.2 ^ 0 ); // 2, сложение выполнится раньше округления |
-1
Внутренний формат чисел устроен так, что для смены знака нужно все биты заменить на противоположные («обратить») и прибавить 1
.
Обращение битов — это побитовое НЕ (~
). То есть, при таком формате представления числа-n = ~n + 1
. Или, если перенести единицу: ~n = -(n+1)
.
Как видно из последнего равенства, ~n == 0
только если n == -1
. Поэтому можно легко проверить равенство n == -1
:
1 |
var n = 5; |
2 |
3 |
if (~n) { // сработает, т.к. ~n = -(5+1) = -6 |
4 |
alert( "n не -1" ); // выведет! |
5 |
} |
1 |
var n = -1; |
2 |
3 |
if (~n) { // не сработает, т.к. ~n = -(-1+1) = 0 |
4 |
alert( "...ничего не выведет..." ); |
5 |
} |
Проверка на -1
пригождается, например, при поиске символа в строке. Вызовstr.indexOf("подстрока")
возвращает позицию подстроки в str
, или -1
если не нашел.
1 |
var str = "Проверка" ; |
2 |
3 |
if (~str.indexOf( "верка" )) { // Сочетание "if (~...indexOf)" читается как "если найдено" |
4 |
alert( 'найдено!' ); |
5 |
} |
2
Оператор a << b
, сдвигая биты, по сути умножает a
на 2b
.
Например:
1 |
alert( 1 << 2 ); // 1*(2*2) = 4 |
2 |
alert( 1 << 3 ); // 1*(2*2*2) = 8 |
3 |
alert( 3 << 3 ); // 3*(2*2*2) = 24 |
Оператор a >> b
, сдвигая биты, производит целочисленное деление a
на 2b
.
1 |
alert( 8 >> 2 ); // 2 = 8/4, убрали 2 нуля в двоичном представлении |
2 |
alert( 11 >> 2 ); // 2, целочисленное деление (менее значимые биты просто отброшены) |
& | ^ << >> >>>
.~
.Как правило, битовое представление числа используется для:
&
. Кроме того, такое упакованное значение будет для функции всего одним параметром, это тоже удобно.(12.34^0) = 12
.-1
: if (~n) { n не -1 }
.
К сожалению, в одной статье не просто дать все знания про побитовые операторы. Но я - старался. Если ты проявишь интерес к раскрытию подробностей,я обязательно напишу продолжение! Надеюсь, что теперь ты понял что такое побитовые операторы и для чего все это нужно, а если не понял, или есть замечания, то не стесняйся, пиши или спрашивай в комментариях, с удовольствием отвечу. Для того чтобы глубже понять настоятельно рекомендую изучить всю информацию из категории Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)
Комментарии
Оставить комментарий
Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)
Термины: Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)