Bitwise Operations and Flags
Table of Contents
- Why Work With Individual Bits?
- AND, OR, XOR, and NOT
- Shifts
- Bitmasks: Packing Many Yes/No Values Into One Number
- Unix Permissions and chmod 755
- Feature Flags
- Handy Bit Tricks
- The 32-Bit Caveat in JavaScript
- Recipe: bits.js
- Try It Yourself
1. Why Work With Individual Bits?
In the last chapter you saw that a number is a row of bits. Sometimes it is useful to treat that row as many separate switches instead of one quantity.
Imagine a user in an app can have these permissions: read, write,
delete, share, export, admin. You could add six true/false columns to
a database. Or you could use one number, where each bit is one
permission. That is smaller, easy to copy, and fast to check. File
systems, network protocols, graphics libraries, and game engines all do
this.
To do it you need bitwise operators: operators that work on each bit of a number separately.
2. AND, OR, XOR, and NOT
Each operator compares the bits of two numbers position by position. We
will use 12 (1100) and 10 (1010).
| Operator | Name | Rule for each bit position | Example |
|---|---|---|---|
& | AND | 1 only if both bits are 1 | 12 & 10 is 8 |
| | OR | 1 if either bit is 1 | 12 | 10 is 14 |
^ | XOR | 1 if the bits are different | 12 ^ 10 is 6 |
~ | NOT | flips every bit (one input) | ~12 is -13 |
Line them up to see it:
1100 (12) 1100 (12) 1100 (12)
& 1010 (10) | 1010 (10) ^ 1010 (10)
---- ---- ----
1000 (8) 1110 (14) 0110 (6)
In code:
console.log(0b1100 & 0b1010); // 8
console.log(0b1100 | 0b1010); // 14
console.log(0b1100 ^ 0b1010); // 6
Here is how to remember them:
- AND is a filter: it keeps only bits that are on in both. It is used to check and to clear bits.
- OR is a merge: it turns bits on. It is used to set bits.
- XOR is a difference detector: it flips a bit where the other side
has a
1. It is used to toggle bits.
~ (NOT) flips every bit. The result looks strange because JavaScript
works with signed 32-bit numbers, so flipping 12 gives -13. A handy
rule is ~n equals -n - 1. To see just the low bits, combine it with
AND:
console.log(~12); // -13
console.log(~0b1100 & 0b1111); // 3 (0011: the four bits flipped)
For flags, you will mostly use ~ together with & to clear bits, so
you rarely look at the raw negative result.
3. Shifts
A shift slides all the bits left or right.
| Operator | Meaning | Example | Result |
|---|---|---|---|
<< | Shift left | 3 << 2 | 12 |
>> | Shift right | 12 >> 2 | 3 |
0011 << 2 = 1100 (3 becomes 12)
1100 >> 2 = 0011 (12 becomes 3)
Shifting left by 1 doubles the number, shifting left by n multiplies
by 2^n. Shifting right by n divides by 2^n and drops the
remainder (it rounds toward minus infinity):
console.log(3 << 2); // 12 (same as 3 * 4)
console.log(1 << 10); // 1024 (same as 2 ** 10)
console.log(12 >> 2); // 3 (same as Math.floor(12 / 4))
console.log(13 >> 1); // 6 (13 / 2 is 6.5, the .5 is dropped)
The shift 1 << n is especially useful. It creates a number with only
bit n turned on, counting from zero on the right:
| Expression | Binary | Value |
|---|---|---|
1 << 0 | 0001 | 1 |
1 << 1 | 0010 | 2 |
1 << 2 | 0100 | 4 |
1 << 3 | 1000 | 8 |
That gives us the building blocks for flags.
There is also >>>, the unsigned right shift. We will meet it in the
32-bit section.
4. Bitmasks: Packing Many Yes/No Values Into One Number
A flag is a number with exactly one bit set. A bitmask is a number used to select or change certain bits. Let's give each permission its own bit:
const READ = 0b100; // 4
const WRITE = 0b010; // 2
const EXEC = 0b001; // 1
A user's permissions are one number that combines the flags with OR:
let perms = READ | WRITE;
console.log(perms); // 6
console.log(perms.toString(2)); // 110
Now the four operations that cover almost every use:
| Task | Expression | Why it works |
|---|---|---|
| Check a flag | (perms & FLAG) === FLAG | AND keeps the flag's bit only if it is on |
| Set a flag | perms | FLAG | OR turns the bit on, leaves others alone |
| Clear a flag | perms & ~FLAG | ~FLAG is all ones except that bit; AND removes it |
| Toggle a flag | perms ^ FLAG | XOR flips just that bit |
console.log((perms & WRITE) === WRITE); // true
console.log((perms & EXEC) === EXEC); // false
perms = perms | EXEC; // set EXEC -> 111 = 7
perms = perms & ~WRITE; // clear WRITE -> 101 = 5
perms = perms ^ WRITE; // toggle WRITE -> 111 = 7
Why compare with === FLAG and not just test for non-zero? When you
check several flags at once, such as READ | WRITE, you need to know
that all of them are on. Comparing to the whole mask does that.
5. Unix Permissions and chmod 755
Linux and macOS store file permissions exactly this way. Each file has three groups of three flags:
| Group | Who |
|---|---|
| Owner (user) | the person who owns the file |
| Group | users in the file's group |
| Other | everyone else |
Each group has read (r = 4), write (w = 2), and execute (x = 1).
Add the numbers to get one digit from 0 to 7:
| Digit | Binary | Permissions |
|---|---|---|
| 7 | 111 | rwx |
| 6 | 110 | rw- |
| 5 | 101 | r-x |
| 4 | 100 | r-- |
| 0 | 000 | --- |
Three digits make the whole mode, which is why you write chmod 755:
chmod 755 deploy.sh # owner: rwx, group: r-x, other: r-x
ls -l deploy.sh
# -rwxr-xr-x 1 dara staff 120 Oct 3 09:00 deploy.sh
The 7 is rwx for the owner, the first 5 is r-x for the group, and
the last 5 is r-x for everyone else. Common modes:
| Mode | Meaning | Typical use |
|---|---|---|
755 | rwxr-xr-x | programs, scripts, folders |
644 | rw-r--r-- | normal files, web pages |
600 | rw------- | private keys, secrets |
This is why octal is useful: one octal digit is exactly three bits,
so 0o755 is 111 101 101 in binary. In JavaScript you can write it
directly and pull each group out with a shift and a mask:
const mode = 0o755;
console.log(mode); // 493
console.log(mode.toString(2)); // 111101101
console.log((mode >> 6) & 0b111); // 7 (owner)
console.log((mode >> 3) & 0b111); // 5 (group)
console.log(mode & 0b111); // 5 (other)
Each line shifts the group you want down to the lowest three bits, then
uses & 0b111 to cut off everything else. We turn this into the function
toUnixMode in the recipe.
6. Feature Flags
The same idea works for app settings. Say your product has four optional features and you save them in one number per user:
const Feature = {
DARK_MODE: 1 << 0, // 1
BETA: 1 << 1, // 2
EXPORT: 1 << 2, // 4
ADMIN: 1 << 3, // 8
};
Using 1 << n means you only have to count positions, not work out
powers of two in your head, and it makes it hard to give two flags the
same bit by mistake.
let user = Feature.DARK_MODE | Feature.ADMIN;
console.log(user); // 9
console.log(user.toString(2).padStart(4, "0")); // 1001
console.log((user & Feature.BETA) === Feature.BETA); // false
console.log((user & Feature.ADMIN) === Feature.ADMIN); // true
user = user | Feature.BETA; // 1011
user = user & ~Feature.DARK_MODE; // 1010
console.log(user); // 10
One number stores all four settings. A database column of type integer holds up to 31 or 32 of them. You can compare, copy, and send the number without any parsing.
Honest trade-off: a number like 10 is unreadable on its own, and
adding a 33rd flag is not possible in a normal 32-bit column. For
settings that people read and edit often, an array such as
["beta", "admin"] or a list of booleans is clearer. Use bit flags when
space, speed, or an existing format (like Unix modes or a binary
protocol) matters. Chapter
5 shows that
a flag number and a set of names are two views of the same idea.
7. Handy Bit Tricks
Once you read bits comfortably, some short expressions become clear:
function isOdd(n) {
return (n & 1) === 1; // the lowest bit is 1 for odd numbers
}
console.log(isOdd(7)); // true
console.log(isOdd(10)); // false
function isPowerOfTwo(n) {
return n > 0 && (n & (n - 1)) === 0;
}
console.log(isPowerOfTwo(64)); // true
console.log(isPowerOfTwo(96)); // false
Why does the power-of-two check work? A power of two has exactly one bit
set, like 1000000 (64). Subtracting 1 gives 0111111 (63), where
every bit is different, so 64 & 63 is 0. For 96 (1100000) the top
bit survives, so the result is not zero.
Another trick is XOR with the same value twice, which gets you back to
the start: 7 ^ 2 ^ 2 is 7. Simple encryption puzzles, checksums, and
swapping algorithms use it, though in everyday code you will rarely need
it.
Be readable first. n % 2 === 0 is clearer than (n & 1) === 0 in most
code. Bit tricks are valuable when you are working with flags and
binary data, not when you are just trying to look clever.
8. The 32-Bit Caveat in JavaScript
Normal JavaScript numbers are 64-bit floating-point values, as you saw in chapter 2. But bitwise operators work on 32-bit signed integers. Before an operation, JavaScript converts your number to 32 bits, and converts the result back. This causes surprises with big numbers:
console.log(2 ** 31); // 2147483648
console.log(2 ** 31 | 0); // -2147483648 (wrapped around to negative)
console.log(1 << 31); // -2147483648
console.log(1 << 32); // 1 (the shift count wraps: 32 acts as 0)
console.log(2 ** 32 + 5 | 0); // 5 (the top bits were cut off)
console.log(5.9 | 0); // 5 (decimals are dropped)
Three rules to take away:
- Only 32 bits. Flags above bit 30 are risky. Bit 31 is the sign bit
and makes the number negative. Keep flags at
1 << 30or lower, or at most 31 flags. - Results can be negative.
1 << 31is-2147483648. Use>>> 0to read a result as an unsigned (never negative) number:
console.log((1 << 31) >>> 0); // 2147483648
console.log(-1 >>> 0); // 4294967295
console.log(~5 >>> 0); // 4294967290
- Need more than 32 flags? Use
BigInt, whose operators&,|,^,<<work on numbers of any size. Write BigInt literals with a trailingn:
console.log(1n << 40n); // 1099511627776n
console.log(5n & 3n); // 1n
You cannot mix BigInt and normal numbers in one expression, such as
1n | 2, so pick one type per flag set. Chapter
7
covers BigInt properly.
One more habit worth having: x | 0 and ~~x are sometimes used to
chop off decimals, but Math.trunc(x) says what you mean and does not
break for numbers beyond 32 bits.
9. Recipe: bits.js
Create src/bits.js with permission flags and the four flag operations:
export const READ = 0b100; // 4
export const WRITE = 0b010; // 2
export const EXEC = 0b001; // 1
export function hasFlag(value, flag) {
return (value & flag) === flag;
}
export function setFlag(value, flag) {
return value | flag;
}
export function clearFlag(value, flag) {
return value & ~flag;
}
export function toggleFlag(value, flag) {
return value ^ flag;
}
function triple(bits) {
return (
(hasFlag(bits, READ) ? "r" : "-") +
(hasFlag(bits, WRITE) ? "w" : "-") +
(hasFlag(bits, EXEC) ? "x" : "-")
);
}
export function toUnixMode(mode) {
const owner = (mode >> 6) & 0b111;
const group = (mode >> 3) & 0b111;
const other = mode & 0b111;
return triple(owner) + triple(group) + triple(other);
}
triple is a small helper that is not exported: it turns three bits into
text like r-x. toUnixMode splits the mode into its three groups, as
in section 5, and joins the three pieces.
Add it to src/index.js:
export * from "./bits.js";
(Keep the lines from earlier chapters, including export * from "./numbers.js";.)
Then add to demo.js:
import {
READ, WRITE, EXEC,
hasFlag, setFlag, clearFlag, toggleFlag, toUnixMode,
} from "./src/index.js";
let perms = READ | WRITE;
console.log(perms); // 6
console.log(hasFlag(perms, WRITE)); // true
console.log(hasFlag(perms, EXEC)); // false
perms = setFlag(perms, EXEC);
console.log(perms); // 7
perms = clearFlag(perms, WRITE);
console.log(perms); // 5
perms = toggleFlag(perms, WRITE);
console.log(perms); // 7
console.log(toUnixMode(0o755)); // rwxr-xr-x
console.log(toUnixMode(0o644)); // rw-r--r--
console.log(toUnixMode(0o754)); // rwxr-xr--
console.log(toUnixMode(0o600)); // rw-------
If your demo.js already has an import line from ./src/index.js,
merge the names into one import instead of adding a second. Run:
node demo.js
The new lines print 6, true, false, 7, 5, 7, and then the
four rwx strings shown in the comments.
10. Try It Yourself
- Paper first. Work out
0b1111 & 0b0101,0b1010 | 0b0101, and0b1110 ^ 0b0111by lining up the bits. Check each one in Node with.toString(2). - Add a fourth flag. Add a
DELETEflag to your permission set that does not collide withREAD,WRITE, orEXEC. Which value should it have? What happens totoUnixModeif you pass it a number containing that flag? - Read a mode. What are
chmod 640andchmod 700inrwxform? Check your answer withtoUnixMode(0o640)andtoUnixMode(0o700). - Build the reverse. Write
fromUnixMode("rwxr-xr--")that returns the number0o754. (Hint: eachr,w,xis a flag; addREAD,WRITE, orEXECand shift each group into place with<<.) - Feature flag helper. Using the
Featureobject from section 6, writelistFlags(value)that returns the names of all flags that are on, for example["DARK_MODE", "ADMIN"]for9. (Hint:Object.entriesandhasFlag.) - Break the 32-bit limit. Run
1 << 31,1 << 32, and(1 << 31) >>> 0. Then rewrite1 << 40usingBigInt. Explain in your own words why the plain version gives the wrong answer.
You can now store and test many yes/no values in one number. In Boolean
Logic, we step back from
bits to the true/false rules behind every if statement, and learn
how to simplify tangled conditions.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!