Built-in prototypes
Learn where JavaScript’s built-in methods live, how arrays and functions inherit them, how to borrow methods safely, and why polyfills must be careful.
- 01Follow a built-in prototype chainUse
Object.getPrototypeOfto follow the chain, and find where a method lives. - 02Borrow a method safelyUse
callto run an array or object method on another object, without changing any built-in. - 03Add missing methods with careExplain why adding methods to built-ins is risky, and how a careful polyfill differs from a ponyfill.
Built-in methods live on shared prototypes
Arrays, strings, and functions come with ready-made methods. For example, every array has map and join. We call these built-in methods, because JavaScript gives them to you.
JavaScript does not copy these methods into every array. It keeps one copy of each method on a shared object, called a built-in prototype. For arrays, that shared object is Array.prototype.
You met prototypes in the lesson The prototype chain. Here is a quick reminder:
- Every object has a hidden link to one other object. That other object is its prototype.
- When you read a property the object doesn't have, JavaScript follows the link and looks in the prototype.
- If the property isn't there either, JavaScript follows the next link, and so on. This line of links is the prototype chain.
In this lesson you will meet Object.prototype, Array.prototype, and Function.prototype. You will also learn three things to do with them: borrow a method, leave them unchanged, and add a missing method safely.
Think of a town library. You don't buy your own copy of every book. When you need one, you borrow it from the shared shelf.
An array works the same way. It doesn't carry its own copy of map. When it needs map, it uses the one on the shared shelf, Array.prototype. Most values can also reach the main shelf, Object.prototype.
- In real life: A shared library shelf
- In JavaScript: A built-in prototype, such as
Array.prototype - In real life: Anyone in town can borrow a book from it
- In JavaScript: Every array can use
map,join, and the other array methods - In real life: The main shelf that every reader can reach
- In JavaScript:
Object.prototype, near the end of most chains
Object.prototype: at the end of most chains
INTERACTIVEFollow the prototype chain of almost any object, and you reach Object.prototype. After it comes null, which means the chain has ended.
So plain objects, arrays, functions, maps, and dates can all use the methods on Object.prototype. Here are three of them:
obj.hasOwnProperty(key)tells you ifkeyis an own property ofobj. An own property is stored on the object itself, not found through the chain.a.isPrototypeOf(b)tells you ifais somewhere in the chain ofb.obj.toString()turns the object into text.
Newer code often writes Object.hasOwn(obj, key) instead of obj.hasOwnProperty(key). It does the same check. But it lives on Object itself, so it still works when an object has no hasOwnProperty method, or has its own property with that name.
The playground below uses Object.getPrototypeOf(x). It gives you the prototype of x: one step up the chain. Pick a value. The playground keeps taking one step up until it reaches null.
Line 2 uses Object(value). For a string or a number, this makes a wrapper object (explained just below), so the walk has an object to start from. For an object, it gives back the same object.
const value = [];let current = Object(value);while (current !== null) { current = Object.getPrototypeOf(current);}- 1. Array.prototype
at, concat, constructor, copyWithin, entries, every, fill, filter - 2. Object.prototype
__defineGetter__, __defineSetter__, __lookupGetter__, __lookupSetter__, constructor, hasOwnProperty, isPrototypeOf, propertyIsEnumerable - 3. null
end of the chain
JavaScript starts at []. Object.getPrototypeOf gives the next prototype, one step at a time, until it reaches null.
A string like "abc" is a primitive: a simple value, not an object. Still, "abc".toUpperCase() works.
When you call a method on a string, JavaScript wraps it in an object for a moment. That wrapper object links to String.prototype, where toUpperCase lives. The lesson Methods on primitives covers this in detail.
const word = "tea";
console.log(word.toUpperCase());
console.log(Object.hasOwn(Object(word), "toUpperCase"));
console.log(Object.getPrototypeOf(Object(word)) === String.prototype);- Line 2 prints
"TEA". JavaScript wraps the string for a moment and findstoUpperCaseonString.prototype. - Line 3 makes the wrapper on purpose, with
Object(word). Then it asks if the wrapper ownstoUpperCase. It printsfalse: the wrapper doesn't store the method itself. - Line 4 prints
true. The wrapper's prototype isString.prototype.
The wrapper lasts for just that one step. word stays a plain string. That is why you can't add your own lasting properties to a string.
Array.prototype and Function.prototype
STEP THROUGHAn array stores its own items and its length. It does not store map, filter, join, or toString. Those live on Array.prototype.
Functions work the same way. A function does not store call, apply, or bind. It gets them from Function.prototype.
list has no map of its own, so JavaScript follows the link to Array.prototype and finds it there. It stops looking as soon as it finds it.Step through the code below and watch where each method comes from. Line 5 uses .call(list). It runs Object.prototype.toString with list as its this. (this is the object a method works on.)
Guess where each method comes from. Then step through and watch JavaScript look on the array first, then on its prototype.
script
const ownMap = Object.hasOwn(list, "map");const mapped = list.map((n) => n * 10);const arrayText = list.toString();const objectText = Object.prototype.toString.call(list);Lines 4 and 5 use two different toString methods:
- Line 4: JavaScript finds
toStringonArray.prototypefirst. That one joins the items, so you get"1,2". - Line 5: you pick
Object.prototype.toStringon purpose. That one gives a type label,"[object Array]".
When two objects in a chain have a property with the same name, JavaScript uses the one it reaches first. We say the nearer one shadows (hides) the other.
const list = [1, 2];
list.map = "later";
console.log(Object.hasOwn(list, "map"));
console.log(typeof Array.prototype.map);- Line 2 stores a string called
mapdirectly on this one array. - Line 3 prints
true: the array now owns amapproperty. - Line 4 prints
"function": the sharedArray.prototype.mapis still there.
But JavaScript finds the array's own map first. So list.map() would now fail, because list.map is a string, not a function.
This helps when you see an error like list.map is not a function. First check Object.hasOwn(list, "map"). If it is true, some code stored a property called map on the array. Delete that property, and the shared method works again.
const scores = { 0: 4, 1: 7, length: 2 };
const doubled = Array.prototype.map.call(scores, (score) => score * 2);
console.log(doubled.join(","));scores is not an array. It is a plain object with number keys (0 and 1) and a length. We call an object like this array-like.
Line 2 runs Array.prototype.map on scores with .call. .call(x, …) runs a function with x as its this. map only needs number keys and a length, so it works. It returns a real array, [8, 14], and line 3 prints "8,14".
function greet(name) { return "Hi, " + name;}const called = greet.call(null, "Ada");const bound = greet.bind(null, "Lin");const later = bound();Line 4 uses greet.call, so called becomes "Hi, Ada". Line 5 uses greet.bind to make a new function, and line 6 calls it, so later becomes "Hi, Lin".
greet doesn't own call or bind. JavaScript finds both on Function.prototype. The lesson call, apply & bind shows how to use them.
The table lists common built-in prototypes. Each row says what the prototype holds and which values use it.
| Built-in prototype | Some methods it holds | Which values use it |
|---|---|---|
Object.prototype | toString, hasOwnProperty, and isPrototypeOf. | Almost all objects: plain objects, arrays, functions, dates, and maps. |
Array.prototype | map, filter, join, at, and toString. | Arrays, such as [1, 2]. |
Function.prototype | call, apply, bind, and toString. | All functions, including classes and arrow functions. |
String.prototype | toUpperCase, slice, and includes. | Strings, through a short-lived wrapper object. |
Map.prototype | get, set, has, and entries. | Maps made with new Map(). |
Borrowing a method
STEP THROUGHTo borrow a method means:
- Take a method from a built-in prototype.
- Run it once on a different object, with
.call. - Leave the prototype as it was.
It's like borrowing a library book for a school project. You use it and give it back. You don't write in it.
.call runs join one time, with letters as its this. Nothing is added to letters, and Array.prototype stays the same.Borrowing only works if the method can handle your object. join needs just number keys and a length, so it works on array-like objects. map does too.
If you need many array methods, Array.from(obj) is often clearer. It makes a real array with the same items. Try a different separator, then step through.
Change the separator. Then step through and watch one borrowed method work on an object that isn't an array.
script
const joined = Array.prototype.join.call(letters, "-");const copied = Array.from(letters);const label = Object.prototype.toString.call(copied);Line 4 borrows another method: Object.prototype.toString.call(value). It gives a type label, such as [object Array]. The lesson Checking types reliably uses this. It isn't perfect: an object can change its own label with Symbol.toStringTag.
Not every method can be borrowed by any object. For example, Map.prototype.get needs a real map. A map keeps its items in a hidden place that plain objects don't have. So Map.prototype.get.call({}, "a") throws a TypeError.
One more catch. Each page has its own set of built-ins, and so does each iframe (a page shown inside another page). One full set of built-ins is called a realm.
An array made inside an iframe links to the iframe's Array.prototype, not the page's. So in the page, arr instanceof Array gives false, because instanceof Array looks for the page's Array.prototype in the chain. Array.isArray(arr) works in every realm.
Why you shouldn't change built-in prototypes
SAFE SANDBOXYou can add your own method to a built-in prototype. For example, an app might add Array.prototype.last. But there is only one Array.prototype in a page. So every array in the page gets last, including the arrays inside libraries you use.
That causes three problems:
- A plain assignment makes the method enumerable, which means loops like
for…inlist it. Afor…inloop over an array then showslastnext to the index numbers. - Another library may add its own
lastthat works differently. One overwrites the other. - A future version of JavaScript may add a real
lastmethod that works differently from yours.
Your own helper function is like notes in your own notebook. Only you read them.
Adding a method to a built-in prototype is like writing in a library book. Your note may help you, but everyone who borrows the book reads it too.
- In real life: Writing notes in a library book
- In JavaScript: Adding your own method to a built-in prototype
- In real life: Every reader of the book sees your notes
- In JavaScript: Every value that uses that prototype gets the method
- In real life: A new edition may print something else on that page
- In JavaScript: A future JavaScript version may add a method with the same name
Array.prototype in a page. Line 1 adds last() to it, so every array gets it, even arrays inside libraries you use.The demo below runs this risky code safely, inside a hidden iframe. The iframe has its own built-ins, so this page is not changed. Line 1 adds last. The loop on line 6 then lists 0, 1, last. Line 11 removes last again.
Array.prototype.last = function () { return this[this.length - 1];}; const keys = [];for (const key in ["red", "blue"]) { keys.push(key);}console.log(keys.join(", ")); delete Array.prototype.last;Click Run. This page's own prototypes stay unchanged.
The iframe has its own built-ins. Line 1 adds a last method to the iframe's Array.prototype, so the for…in loop lists it. Line 11 removes it again.
A realm is one full set of built-ins. This page and the demo's iframe each have their own Array.prototype. So a change inside the iframe doesn't reach this page.
But a change in your page reaches every script in that page, including every library.
In 2018, JavaScript planned a new array method called flatten. But an old library, MooTools, had already added its own flatten to Array.prototype. Websites that used MooTools would have broken. So the new method was renamed to flat.
Years before, the array method contains was renamed to includes for the same kind of reason, also because of MooTools code. Names on built-in prototypes are shared by the whole web.
Polyfills: adding a missing method carefully
CODEOld browsers lack some newer methods. A polyfill is code that adds a missing standard method, so an old browser works like a new one. "Standard" means the method is part of the official JavaScript rules.
A careful polyfill does three things:
- It checks first if the method is missing. This is called feature detection. If the method is already there, the polyfill does nothing.
- It adds the method with the same settings as real built-in methods. For example, it is not enumerable.
- It handles the same special cases as the real method.
Check for the method itself, not for the browser's name. Two versions of the same browser can support different methods.
A full polyfill is hard to write. It must handle many special cases, such as odd inputs and very old engines. Real projects use tested packages such as core-js, added by build tools like Babel. The later lesson Transpilers & polyfills shows how.
In your own app code, a ponyfill is often safer. It is a plain function, such as at(array, index). It does the same job, but it doesn't touch any built-in prototype.
Imagine an old copy of a book with page 42 missing. You copy page 42 from a newer edition and tape it in.
You only do this if the page is missing. If page 42 is already there, you leave the book alone.
- In real life: An old book with page 42 missing
- In JavaScript: An old browser without
Array.prototype.at - In real life: Tape in a copy only if page 42 is missing
- In JavaScript: Check first, then add the method only if it is missing
- In real life: A separate sheet with page 42 on it
- In JavaScript: A ponyfill: a plain function such as
at(array, index)
at() only where it is missing, and never replaces a real built-in method.Here is a polyfill for at. array.at(i) gives the item at index i, and a negative i counts from the end.
- Line 1 checks if
Array.prototype.atis missing. - Line 2 uses
Object.defineProperty. It adds a property and sets its settings in one step. - Lines 3 to 8 are the method itself.
- Lines 9 to 11 set the settings: writable, configurable, and not enumerable.
at that checks firstif (!Array.prototype.at) { Object.defineProperty(Array.prototype, "at", { value(index) { const length = this.length; const integer = Math.trunc(index) || 0; const position = integer < 0 ? length + integer : integer; return this[position]; }, writable: true, configurable: true, enumerable: false, });}function at(array, index) { const length = array.length; const integer = Math.trunc(index) || 0; const position = integer < 0 ? length + integer : integer; return array[position];}This at does the same work as the polyfill, as a plain function. You call it as at(list, -1). No built-in prototype changes.
Every property has three settings, called flags:
enumerable: loops likefor…inlist the property.writable: you can give the property a new value.configurable: you can delete the property or change its flags.
Object.getOwnPropertyDescriptor(obj, key) shows the flags of one property. Here it checks the real map method.
const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, "map");
console.log(descriptor.enumerable);
console.log(descriptor.writable);
console.log(descriptor.configurable);It prints false, true, and true. So map is hidden from loops, but it can still be replaced or deleted.
- Borrow
Array.prototype.joinwithcallfor one object - Add
debugtoObject.prototype - A tested polyfill package that checks first and hides the method from loops
- A separate
at(array, index)function - Replace
console.logfor the whole page - Add
last()toArray.prototypein an app
Sort each practice: is it usually fine in app code, or risky?
Where you'll use this
You'll use what you learned here when you:
- fix an error like
x.map is not a function, - check if a property belongs to the object or comes from its prototype,
- run an array method on an array-like object,
- read older code that changes built-in prototypes,
- decide whether to add a polyfill yourself or let your build tools do it.
const cards = { 0: "Buy milk", 1: "Send email", length: 2 };
const todo = Array.from(cards);
console.log(todo.map((card) => card.toLowerCase()).join(" | "));Line 2 uses Array.from to make a real array from cards. Line 3 can then use map and join as usual. It prints "buy milk | send email".
When should you copy with Array.from, and when should you borrow?
- Use
Array.fromwhen you'll call several array methods. Readers also see right away that you made a copy. - Borrow one method with
.callwhen you need just one step and the object clearly fits. - Neither one changes the prototype of the original object.
Array.from makes a new array, so it uses a little more memory. In most page code that doesn't matter. Pick the code that is easier to read.
Some short rules to keep:
- Use
Object.hasOwn(obj, key)to check if a property is the object's own. - Use
Array.from(nodeList)to get array methods on aNodeList(a list of page elements). - Borrow with
.callwhen a method fits your object and you need it once. - In app code, prefer ponyfills. Leave polyfills to tested packages.
Common misconceptions
"Every array gets its own copy of each method."
No. All arrays share one copy of each method, on Array.prototype. This saves memory, and every array behaves the same.
"Borrowing a method changes the prototype."
No. .call sets this for one call only. Your object's prototype stays the same, and so does the prototype you borrowed from.
"Any method added to a built-in prototype is a polyfill."
No. A polyfill adds a missing standard method. Adding your own made-up method is called monkey-patching.
"A method hidden from loops is safe to add."
Not quite. Making it not enumerable fixes the for…in problem. But the name can still clash with a library's method or a future JavaScript method.
"Every object has Object.prototype in its chain."
No. Object.create(null) makes an object with no prototype at all. This is handy for a simple list of keys and values. But such an object has no toString or hasOwnProperty to use.
const settings = Object.create(null);
settings.theme = "dark";
console.log(Object.getPrototypeOf(settings));
console.log(Object.hasOwn(settings, "theme"));
console.log("hasOwnProperty" in settings);- Line 1 makes an object with no prototype.
- Line 3 prints
null: there is no prototype. - Line 4 prints
true, becausesettingsownstheme. - Line 5 prints
false. Theinoperator looks at the object and its whole chain, and there is nohasOwnPropertyanywhere.
Practice: built-in prototypes
5 EXERCISESPredict both lines this code prints.
const xs = [1, 2];
console.log(Object.hasOwn(xs, "map"));
console.log(xs.map((n) => n + 1).join("|"));xs doesn't own map, so line 2 prints false. Line 3 finds map on Array.prototype. It makes [2, 3], and join prints 2|3.
What does the borrowed method print?
const buttons = { 0: "Save", 1: "Cancel", length: 2 };
console.log(Array.prototype.join.call(buttons, " / "));const buttons = { 0: "Save", 1: "Cancel", length: 2 };
console.log(Array.prototype.join.call(buttons, " / "));Array.prototype.join.call runs join once, with buttons as its this. It joins index 0 and index 1 with / between them, so it prints Save / Cancel.
last(array)Write a plain function that returns the last item in an array. Don't change Array.prototype.
function last(array) {
return array[array.length - 1];
}
console.log(last(["red", "blue"]));function last(array) {
return array[array.length - 1];
}
console.log(last(["red", "blue"]));last is a plain function, so it is a ponyfill. It works on any array you pass in, and it changes no built-in prototype.
The code adds the polyfill, then reads the flags of at. What do the last two lines print?
if (!Array.prototype.at) {
Object.defineProperty(Array.prototype, "at", {
value(index) {
const length = this.length;
const integer = Math.trunc(index) || 0;
const position = integer < 0 ? length + integer : integer;
return this[position];
},
writable: true,
configurable: true,
enumerable: false,
});
}
const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, "at");
console.log(typeof descriptor.value);
console.log(descriptor.enumerable);if (!Array.prototype.at) {
Object.defineProperty(Array.prototype, "at", {
value(index) {
const length = this.length;
const integer = Math.trunc(index) || 0;
const position = integer < 0 ? length + integer : integer;
return this[position];
},
writable: true,
configurable: true,
enumerable: false,
});
}
const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, "at");
console.log(typeof descriptor.value);
console.log(descriptor.enumerable);It prints function and false. If at was missing, the polyfill added it with enumerable: false. If at was already there, the polyfill did nothing, and the real at has the same flag. Either way the output is the same.
JavaScript planned an array method called flatten, but the name clashed with old websites. What name was used instead?
The method was named flat, not flatten, so old MooTools websites kept working. Names on built-in prototypes are shared by the whole web.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7Where does
[1, 2].mapfindmap?Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst xs = [1, 2]; console.log(xs.toString());Choose an answer to see the explanation.
Question 3 of 7What type label does this print?
Read the code, then predictconsole.log(Object.prototype.toString.call([1, 2]));Choose an answer to see the explanation.
Question 4 of 7Why does
Array.prototype.join.call({0: 'a', length: 1}, '-')work?Choose an answer to see the explanation.
Question 5 of 7What is the biggest problem with adding your own methods to built-in prototypes?
Choose an answer to see the explanation.
Question 6 of 7What should a careful polyfill do first?
Choose an answer to see the explanation.
Question 7 of 7Which sentence about
Function.prototypeis true?Choose an answer to see the explanation.
Key takeaways
- Built-in methods live on shared prototype objects, not on each value.
Object.prototypeis near the end of most chains. Objects made withObject.create(null)don't have it.Array.prototypeholds the array methods.Function.prototypeholdscall,apply, andbind.- Borrowing a method with
.callchanges nothing. Adding a method to a built-in prototype changes it for the whole page. - A polyfill adds a missing standard method, carefully. A ponyfill is a plain function and changes no built-ins.
Remember the one-liner.
Built-in prototypes are shared shelves of methods: use them, borrow from them with care, and don't add your own methods to them.
Up next: Object.create & null-prototype objects.