Methods on primitives
Learn how "abc".toUpperCase() works when strings are primitives, why autoboxing uses temporary wrapper objects, and why new String(), new Number(), and new Boolean() cause surprising bugs.
- 01Explain wrapper objectsName the short-lived object JavaScript makes so a string, number, boolean, symbol, or bigint can use a method.
- 02Predict autoboxingSay why a method works on a primitive, but your own property on a primitive is not saved.
- 03Avoid wrapper trapsSpot bugs from
new String,new Number, andnew Boolean, and get the primitive back out of a wrapper.
How can a string have methods?
In the Data types lesson, you met the primitive values. A primitive is a simple value that is not an object. There are seven kinds:
- strings, numbers, and booleans
- symbols and bigints
nullandundefined
In the Strings lesson, you used methods such as toUpperCase(). A method is a function that you call with a dot after a value. Methods normally belong to objects. So here is a puzzle: "abc" is not an object, so how can it have a method?
The answer is called autoboxing. It works in four steps:
- You ask a primitive for a method, like
"abc".toUpperCase. - JavaScript makes a short-lived object around the primitive. This object is called a wrapper object.
- The wrapper finds the method, and the method runs.
- JavaScript throws the wrapper away. The primitive is still just a plain value.
const word = "tea";
const loudWord = word.toUpperCase();
console.log(word);
console.log(loudWord);Line 1 saves the string "tea" in word. Line 2 calls toUpperCase() on it. A wrapper finds the method, and the method gives back a new string, "TEA". Line 2 saves that new string in loudWord. Line 3 prints tea, and line 4 prints TEA.
So the method did not change word. A string can never be changed: a string method always gives back a new string. The wrapper only helped find the method. It never went into word.
"tea" has no methods of its own. For one moment, JavaScript puts it in a wrapper object that can reach toUpperCase. The method gives back a new string, "TEA". Then the wrapper is thrown away, and word is still "tea".Think of a primitive as a phone with no buttons. When you want to use a method, a case with buttons snaps onto the phone. You press one button. Then the case comes off again, and you have the same plain phone.
- In real life: A phone with no buttons
- In JavaScript: The primitive string
"tea" - In real life: A case snapped on for a moment
- In JavaScript: A short-lived
Stringwrapper object - In real life: A button on the case
- In JavaScript: A method such as
toUpperCase - In real life: Taking the case off again
- In JavaScript: JavaScript throws the wrapper away
Autoboxing means JavaScript puts a primitive inside a short-lived wrapper object, so it can find a method or property. For "abc".toUpperCase(), the wrapper is a String object, and it finds toUpperCase. Then the wrapper is thrown away.
Wrapper objects
COMPAREFive kinds of primitive have a wrapper. Each kind has a function with the same name and a capital letter:
Stringfor stringsNumberfor numbersBooleanfor booleansSymbolfor symbolsBigIntfor bigints
Each of these functions has a prototype. A prototype is an object that holds the methods shared by all values of one kind. For example, String.prototype holds toUpperCase, trim, and the other string methods. A later lesson shows how JavaScript searches a prototype. For now, remember one thing: the methods live there.
You can also make a wrapper object yourself with new, like new String("x"). That wrapper does not get thrown away. It is not the same thing as the primitive. The code below compares the two.
const primitive = "x";
const wrapped = new String("x");
console.log(typeof primitive, typeof wrapped);
console.log(wrapped.valueOf() === primitive);Line 1 saves the primitive string "x". Line 2 uses new String("x") to make a wrapper object that holds "x". Line 3 uses typeof, which gives the name of a value's type. It prints string object: the first value is a string, and the second is an object.
Line 4 calls valueOf(). This method gives back the primitive inside a wrapper. The result is "x", which equals primitive, so line 4 prints true.
valueOf() does not change the wrapper. It gives you a separate primitive value. wrapped is still an object.
const primitive = "x";const wrapper = new String("x");primitive: stringwrapper: objectprimitive: truewrapper: falseprimitive: truewrapper: trueprimitive: truewrapper: trueprimitive: "x"wrapper: "x"primitive: ["0"]wrapper: ["0"]primitive: "x"wrapper: "x"Pick a pair. Each row runs one check on the primitive and on the wrapper. typeof, ===, and truthiness see the wrapper as an object, even though valueOf() gives the same primitive back.
The table compares a primitive with a wrapper object. Read each row as a question, then compare the two answers.
| Question | Primitive | Wrapper object |
|---|---|---|
| What is it? | A plain value, such as "x", 5, or false. | An object made by new String, new Number, or new Boolean. |
What does typeof say? | "string", "number", or "boolean". | Always "object". |
Does an if treat it as true? | It depends on the value. false is treated as false. | Yes, always. An if treats every object as true. |
| When should you use it? | Almost always. | Almost never in app code. |
Autoboxing: the wrapper is short-lived
STEP THROUGHA method call works on a primitive because a wrapper finds the method. But what if you try to save your own property on a primitive, like word.extra = 1? That can't work. A primitive has no place to keep a property, and the wrapper is thrown away right after the line runs.
What happens next depends on the mode the code runs in:
- In sloppy mode, JavaScript quietly ignores the write. Sloppy mode is the older set of rules. It is used when a script has no
"use strict"line at the top. - In strict mode, JavaScript throws a
TypeError. ATypeErroris an error that means a value was used in the wrong way.
The code examples on this site run in strict mode, so strict mode is what you'll usually see here.
Step through the method call first. Then see why a property saved on a primitive is lost. Switch modes to compare sloppy mode with strict mode.
script
const upper = word.toUpperCase();word.extra = 1;console.log(word.extra);Step through the replay. Line 1 saves the primitive "hi". Line 2 calls toUpperCase() and gets back a new string, "HI". Line 3 tries to save extra on word. In sloppy mode, the write is ignored, so line 4 prints undefined. In strict mode, the write throws a TypeError, so the code never reaches the read.
The replay follows the rules in the JavaScript specification, the document that says how JavaScript must work. Real JavaScript engines, the programs that run your code, usually skip making the wrapper to save time. Your code still gives the same results as if the wrapper was made.
const word = "tea";
const savedWord = new String("tea");
savedWord.note = "hot";
console.log(savedWord.note);
console.log(word.note);Line 1 saves the primitive string "tea" in word. Line 2 makes a real wrapper object with new String("tea"). Line 3 saves a note property on that object, so line 4 prints hot. Line 5 reads note from the primitive word. Nothing was ever saved there, so it prints undefined.
savedWord is a real object, so it can keep note. On the left, word is a primitive string. It has no place to keep a property, so word.note is undefined.Where methods live
STEP THROUGHA method is not copied onto each string. All strings share one toUpperCase function, kept in String.prototype. In the same way, all numbers share methods such as toFixed, kept in Number.prototype.
const first = "cat".toUpperCase;
const second = "dog".toUpperCase;
console.log(first === second);
console.log("cat".toUpperCase());Line 1 reads toUpperCase from "cat" but does not call it, because there are no call brackets (). It saves the function in first. Line 2 does the same for "dog" and saves the function in second. Line 3 checks whether both are the same function, and prints true.
Line 4 calls the method on "cat" and prints CAT. The function is shared, but each call works on its own string.
"cat" and "dog" don't each carry a copy of toUpperCase. Both reach the same one, kept in String.prototype. That is why first === second is true.Numbers need one extra trick. A number literal is a number written straight into the code, like 5. You can't write 5.toFixed(2), because JavaScript reads the dot as a decimal point, as in 5.5. That gives a SyntaxError, the error for code JavaScript can't read. Two ways work instead:
(5).toFixed(2): the brackets end the number first.5..toFixed(2): the first dot is the decimal point, and the second dot starts the method.
toFixed(2) gives back the number as a string with 2 digits after the decimal point, so both give "5.00".
const orderId = Symbol("order");
const seats = 3n;
console.log(typeof orderId.description, orderId.description);
console.log(seats.toString());Line 1 makes a symbol, a unique value often used as a hidden property name. Its description is "order". Line 2 makes a bigint, a whole number of any size, written with an n at the end.
Line 3 reads description, which is a string, so it prints string order. Line 4 calls toString(), which turns the bigint into the string "3", so it prints 3. Both kinds get a wrapper for method calls, even though you can't make them with new.
Methods live on prototypes. Step through two ways to call a number method on the number 5.
script
const fixedA = (5).toFixed(2);const fixedB = 5..toFixed(2);console.log(sameMethod);console.log(fixedA, fixedB);In the replay, line 1 checks that the method reached from "abc" is the same function as String.prototype.toUpperCase. Lines 2 and 3 use the two ways to call toFixed on 5.
null and undefined are the only two primitives with no wrapper. So reading any property on them, like null.toString or undefined.valueOf, throws a TypeError.
console.log(typeof null);
try {
null.toString();
} catch (error) {
console.log(error.name);
}
try {
undefined.valueOf();
} catch (error) {
console.log(error.name);
}Line 1 prints object. That is an old mistake in JavaScript that can no longer be fixed. null is not an object.
Lines 2 to 6 try null.toString(). try runs some code, and if that code throws an error, catch runs instead. Here catch prints the error's name, TypeError. Lines 7 to 11 do the same for undefined.valueOf() and print TypeError again.
Why new String() is a trap
SORTIn the Constructors & new lesson, you saw a first warning about wrappers. Here is the full rule:
String(x),Number(x), orBoolean(x)withoutnewconvertsxand gives back a primitive.- With
new, the same function makes a wrapper object that stays around. That object acts differently in comparisons and inifchecks.
A normal method call snaps the case on for one moment. new String() leaves the case on for good. Anyone who looks at the phone sees the case first.
- In real life: Snapping on a case for one call
- In JavaScript: Autoboxing for one method call
- In real life: Leaving the case on for good
- In JavaScript:
new String("x")keeps a wrapper object - In real life: Seeing the case first
- In JavaScript:
typeof,===, andifsee an object
const good = String(123); // "123" primitiveconst trap = new String("x"); // object wrapperconsole.log(typeof good);console.log(typeof trap);console.log(trap === "x");Line 1 calls String(123) without new and gets the primitive string "123". Line 2 calls new String("x") and gets a wrapper object. Line 3 prints string, and line 4 prints object.
Line 5 prints false. === is strict equality: it checks the type as well as the value. An object is never strictly equal to a string.
new, String(123) gives back a plain string. With new, new String("x") gives back an object that holds the string. Checks such as typeof see the object, not the string inside it.const text = String(123);
const wrappedText = new String(123);
console.log(typeof text, typeof wrappedText);
console.log(typeof Symbol(), typeof BigInt(1));Line 1 gets a primitive string from String(123). Line 2 gets a wrapper object from new String(123). So line 3 prints string object. Line 4 calls Symbol() and BigInt(1) without new. Both give back primitives, so it prints symbol bigint.
const boxed = Object("tea");
console.log(typeof boxed);
console.log(boxed.valueOf());Line 1 calls Object() with a primitive string. Object() makes a wrapper for it, just like new String would. So line 2 prints object. Line 3 calls valueOf(), gets the primitive string back, and prints tea.
The table lists five wrapper surprises. Read each row as: "When I write this, I get this result, and this is why it matters."
| When you write | What you get | Why it matters |
|---|---|---|
String(123) | The primitive string "123". | This is the right way to turn a value into a string. |
new String("x") | A wrapper object. | typeof says "object", and === "x" is false. |
new Boolean(false) | A wrapper object, which is truthy. | An if treats it as true, even though the value inside is false. |
new Symbol() | A TypeError. | Symbols get wrappers for method calls, but you can't make one with new. |
new BigInt(1) | A TypeError. | You can't make a bigint with new either. Write BigInt(1) instead. |
Sort each expression by what it gives you: a primitive or a wrapper object.
String("a")new String("a")Object("a")"a".valueOf()Number("5")new Number(5)
Use the rule: a converter without new gives a primitive. new and Object(...) make a wrapper object.
Where you’ll use this
In real apps, use primitives. To convert a value, call String, Number, or Boolean without new. Then type checks, comparisons, JSON, and if checks all work the way you expect.
const rawId = " Asha-7 ";
const id = String(rawId).trim().toLowerCase();
const enabled = Boolean("");
console.log(id);
console.log(enabled);Line 1 is an ID typed into a form, with extra spaces. Line 2 turns it into a string, removes the spaces at both ends with trim(), and makes it lowercase with toLowerCase(). Line 3 turns an empty string into a boolean, false.
Line 4 prints asha-7, and line 5 prints false. Now the rest of the app gets plain primitives that are easy to check.
const oldId = new String(" SAVE ");
const id = oldId.valueOf().trim().toLowerCase();
const price = Number("12.50");
console.log(id, typeof id);
console.log(price, typeof price);Line 1 stands for a wrapper object that came from older code. Line 2 calls valueOf() to get the primitive string out, then trims it and makes it lowercase. Line 3 converts the text "12.50" with Number, without new. Line 4 prints save string, and line 5 prints 12.5 number.
console.log(Boolean("false"));
console.log(Boolean(""));Boolean() only asks whether a value is truthy, which means an if would treat it as true. Every string with at least one character is truthy, even "false". So line 1 prints true. Only the empty string "" is falsy, so line 2 prints false.
If a form sends the word "false", check the text itself instead, for example text === "true".
function normalizeId(id) { return String(id).trim().toLowerCase();} function isEnabled(flag) { return Boolean(flag);}- Line 2:
String(id)turns a number ID or a string ID into a string, sotrim()always works. - Line 6:
Boolean(flag)gives back the primitivetrueorfalse. - If a wrapper comes in from old code,
wrapper.valueOf()gets the primitive out. - Don't use
Boolean(text)to read words such as"false". Check the text itself.
Common misconceptions
“Primitives are secretly objects.”
No. They stay plain values. Autoboxing only makes a short-lived wrapper to find a method.
“If a method works, my own property should stay too.”
No. Finding a method and saving a property are different jobs. The wrapper is thrown away, so nothing is saved.
“Loose equality makes wrappers safe.”
new String("x") == "x" is true, because == unwraps the object. But ===, typeof, and if still see an object.
“new Boolean(false) acts like false.”
No. It is an object, and an if treats every object as true. That makes Boolean wrappers very risky in if checks.
“Symbol and BigInt work with new because they have wrappers.”
No. They get a wrapper for method calls, but new Symbol() and new BigInt(1) throw a TypeError.
Practice: wrappers and primitives
5 EXERCISESPredict both printed lines, then check your answer.
console.log(typeof "hi");
console.log(Boolean(new Boolean(false)));The first line prints string. The second line prints true, because new Boolean(false) is an object, and every object is truthy.
In sloppy mode, const s = "hi"; s.extra = 1; s.extra; does not keep extra. What value does s.extra give back?
It reads back as undefined. The wrapper made for the write is thrown away, so extra was never saved anywhere.
Write two working calls that show the number 5 with two digits after the decimal point.
console.log((5).toFixed(2));
console.log(5..toFixed(2));console.log((5).toFixed(2));
console.log(5..toFixed(2));Both lines call the number method and give back the string 5.00. Don't write 5.toFixed(2): that is a SyntaxError.
This code runs the wrong branch and prints delete account. What primitive value should replace new Boolean(false)?
const enabled = new Boolean(false);
if (enabled) {
console.log("delete account");
} else {
console.log("stay safe");
}const enabled = false;
if (enabled) {
console.log("delete account");
} else {
console.log("stay safe");
}With the primitive false, the if skips the risky branch and prints stay safe.
Use valueOf() to get the primitive string out of a wrapper object.
const wrapped = new String("save");
const plain = wrapped.valueOf();
console.log(typeof plain, plain === "save");const wrapped = new String("save");
const plain = wrapped.valueOf();
console.log(typeof plain, plain === "save");valueOf() gives back the primitive string inside the wrapper. So typeof plain is string, and plain === "save" is true.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8What is a wrapper object?
Choose an answer to see the explanation.
Question 2 of 8In sloppy mode, what does this code print?
Read the code, then predictconst s = "hi"; s.extra = 1; console.log(s.extra);Choose an answer to see the explanation.
Question 3 of 8In strict mode, what happens when you save a property on a primitive string?
Choose an answer to see the explanation.
Question 4 of 8What does this
new Stringcode print?Read the code, then predictconsole.log(typeof new String("x"), new String("x") === "x");Choose an answer to see the explanation.
Question 5 of 8What does this
ifprint?Read the code, then predictif (new Boolean(false)) { console.log("runs"); } else { console.log("skips"); }Choose an answer to see the explanation.
Question 6 of 8Which way of calling a method on the number 5 is a SyntaxError?
Choose an answer to see the explanation.
Question 7 of 8Which call converts a value and gives back a primitive, not a wrapper?
Choose an answer to see the explanation.
Question 8 of 8Which values have no wrapper at all?
Choose an answer to see the explanation.
Key takeaways
- Primitives are not objects. To find a method, JavaScript puts most of them in a short-lived wrapper object.
- Methods live on prototypes such as
String.prototypeandNumber.prototype, shared by every value of that kind. - Your own properties on a primitive are not saved. Sloppy mode ignores the write; strict mode throws a
TypeError. - Don't use
new String(),new Number(), ornew Boolean(). To convert, call them withoutnew. nullandundefinedhave no wrapper, so reading a property on them throws.
One-line definition: a primitive can use methods because JavaScript puts it in a short-lived wrapper object to find the method, then throws the wrapper away.
Up next: Type coercion.