Key Takeaways
- With Number operands, JavaScript bitwise operators work on 32-bit integers, not the full floating-point representation.
- Use bitwise operators for masks, flags, and packed integer fields—not as a general arithmetic optimization.
- The signed right shift,
>>, preserves the sign; the zero-fill right shift,>>>, returns an unsigned 32-bit result. - BigInt supports arbitrary-width bitwise operations, but you can't mix Number and BigInt operands or use
>>>with BigInt.
JavaScript bitwise operators manipulate individual binary digits: they combine, invert, or shift bits. Their most useful applications are bit masks, flags, and low-level integer data manipulation; they aren't inherently faster replacements for ordinary arithmetic.
The important distinction is between Number operations, which use a 32-bit integer domain, and BigInt operations, which don't have that fixed width. The examples below cover both, with the Number behavior explained first.
How JavaScript Bitwise Operators Convert Numbers
JavaScript's ordinary Number type uses IEEE 754 binary64 floating-point values. A bitwise operation doesn't manipulate those floating-point encoding bits directly: it converts the numeric value into a 32-bit integer first. The result is still a Number, not a binary string or a separate integer type.
For Number operands, the conversion works like this:
- Discard the fractional part toward zero.
- Reduce the integer modulo
2 ** 32, keeping its lowest 32 bits. - For a signed interpretation, treat values at or above
2 ** 31as negative by subtracting2 ** 32.
The signed range is −2,147,483,648 through 2,147,483,647. The unsigned range is 0 through 4,294,967,295. The conversions map NaN, either infinity, and either signed zero to positive zero. The specification defines these conversions as ToInt32 and ToUint32.
console.log(-5.23 | 0); // -5
console.log((2 ** 32 + 1) | 0); // 1
console.log(0x80000000 | 0); // -2147483648
console.log(-1 >>> 0); // 4294967295
console.log(Infinity | 0); // 0The operators &, |, ^, ~, <<, and >> produce signed 32-bit results for Number operands. Only >>> produces an unsigned 32-bit result.
Also distinguish bitwise & and | from logical && and ||. The former combine bits; the latter make decisions using truthiness. For the broader operator categories, see JavaScript ES6 Operators.
Bitwise AND, OR, XOR, and NOT Explained
In the small positive-number diagrams below, leading zeroes are omitted. NOT uses all 32 bits because the sign matters.
Bitwise AND: &
AND produces a 1 only where both corresponding input bits are 1. This makes it useful for selecting particular bits with a mask.
console.log(1 & 1); // 1
console.log(2 & 1); // 0
console.log(3 & 2); // 2 11 (3)
& 10 (2)
----
10 (2)For 3 & 2, both operands have the bit worth 2 set, so the result is binary 10, decimal 2.
Checking even and odd integers
For a known signed 32-bit integer x, (x & 1) === 0 tests whether its lowest bit is zero, meaning it's even. Don't apply this blindly to fractions or non-finite values. For general application code, a remainder test with explicit safe-integer validation is clearer.
function isEven(value) {
if (!Number.isSafeInteger(value)) {
throw new TypeError("Expected a safe integer");
}
return value % 2 === 0;
}
console.log(isEven(42)); // true
console.log(isEven(-3)); // falseBitwise OR: |
OR produces a 1 wherever either input bit is 1. Use it to combine flags or set selected bits without changing the others.
console.log(1 | 1); // 1
console.log(2 | 1); // 3
console.log(3 | 2); // 3 11 (3)
| 10 (2)
----
11 (3)Although 5.23 | 0 returns 5, OR isn't a general flooring operation. The negative-number and overflow cases in Common Mistakes show why.
Bitwise XOR: ^
XOR, or exclusive OR, produces a 1 where the corresponding input bits differ. XOR with a mask toggles its selected bits: 0 becomes 1, and 1 becomes 0.
console.log(1 ^ 1); // 0
console.log(2 ^ 1); // 3
console.log(3 ^ 2); // 1 11 (3)
^ 10 (2)
----
01 (1)Toggling flags is a practical use of XOR. Swapping ordinary variables with repeated XOR assignments isn't a good JavaScript convention; use destructuring or a temporary variable instead.
Bitwise NOT: ~
NOT flips every bit. Number bitwise operations use two's-complement representation, where negative integers have their highest bit set. In two's complement, negation is bit inversion followed by adding one; inversion alone therefore gives -n - 1. For a signed 32-bit integer n, ~n === -(n + 1).
console.log(~1); // -2
console.log(~2); // -3
console.log(~3); // -4 2: 00000000 00000000 00000000 00000010
~2: 11111111 11111111 11111111 11111101 (-3)In practice, combine NOT with AND to clear selected flags: flags & ~mask preserves the bits outside the mask and clears those inside it.
JavaScript Bitwise Shift Operators
Left shift: <<
For Number operands, left shift moves the coerced 32-bit value left, inserts zeroes on the right, and discards bits that leave the left edge. The result is signed, so shifting into the highest bit can produce a negative number.
3 << 2:
00000011 -> 00001100 (12; leading zeroes omitted)console.log(3 << 2); // 12
console.log(1.5 << 2); // 4, not 6
console.log(0x40000000 << 1); // -2147483648
console.log(0x40000000 * 2); // 2147483648For an integer already in the signed 32-bit range, x << 2 agrees with x * 4 when the product also fits that range. It isn't a replacement for multiplication over general JavaScript numbers.
Signed right shift: >>
Signed right shift discards bits on the right and copies the sign bit into the newly opened positions on the left. For example, -8 >> 2 is −2. For negative values, this isn't truncating division: -5 >> 1 is −3, not −2.
Zero-fill right shift: >>>
Zero-fill right shift inserts zeroes on the left and interprets the result as unsigned. This is why shifting the same negative input can produce a large positive value. A shift by zero still performs unsigned conversion: -1 >>> 0 is 4,294,967,295.
Input -8: 11111111 11111111 11111111 11111000
-8 >> 2: 11111111 11111111 11111111 11111110 (-2)
-8 >>> 2: 00111111 11111111 11111111 11111110 (1073741822)console.log(-8 >> 2); // -2
console.log(-8 >>> 2); // 1073741822Number shift counts wrap at 32
For all three Number shifts, the right operand is converted to an unsigned 32-bit integer, then reduced modulo 32. Only its lowest five bits determine the shift distance, so the effective count is 0 through 31.
console.log(1 << 32); // 1: same as shifting by 0
console.log(1 << 33); // 2: same as shifting by 1Using Bit Masks and Flags in JavaScript
A bit mask selects particular bit positions. A flag assigns one of those positions a meaning, such as whether a feature or permission is enabled. Give each independent flag a different bit, then combine them with OR.
This standalone example sets, tests, clears, and toggles flags. The compound assignments update the existing variable: |= sets bits, &= ~ clears them, and ^= toggles them.
const READ = 1 << 0; // 001
const WRITE = 1 << 1; // 010
const EXECUTE = 1 << 2; // 100
let permissions = READ | WRITE; // 011
console.log((permissions & READ) !== 0); // true
const required = READ | EXECUTE;
console.log((permissions & required) !== 0); // true: any
console.log((permissions & required) === required); // false: all
permissions |= EXECUTE; // 111: set
permissions &= ~WRITE; // 101: clear
permissions ^= EXECUTE; // 001: toggle
console.log(permissions); // 1Parenthesize the bitwise expression before comparing it. Equality binds more tightly than AND, so flags & READ === READ parses as flags & (READ === READ), not the intended comparison. See the operator precedence reference when combining operators.
Extracting packed integer fields
Shifts and masks also let you extract fields from a packed integer. Here, a 24-bit RGB value holds three 8-bit color channels; shifting selects the channel, and & 0xff keeps its lowest eight bits. This example is derived from the operator rules above.
const rgb = 0x3366cc;
const red = (rgb >>> 16) & 0xff;
const green = (rgb >>> 8) & 0xff;
const blue = rgb & 0xff;
console.log(red, green, blue); // 51 102 204JavaScript BigInt Bitwise Operators
BigInt supports &, |, ^, ~, <<, and >> without the Number operators' fixed 32-bit width. For binary bitwise operations, both operands must have the same numeric type, including the shift count.
| Behavior | Number operands | BigInt operands |
|---|---|---|
| Integer width | 32 bits for bitwise operations | Arbitrary width |
| Result type | Number | BigInt |
| Shift distance | Reduced modulo 32 | Not reduced modulo 32 |
| Unsigned right shift | >>> is supported | >>> throws TypeError |
BigInt has no fixed highest bit to zero-fill, so >>> isn't defined for it. Its complement also isn't confined to a chosen field width: ~0n is -1n.
console.log(1 << 40); // 256: Number shift count becomes 8
console.log(1n << 40n); // 1099511627776n
console.log(~2n); // -3n
try {
console.log(1n & 1);
} catch (error) {
if (!(error instanceof TypeError)) throw error;
console.log(error.name); // TypeError
}Choose BigInt when your mask or packed integer needs more than 32 bits. Keep the whole calculation in BigInt rather than converting values back and forth.
Common Mistakes
Using OR as a flooring shortcut
| 0 performs signed 32-bit conversion. Within range, it truncates toward zero rather than flooring toward negative infinity; outside that range, it can wrap. Use Math.trunc() to discard a fraction and Math.floor() to round downward.
console.log(5.23 | 0); // 5
console.log(-5.23 | 0); // -5
console.log(Math.trunc(-5.23)); // -5
console.log(Math.floor(-5.23)); // -6
console.log(2 ** 32 | 0); // 0
console.log(Math.trunc(2 ** 32)); // 4294967296Using NOT to hide an array membership check
The legacy expression !!~arr.indexOf(value) relies on NOT turning the absence marker −1 into zero. Prefer includes() for membership, or compare an index explicitly when you need its position. Unlike indexOf(), includes() can also find NaN.
const values = ["read", "write"];
console.log(values.includes("write")); // true
console.log(values.indexOf("write") !== -1); // trueSee JavaScript array methods for more readable collection operations.
Swapping variables with XOR assignments
A Number-based XOR swap coerces the values to signed 32-bit integers, so it doesn't preserve arbitrary JavaScript values. Destructuring expresses the actual intent without numeric coercion.
let a = 5.23;
let b = "ready";
[a, b] = [b, a];
console.log(a, b); // ready 5.23Assuming the rules transfer unchanged between languages
Python, Java, and C++ also support bitwise operators. Learn the bit-level idea once, but check each language's integer types, conversion rules, and shift behavior rather than assuming JavaScript's rules apply everywhere.
Performance: Benchmark the Actual Workload
The original claim that bitwise operators are generally faster than arithmetic was misleading. There's no general speed advantage you should assume; replacing multiplication with a shift also changes the numeric semantics.
If performance motivates a change, first verify equivalent results for your supported inputs, then benchmark representative work in your target runtimes. Tiny synthetic loops can misrepresent application performance—a problem the V8 team explains in its discussion of real-world measurement. For broader optimization work, see JavaScript performance optimization.
FAQ
Why do JavaScript bitwise operators use 32-bit integers?
The language specifies 32-bit integer conversions for Number bitwise operations, even though Number itself uses binary64 floating-point values. BigInt bitwise operations don't have that fixed 32-bit width.
What is the difference between signed and zero-fill right shift in JavaScript?
With Number operands, >> copies the sign bit into the opened positions and returns a signed result. The >>> operator inserts zeroes and returns an unsigned result: -8 >> 2 is -2, while -8 >>> 2 is 1073741822.
When should I use BigInt bitwise operators?
Use BigInt when your masks or integer fields need more than 32 bits. Keep both operands, including shift counts, as BigInt values; mixing Number and BigInt throws TypeError, and BigInt doesn't support >>>.
Are JavaScript bitwise operators faster than arithmetic?
Not as a general rule. Bitwise operators change Number values into a 32-bit integer domain, so verify equivalent behavior and benchmark your actual workload before treating them as an optimization.
How do I set, test, and clear a bit flag in JavaScript?
For a single-bit flag, use flags |= FLAG to set it, (flags & FLAG) !== 0 to test it, and flags &= ~FLAG to clear it. Use flags ^= FLAG to toggle it, and parenthesize the bitwise expression before a comparison.
Further Reading
- MDN: Expressions and operators — the complete operator set and compound assignments.
- ECMAScript: Numeric conversions — the precise ToInt32 and ToUint32 rules.
- MDN: Number — floating-point representation and precision.
- MDN: BigInt — supported operators and numeric-type boundaries.
- MDN: Operator precedence — grouping bitwise operations and comparisons correctly.
- MDN: Math.trunc() — truncation without bitwise wraparound.
- MDN: Array.prototype.includes() — explicit array membership checks.
- V8: Measuring real-world performance — why representative workloads matter.
