Skip to main content
This book is currently a work in progress. Content is being actively developed.

Chapter 2: Primitive Behaviors

So far, we’ve explored seven built-in primitive value types in JS: null, undefined, boolean, string, number, bigint, and symbol. Chapter 1 was quite a lot to take in. Once you’re ready to move on, let’s dig into certain behaviors implied by value types for all their respective values.

Primitive Immutability

All primitive values are immutable, meaning nothing in a JS program can reach into the contents of the value and modify it in any way.
The myAge = 43 statement doesn’t change the value 42. It reassigns a different value 43 to myAge, completely replacing the previous value. New values are also created through various operations:
Even a string value is immutable:
In non-strict mode, assigning to a read-only property (like greeting[5]) silently fails. In strict-mode, the disallowed assignment will throw an exception.
The nature of primitive values being immutable is NOT affected by how the variable is declared (const, let, or var). const doesn’t create immutable values — it declares variables that cannot be reassigned.

Primitives With Properties?

Properties cannot be added to any primitive values:
However, properties CAN be accessed on all non-nullish primitive values. For example, all string values have a read-only length property:
As mentioned in Chapter 1, these property/method accesses on primitive values are facilitated by an implicit coercive behavior called auto-boxing. We’ll cover this in detail in “Automatic Objects” in Chapter 3.

Primitive Assignments

Any assignment of a primitive value from one variable/container to another is a value-copy:
Here, the myAge and yourAge variables each have their own copy of the number value 42. If we later reassign myAge:

String Behaviors

String values have a number of specific behaviors that every JS developer should be aware of.

String Character Access

Though strings are not actually arrays, JS allows [ ] array-style access of a character at a numeric (0-based) index:

Character Iteration

Strings are iterables, which means the characters (code-units) can be iterated individually:
Values like strings and arrays are iterables because they expose an iterator-producing method at the special symbol property location Symbol.iterator:

Length Computation

The reported length value somewhat corresponds to the number of characters in the string, but it’s more complex when Unicode characters are involved.
Most standard characters: one character = one code-point = one code-unit:
Characters like "é" can be stored as composed or decomposed:
Extended Unicode characters above code-point 65535 need surrogate pairs:
Multiple code-points can cluster into single visual symbols:
Counting the length of a string to match our human intuitions is remarkably challenging. Many emoji and international characters don’t count as you’d expect. Libraries exist for handling some of this logic, but they’re often large and not necessarily perfect.

Internationalization (i18n) and Localization (l10n)

JS programs can operate in any international language/culture context using the ECMAScript Internationalization API.
1

Locale-Aware Sorting

Use Intl.Collator for locale-specific string comparison:
2

Right-to-Left (RTL) Languages

Languages like Hebrew and Arabic use RTL ordering:
Index-positional access follows logical position, not rendered position.
3

Word Segmentation

Use Intl.Segmenter to segment multi-word strings:

String Comparison

String values can be compared for both equality and relational ordering.

String Equality

The === and == operators are the most common way to compare strings:
The === operator (strict equality) first checks if the types match. If they do, it checks if the values are the same via per-code-unit comparison.The == operator (coercive equality) performs type coercion if the types don’t match. If both operands are already strings, == just hands off to ===.
Really Strict Equality
In addition to == and ===, JS provides Object.is():
I half-jokingly think of Object.is() as a ==== (fourth = added) operator, for the really-truly-strict-no-exceptions kind of equality checking!For strings, === is extremely predictable with no weird exceptions. I recommend using == or === for string checks, and reserve Object.is() for corner cases with numbers.

String Relational Comparisons

The <, <=, >, and >= operators compare strings lexicographically (like dictionary order):
These operators are always coercive when types don’t match. The only way to do a relational comparison with strings is to ensure both operands are already string values.
Be careful with numeric-looking strings:
Numerically, 100 is greater than 11. But in lexicographic ordering, the second "0" character (in "100") comes before the second "1" (in "11").
Locale-Aware Relational Comparisons
Use localeCompare() for locale-specific comparisons:
Using with array sorting:

String Concatenation

Two or more string values can be concatenated using the + operator:
If one operand is a string and the other is not, the non-string is coerced to its string representation:
Template literals are generally more preferred for string interpolation:

String Value Methods

