Native prototypes
The prototype property is not just something you use in your own constructor functions. The language itself is built on it. Every built-in constructor — Object, Array, Function, Date, and the rest — parks its methods on a shared prototype object, and every value you create points back at one of them.
Once you see this, a lot of mysterious behavior stops being mysterious. Why does an empty object already know how to turn itself into a string? Why can a plain string call .toUpperCase() when strings are not objects? The answers all lead to the same place. We’ll walk the chain first, then look at when it’s actually reasonable to reach in and modify these prototypes.
Object.prototype
Start with something that looks like it should do nothing:
let obj = {};
alert( obj ); // "[object Object]"
Passing obj to alert forces it to a string, and out comes "[object Object]". Some code produced that text. It’s a built-in toString method — but where does it live? The object is empty. It has no toString of its own.
The clue is that obj = {} is shorthand. Under the hood it means the same thing as obj = new Object(), where Object is a built-in constructor function. That function carries a prototype property, and Object.prototype is a large object holding toString along with a pile of other shared methods.
When new Object() runs (or when you write a literal {...}), the new object’s [[Prototype]] is wired to Object.prototype. This is the same rule from the previous chapter: the constructor’s prototype property becomes the created object’s hidden [[Prototype]] link.
So obj.toString() finds nothing on obj, climbs the [[Prototype]] link, and picks up toString from Object.prototype. You can confirm every link with equality checks:
let obj = {};
alert(obj.__proto__ === Object.prototype); // true
alert(obj.toString === obj.__proto__.toString); // true
alert(obj.toString === Object.prototype.toString); // true
Those === results say the identity matches: obj isn’t holding a copy of toString, it’s reading the exact same function object that sits on Object.prototype. That sharing is the whole point — millions of objects, one method in memory.
And the chain has a top. Nothing sits above Object.prototype:
alert(Object.prototype.__proto__); // null
That null is the signal that the search is over. When a lookup reaches Object.prototype and still hasn’t found the property, the next step is null, and the result is undefined.
Other built-in prototypes
The same design repeats for Array, Date, Function, and the others. Each keeps its methods on its own prototype.
Create an array with a literal and the engine uses new Array() internally, so the array’s [[Prototype]] becomes Array.prototype. That’s where map, filter, push, and friends actually live. None of them are copied onto individual arrays; they’re shared, which is why building thousands of arrays stays cheap.
There’s a second link worth noticing. By specification, Array.prototype itself inherits from Object.prototype. Same for Function.prototype, Number.prototype, and the rest. So every built-in chain eventually funnels into Object.prototype — which is the grain of truth behind the saying “everything inherits from objects.”
You can trace an array’s chain by hand:
let arr = [1, 2, 3];
// inherits from Array.prototype?
alert( arr.__proto__ === Array.prototype ); // true
// then from Object.prototype?
alert( arr.__proto__.__proto__ === Object.prototype ); // true
// and null at the top
alert( arr.__proto__.__proto__.__proto__ ); // null
Two hops to Object.prototype, one more to null. Every array walks that path.
Pick a value below and watch its [[Prototype]] chain get built one hop at a time. Notice that whatever you start with, the path always funnels into Object.prototype and then ends at null.
Sometimes a method name appears on more than one level of the chain. Array.prototype has its own toString that joins elements with commas, separate from the Object.prototype version:
let arr = [1, 2, 3];
alert(arr); // 1,2,3 <-- from Array.prototype.toString
Both toString methods exist, but the lookup stops at the first match. Array.prototype sits closer to the array than Object.prototype, so its toString wins. That’s ordinary shadowing — the nearer definition hides the farther one.
Functions follow the identical rule. A function is an object created by the built-in Function constructor, so its methods — call, apply, bind — come from Function.prototype. Functions carry their own toString too (the one that gives you the source text back).
function f() {}
alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true, inherits from objects
Primitives
Strings, numbers, and booleans are the tricky case, because they are not objects at all.
Yet this works:
alert( "hello".toUpperCase() ); // HELLO
A primitive string has no properties. So how does .toUpperCase() exist? When you access a property or method on a primitive, the engine wraps it, for that single operation, in a temporary object built by the matching constructor — String, Number, or Boolean. That wrapper carries the method (from String.prototype, Number.prototype, Boolean.prototype), the method runs, and the wrapper is thrown away.
These wrapper objects are created invisibly, and most engines optimize the actual allocation away for speed. But the specification describes the behavior exactly this way, and the mental model holds: primitive methods live on String.prototype, Number.prototype, and Boolean.prototype.
Type a plain string below and call methods on it. Each method comes from String.prototype, reached through a throwaway wrapper — yet the value you typed stays a primitive the whole time.
Changing native prototypes
Native prototypes are ordinary objects, and you can add to them. Extend String.prototype and every string in the program gains the method:
String.prototype.yell = function() {
alert(this);
};
"whoosh".yell(); // whoosh
The literal "whoosh" gets a temporary String wrapper, that wrapper inherits from String.prototype, and yell is now sitting there. Neat trick. Also, in most real code, a bad idea.
There’s a subtler hazard too. Adding an enumerable property to Object.prototype leaks into every for...in loop over every object in the program, which is a classic way to break code that was written before you showed up.
In modern practice there’s essentially one accepted reason to modify a native prototype: polyfilling.
A polyfill is a stand-in for a method that the language specification already defines, but that a particular engine hasn’t shipped yet. You detect the gap and fill it in, so old and new environments both expose the same standard method. Because you’re implementing behavior the spec already blesses, there’s no naming conflict to worry about — everyone agrees on what the method should do.
if (!String.prototype.repeat) { // only if the engine lacks it
// add it to the prototype
String.prototype.repeat = function(n) {
// repeat the string n times
// the real algorithm from the spec is a bit more involved,
// but even a rough polyfill is often good enough in practice
return new Array(n + 1).join(this);
};
}
alert( "ab".repeat(3) ); // ababab
The if (!String.prototype.repeat) guard is the important part. If the engine already has a native, correct repeat, you leave it alone and only supply your version where it’s missing.
Borrowing from prototypes
In Decorators and forwarding, call/apply we covered method borrowing: pulling a method off one object and running it against a different one.
Native prototype methods are prime candidates for borrowing. Suppose you have an array-like object — numeric keys plus a length, but not an actual array — and you want array behavior on it:
let obj = {
0: "north",
1: "south",
length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // north,south
This works because Array.prototype.join doesn’t check whether it’s operating on a real array. Its internal algorithm only reads indexed properties (0, 1, …) up to length. Give it an object shaped like an array and it does its job happily. Plenty of built-in array methods are written this generically, which is what makes borrowing them viable.
The demo below borrows join onto a plain object. Edit the items or the separator and re-run: notice obj never becomes an array (Array.isArray stays false), yet the array method runs on it just fine.
join ▶
ignores “am I an array?”
There’s a second route to the same goal: instead of copying one method at a time, set obj.__proto__ = Array.prototype so the whole set of array methods becomes reachable through inheritance. That’s cleaner when you want many methods — but it comes with a hard limit. An object has exactly one [[Prototype]], so if obj already inherits from something else, you can’t also inherit from Array.prototype. There’s no multiple inheritance here.
Copying individual methods sidesteps that constraint. It lets you mix a join from Array, maybe a method from somewhere else, all onto one object without touching its prototype chain. Borrowing trades a little tidiness for that flexibility.
Summary
- Every built-in follows one pattern. Methods live on the prototype (
Object.prototype,Array.prototype,Date.prototype, and so on); the individual value stores only its data (the array’s elements, the object’s own properties, the date’s timestamp). - All the built-in prototypes chain up to
Object.prototype, which is why “everything inherits from objects” holds, and why the chain always ends atnull. - Primitives get their methods the same way, through temporary wrapper objects whose methods come from
Number.prototype,String.prototype, andBoolean.prototype. Onlynullandundefinedhave no wrapper and no prototype. - You can add methods to native prototypes, but as a rule you shouldn’t — they’re global and collisions are silent. The one broadly accepted exception is polyfilling a standardized method an engine hasn’t implemented yet.
- Because many built-in methods only care about an object’s shape, not its type, you can borrow them (like
Array.prototype.join) onto array-like objects.