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.
- 01Read real prototype chainsWalk built-in chains with
Object.getPrototypeOfand spot where methods live. - 02Borrow safelyUse
callwith array and object methods without changing built-ins. - 03Patch only with careExplain why extending built-ins is risky and how guarded polyfills differ from app ponyfills.
Built-in prototypes are the shared shelves
JavaScript gives arrays, functions, dates, maps, and ordinary objects useful methods without copying those methods onto every value. The methods live on built-in prototype objects. When code asks for a missing property, the lookup climbs the prototype chain you met in The prototype chain.
The curriculum summary for this lesson is short and important: built-in prototypes explain where array and string methods actually live. That includes Object.prototype, Array.prototype, Function.prototype, method borrowing, the danger of changing built-ins, and careful polyfills.
Imagine every array carrying a library card. It does not own a private copy of every book. When it needs map, it visits the shared Array.prototype shelf. Most values can also reach the central Object.prototype reference desk.
- In real life: A public library shelf
- In JavaScript: A built-in prototype such as
Array.prototype - In real life: Every resident can borrow a book
- In JavaScript: Every array can use
map,join, and friends - In real life: The central reference desk
- In JavaScript:
Object.prototype, near the end of most chains
Where the analogy stops: Books are borrowed physically. Prototype methods are just looked up and called; no method is copied onto the object.
Object.prototype: the common ancestor
INTERACTIVEFor most ordinary objects, the chain eventually reaches Object.prototype, then null. That is why objects, arrays, functions, maps, and dates can use old helpers such as hasOwnProperty, isPrototypeOf, and Object.prototype.toString. Modern code often prefers static helpers like Object.hasOwn(obj, key), because they work even when an object shadows or lacks hasOwnProperty.
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
Starting with [], JavaScript walks these prototype links with Object.getPrototypeOf until it reaches null.
The unpublished Methods on primitives lesson covers autoboxing: "abc".toUpperCase() works because JavaScript temporarily treats the primitive like a string wrapper whose methods live on String.prototype.
Array.prototype and Function.prototype
STEP THROUGHAn array instance usually owns its elements and length. It does not own map, filter, join, or toString. Those are on Array.prototype. A function object does not own call, apply, or bind; it inherits them from Function.prototype.
Predict where each method comes from. Step through the lookup from the array itself to its prototypes.
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);function greet(name) { return "Hi, " + name;}const called = greet.call(null, "Ada");const bound = greet.bind(null, "Lin");const later = bound();That code works because greet.call and greet.bind are found on Function.prototype. The published call, apply & bind lesson uses them to choose this and to borrow methods.
| Prototype | Common members | Who inherits from it |
|---|---|---|
Object.prototype | toString, hasOwnProperty, isPrototypeOf | Most ordinary objects, arrays, functions, dates, maps |
Array.prototype | map, filter, join, at, toString | Arrays such as [1, 2] |
Function.prototype | call, apply, bind, toString | Functions, classes, arrow functions |
String.prototype | toUpperCase, slice, includes | Temporary wrapper objects for string primitives |
Map.prototype | get, set, has, entries | Maps created with new Map() |
Borrowing methods
STEP THROUGHBorrowing a method means: take a function from one prototype, call it once with a different receiver, and leave the prototype alone. It is like borrowing a library book for your own project, not scribbling in the book. This works only when the method’s expectations match the receiver. join is flexible because it needs numeric properties and length. map can also work on array-likes, but Array.from is often clearer when you want a real array.
Change the separator, then step through how one borrowed method can work on an array-like object.
script
const joined = Array.prototype.join.call(letters, "-");const copied = Array.from(letters);const label = Object.prototype.toString.call(copied);Another practical borrowed method is Object.prototype.toString.call(value), the classic type-label trick discussed in Checking types reliably. It is not perfect—Symbol.toStringTag can customize it—but it explains why [1,2].toString() and Object.prototype.toString.call([1,2]) differ.
Why not to extend built-ins
SAFE SANDBOXChanging a built-in prototype changes a shared public shelf. If app code adds Array.prototype.last, every array in that realm can see it. If it is enumerable, for…in over arrays sees it too. Even non-enumerable additions can collide with future standards or with other libraries.
Adding a method to a built-in prototype is not like putting a helper in your own module. It is like writing in the town’s shared book. Your note may be helpful to you, but everyone else now reads it too.
- In real life: Writing notes in a public book
- In JavaScript: Adding app methods to built-in prototypes
- In real life: Everyone sees your notes
- In JavaScript: Every value using that prototype sees the method
- In real life: A future edition may use the same page
- In JavaScript: A future standard method name may collide
Where the analogy stops: A real librarian can remove one marked book. A modified prototype affects all code in that JavaScript realm until changed back.
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. The real page prototypes are not changed.
The iframe has its own built-ins. Adding an enumerable last method there makes for…in see it, then the snippet deletes it before reporting back.
The proposal that became Array.prototype.flat was renamed from flatten in 2018 for web compatibility after MooTools sites conflicted. Earlier, the array contains name was changed to includes after web compatibility problems, also involving MooTools-era code. The moral is stable: names on built-in prototypes are public territory.
Polyfills: missing pages, carefully taped in
CODEA polyfill implements a standard feature in an older environment that lacks it. A good polyfill first feature-detects, then defines the method with descriptors that match the standard shape: writable and configurable, but not enumerable. It also tries to match edge cases, which is why production teams use tested packages such as core-js through tooling like Babel. The later Transpilers & polyfills lesson covers that workflow.
A polyfill is not a random patch. It is a careful replacement for a standard page that is missing from an old edition. If the page is already there, leave it alone.
- In real life: Old book missing page 42
- In JavaScript: Old browser missing
Array.prototype.at - In real life: Tape in a copy only if page 42 is absent
- In JavaScript: Feature-detect before defining
- In real life: A separate handout
- In JavaScript: A ponyfill helper such as
at(array, index)
Where the analogy stops: Real pages do not have spec edge cases. Real polyfills must match details such as property descriptors, holes, coercion, and cross-browser bugs.
at polyfill shapeif (!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];}- Borrow
Array.prototype.joinwithcallfor one object - Add
debugtoObject.prototype - A guarded, non-enumerable polyfill from a compatibility library
- A separate
at(array, index)helper - Monkey-patch
console.logglobally - Add
last()toArray.prototypein an app
Sort each practice by whether it usually belongs in application code.
Where you’ll use this
You will use built-in prototype knowledge when debugging “is not a function”, checking whether a property is own or inherited, borrowing a method for an array-like value, reading older libraries, and deciding whether compatibility code belongs in your app or your build tooling.
- Use
Object.hasOwn(obj, key)to avoid inherited surprises. - Use
Array.from(nodeList)when you want array methods on DOM collections. - Borrow with
callwhen the method is generic and you want one precise call. - Prefer ponyfills in app code; leave broad polyfills to compatibility libraries.
Common misconceptions
“Methods are copied into every array.”
No. Arrays share methods through Array.prototype, which saves memory and keeps behavior consistent.
“Borrowing changes the borrowed-from prototype.”
No. call chooses this for one invocation. It does not relink prototypes.
“All prototype extension is a polyfill.”
A polyfill implements a standard missing feature. Adding your own convenience method is monkey-patching.
“If a method is non-enumerable, extending built-ins is safe.”
Non-enumerable avoids one loop problem, but collisions and semantic mismatches remain.
“Object.prototype is always present.”
Object.create(null) creates a null-prototype object. The next lesson, Object.create & null-prototype objects, explores that.
Practice: built-in prototypes
5 EXERCISESPredict both printed lines.
const xs = [1, 2];
console.log(Object.hasOwn(xs, "map"));
console.log(xs.map((n) => n + 1).join("|"));The array does not own map, so the first line is false. The inherited map returns [2, 3], and join prints 2|3.
Work out what the borrowed method prints.
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 treats buttons as its this value for one call, so it prints Save / Cancel.
last(array)Write a standalone function that returns the last item in an array.
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"]));This helper is a ponyfill-style function: it works with arrays you pass in and leaves every built-in prototype untouched.
What does the descriptor check 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);A careful polyfill feature-detects first and defines a non-enumerable method. In a vm context with this snippet, the descriptor’s value is a function and enumerable is false.
The proposed array method name flatten collided with existing web code. What name did the standard choose instead?
The standard method was named flat, not flatten, after web compatibility concerns around MooTools. It is a reminder that built-in prototype names are shared with the whole web.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7Where does
[1, 2].mapusually findmap?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 can
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 app 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 statement about
Function.prototypeis true?Choose an answer to see the explanation.
Key takeaways
- Built-in methods usually live on shared prototype objects, not on each value.
Object.prototypeis near the end of most chains, but null-prototype objects skip it.Array.prototypeprovides array methods;Function.prototypeprovidescall,apply, andbind.- Borrowing methods with
callis local; extending built-ins is global and risky. - Polyfills fill missing standard features carefully; ponyfills avoid changing built-ins.
Remember the one-liner.
Built-in prototypes are JavaScript’s shared method shelves; use them, borrow from them carefully, and avoid writing your own notes on them.
Up next: Object.create & null-prototype objects.