String values provide a variety of methods:
  • charAt(index) - Returns character at index
  • at(index) - Like charAt, but supports negative indices
  • charCodeAt(index) - Returns numeric code-unit
  • codePointAt(index) - Returns whole code-point
  • substr(start, length) - Extract substring (deprecated)
  • substring(start, end) - Extract substring
  • slice(start, end) - Extract substring (supports negative indices)
  • toUpperCase() - Convert to uppercase
  • toLowerCase() - Convert to lowercase
  • toLocaleUpperCase() - Locale-aware uppercase
  • toLocaleLowerCase() - Locale-aware lowercase
  • indexOf(searchString, position) - Find index of substring
  • lastIndexOf(searchString, position) - Find last index
  • includes(searchString, position) - Boolean check
  • search(regexp) - Search with regular expression
  • startsWith(searchString) - Check if starts with
  • endsWith(searchString) - Check if ends with
  • concat(...strings) - Concatenate strings
  • repeat(count) - Repeat string n times
  • trim() - Remove whitespace from both ends
  • trimStart() / trimEnd() - Remove whitespace from one end
  • padStart(length, padString) - Pad start to length
  • padEnd(length, padString) - Pad end to length
  • split(separator) - Split into array
  • match(regexp) - Match against regular expression
  • matchAll(regexp) - Get all matches
  • replace(pattern, replacement) - Replace occurrences
  • normalize(form) - Unicode normalization
  • localeCompare(compareString, locales, options) - Locale-aware comparison

Static String Helpers

The following utilities are provided directly on the String object:
  • String.fromCharCode(...codes) - Create string from code-units
  • String.fromCodePoint(...codePoints) - Create string from code-points
  • String.raw(template, ...substitutions) - Template tag for raw strings
Most values can be explicitly coerced to strings:

Number Behaviors

Numbers are used for a variety of tasks, but mostly for mathematical computations.

Floating Point Imprecision

One classic gotcha of any IEEE-754 number system — NOT UNIQUELY JS — is that not all operations and values can fit neatly into the IEEE-754 representations:
This behavior is NOT IN ANY WAY unique to JS. This is exactly how any IEEE-754 conforming programming language will work. The temptation to make fun of JS for 0.1 + 0.2 !== 0.3 is strong, but it’s completely bogus.

Epsilon Threshold

A common piece of advice uses the Number.EPSILON value:
This approach isn’t actually safe:
Number.EPSILON only works as an error threshold for certain small numbers. For other cases, it’s far too small and yields false negatives.
Better approaches:
  1. Avoid floating-point by scaling to integers (do math, then scale back for display)
  2. Use an arbitrary precision decimal library
  3. Do math in another environment that’s not based on IEEE-754

Numeric Comparison

Numeric Equality

Just like strings, equality comparisons for numbers use == / === or Object.is():
For coercive equality, if either operand is not a string, == prefers numeric comparison:
Two frustrating exceptions in numeric equality (both == and ===):
For these cases, use Object.is() or Number.isNaN():

Numeric Relational Comparisons

The relational operators work with numbers as expected:
Remember: like ==, the < and > operators are also coercive. Ensure both operands are numbers to avoid coercion.

Mathematical Operators

The basic arithmetic operators are +, -, *, /, ** (exponentiation), and % (modulo):
The + operator is overloaded: when one or both operands is a string, it performs string concatenation. Otherwise, it performs numeric addition.
All mathematical operators coerce non-number operands to numbers:
Unary + and - operators:

Increment and Decrement

The ++ and -- operators perform their operation and reassign:

Bitwise Operators

JS provides several bitwise operators that work on 32-bit signed integers:
  • & (AND), | (OR), ^ (XOR), ~ (NOT)
  • << (left shift), >> (sign-propagating right shift)
  • >>> (zero-fill right shift)
A common idiom uses bitwise OR to truncate decimals:
| 0 is truncation, NOT floor. The result agrees with Math.floor() on positive numbers, but differs on negative numbers, because floor rounds towards -Infinity.

Number Value Methods

Number values provide these methods:
  • toExponential(fractionDigits) - Scientific notation string
  • toFixed(digits) - Fixed decimal places
  • toPrecision(precision) - Significant digits
  • toLocaleString(locales, options) - Locale-aware string
The . can be ambiguous with number literals. Use whitespace or parentheses:

Static Number Properties

  • Number.EPSILON - Smallest difference between 1 and next value
  • Number.MIN_SAFE_INTEGER / Number.MAX_SAFE_INTEGER - Safe integer range
  • Number.MIN_VALUE / Number.MAX_VALUE - Representable value range
  • Number.NEGATIVE_INFINITY / Number.POSITIVE_INFINITY - Infinite values
  • Number.NaN - The special invalid number value

Static Number Helpers

  • Number.isFinite(value) - Check if finite
  • Number.isInteger(value) - Check if integer
  • Number.isSafeInteger(value) - Check if safe integer
  • Number.isNaN(value) - Check if NaN (bug-fixed version)
  • Number.parseFloat(string) - Parse float
  • Number.parseInt(string, radix) - Parse integer

Static Math Namespace

JS includes many mathematical constants and utilities on the Math namespace:
Math.random() is not cryptographically secure. For security-sensitive random number generation, use crypto.getRandomValues() instead.

BigInts and Numbers Don’t Mix

Values of number type and bigint type cannot mix in operations:
To convert between types:

Primitives Are Foundational

Over the last two chapters, we’ve dug deep into how primitive values behave in JS. The story doesn’t end here — in the next chapter, we’ll turn our attention to understanding JS’s object types.

Continue to Chapter 3

Learn about object values, arrays, functions, and how they differ from primitives