Skip to main content

Native Prototypes in JavaScript

Every built-in type in JavaScript, from arrays and strings to functions and dates, comes with methods you use daily. When you call [1, 2, 3].map() or "hello".toUpperCase(), those methods do not live on the array or string itself. They live on native prototypes: shared prototype objects that JavaScript creates automatically for each built-in type.

Understanding native prototypes reveals how the entire JavaScript standard library is organized. It explains where built-in methods come from, why every object has toString(), how primitives like strings suddenly gain methods, and why the entire prototype system converges at a single root object. This knowledge also helps you understand advanced patterns like borrowing methods from one type to use on another.

This guide maps out the complete hierarchy of native prototypes, shows you how primitives access prototype methods, and discusses the controversial practice of extending built-in prototypes.

Object.prototype: The Top of the Chain

At the very top of JavaScript's prototype hierarchy sits Object.prototype. This is the ultimate ancestor of almost every object in JavaScript. When a property lookup walks up the prototype chain and reaches Object.prototype, it has one more step before giving up: Object.prototype's own [[Prototype]] is null, marking the absolute end of the chain.

const obj = { name: "Alice" };

// obj's prototype is Object.prototype
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

// Object.prototype's prototype is null (end of the line)
console.log(Object.getPrototypeOf(Object.prototype)); // null

What Lives on Object.prototype

Object.prototype provides the fundamental methods that every object inherits:

const methods = Object.getOwnPropertyNames(Object.prototype);
console.log(methods);
// [
// "constructor",
// "hasOwnProperty",
// "isPrototypeOf",
// "propertyIsEnumerable",
// "toString",
// "valueOf",
// "toLocaleString",
// "__defineGetter__",
// "__defineSetter__",
// "__lookupGetter__",
// "__lookupSetter__",
// "__proto__"
// ]

This is why you can call toString() on any object, even one you just created with no methods of its own:

const empty = {};
console.log(empty.toString()); // "[object Object]"

// The method is not on empty (it's inherited)
console.log(empty.hasOwnProperty("toString")); // false
console.log(Object.prototype.hasOwnProperty("toString")); // true

Object the Constructor vs. Object.prototype

It is important not to confuse Object (the constructor function) with Object.prototype (the prototype object). They serve different roles:

// Object is a function with static methods
console.log(typeof Object); // "function"
console.log(typeof Object.keys); // "function" (static method on Object itself)
console.log(typeof Object.freeze); // "function" (static method on Object itself)

// Object.prototype is an object with instance methods
console.log(typeof Object.prototype); // "object"
console.log(typeof Object.prototype.toString); // "function" (inherited by all objects)

Static methods like Object.keys(), Object.freeze(), and Object.assign() live directly on the Object function. They are not inherited by regular objects. Instance methods like toString() and hasOwnProperty() live on Object.prototype and are inherited by all objects.

const user = { name: "Alice" };

// Instance method (inherited, works on the object)
console.log(user.toString()); // "[object Object]"

// Static method (NOT inherited, must call on Object directly)
console.log(user.keys); // undefined
console.log(Object.keys(user)); // ["name"]

Array.prototype, Function.prototype, String.prototype, and Others

Each built-in type in JavaScript has its own prototype object. These prototypes hold all the methods specific to that type and themselves inherit from Object.prototype.

Array.prototype

Every array you create inherits from Array.prototype, which provides all the array methods:

const arr = [1, 2, 3];

// arr's prototype is Array.prototype
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true

// Array.prototype's prototype is Object.prototype
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true

All the methods you use on arrays live on Array.prototype:

console.log(typeof Array.prototype.map);      // "function"
console.log(typeof Array.prototype.filter); // "function"
console.log(typeof Array.prototype.reduce); // "function"
console.log(typeof Array.prototype.push); // "function"
console.log(typeof Array.prototype.indexOf); // "function"

// These are the same methods your arrays use
const arr = [1, 2, 3];
console.log(arr.map === Array.prototype.map); // true

And because Array.prototype inherits from Object.prototype, arrays also have access to object methods:

const arr = [1, 2, 3];

// From Array.prototype
console.log(arr.map(x => x * 2)); // [2, 4, 6]

// From Object.prototype (inherited through the chain)
console.log(arr.hasOwnProperty(0)); // true
console.log(arr.hasOwnProperty(5)); // false
console.log(arr.toString()); // "1,2,3" (Array.prototype.toString overrides Object's)

Notice that toString() on arrays produces "1,2,3" instead of "[object Object]". This is because Array.prototype has its own toString() that overrides the one from Object.prototype:

console.log(Array.prototype.hasOwnProperty("toString"));  // true

// Array's toString joins elements with commas
console.log([1, 2, 3].toString()); // "1,2,3"

// Object's toString produces the type tag
console.log(Object.prototype.toString.call([1, 2, 3])); // "[object Array]"

Function.prototype

Every function inherits from Function.prototype, which provides methods like call, apply, and bind:

function greet(name) {
return `Hello, ${name}`;
}

console.log(Object.getPrototypeOf(greet) === Function.prototype); // true
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype); // true

// Function methods come from Function.prototype
console.log(typeof Function.prototype.call); // "function"
console.log(typeof Function.prototype.apply); // "function"
console.log(typeof Function.prototype.bind); // "function"

console.log(greet.call === Function.prototype.call); // true

An interesting detail: Function.prototype is itself a function (one that always returns undefined):

console.log(typeof Function.prototype); // "function"
console.log(Function.prototype()); // undefined

Number.prototype, String.prototype, Boolean.prototype

Each primitive type has a corresponding prototype that provides type-specific methods:

// Number methods
console.log(typeof Number.prototype.toFixed); // "function"
console.log(typeof Number.prototype.toPrecision); // "function"
console.log(typeof Number.prototype.toString); // "function"

// String methods
console.log(typeof String.prototype.toUpperCase); // "function"
console.log(typeof String.prototype.slice); // "function"
console.log(typeof String.prototype.includes); // "function"

// Boolean methods
console.log(typeof Boolean.prototype.toString); // "function"
console.log(typeof Boolean.prototype.valueOf); // "function"

All of these inherit from Object.prototype:

console.log(Object.getPrototypeOf(Number.prototype) === Object.prototype);  // true
console.log(Object.getPrototypeOf(String.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Boolean.prototype) === Object.prototype); // true

Other Built-In Prototypes

The same pattern applies to every built-in type:

// Date
const date = new Date();
console.log(Object.getPrototypeOf(date) === Date.prototype); // true
console.log(typeof Date.prototype.getFullYear); // "function"

// RegExp
const regex = /abc/;
console.log(Object.getPrototypeOf(regex) === RegExp.prototype); // true
console.log(typeof RegExp.prototype.test); // "function"

// Map
const map = new Map();
console.log(Object.getPrototypeOf(map) === Map.prototype); // true
console.log(typeof Map.prototype.get); // "function"

// Set
const set = new Set();
console.log(Object.getPrototypeOf(set) === Set.prototype); // true
console.log(typeof Set.prototype.has); // "function"

// Error
const err = new Error("test");
console.log(Object.getPrototypeOf(err) === Error.prototype); // true
console.log(typeof Error.prototype.toString); // "function"

Inspecting All Methods on a Prototype

You can see every method available on any prototype:

function listPrototypeMethods(prototype, name) {
const methods = Object.getOwnPropertyNames(prototype)
.filter(prop => typeof prototype[prop] === "function" && prop !== "constructor");
console.log(`${name} methods (${methods.length}):`);
console.log(methods.join(", "));
}

listPrototypeMethods(Array.prototype, "Array");
// Array methods (34):
// at, concat, copyWithin, entries, every, fill, filter, find, findIndex,
// findLast, findLastIndex, flat, flatMap, forEach, includes, indexOf, join,
// keys, lastIndexOf, map, pop, push, reduce, reduceRight, reverse, shift,
// slice, some, sort, splice, toLocaleString, toString, unshift, values, ...

Primitive Wrapper Prototypes

Primitives like strings, numbers, and booleans are not objects. They have no properties or methods of their own. Yet you can call methods on them without any issues:

console.log("hello".toUpperCase()); // "HELLO"
console.log((42).toFixed(2)); // "42.00"
console.log(true.toString()); // "true"

How is this possible? The answer is auto-boxing: when you access a property or method on a primitive, JavaScript temporarily wraps it in a wrapper object, accesses the property through that wrapper's prototype, and then discards the wrapper immediately.

How Auto-Boxing Works Step by Step

When you write "hello".toUpperCase(), this is what JavaScript does internally:

// Your code:
"hello".toUpperCase();

// What JavaScript does behind the scenes (approximately):
// 1. Create a temporary String wrapper object
let temp = new String("hello");
// 2. Access the method on that object (inherited from String.prototype)
let result = temp.toUpperCase();
// 3. Discard the wrapper
temp = null;
// 4. Return the result
// result === "HELLO"

You can verify that primitive methods come from the corresponding prototype:

const str = "hello";

// The method exists on String.prototype
console.log(String.prototype.hasOwnProperty("toUpperCase")); // true

// When called on a string, it uses String.prototype
console.log(str.toUpperCase === String.prototype.toUpperCase); // true

The Wrapper Is Temporary and Invisible

Because the wrapper object is created and destroyed instantly, you cannot store properties on primitives:

let str = "hello";
str.custom = "test"; // Creates wrapper, sets property on it, wrapper is discarded
console.log(str.custom); // undefined (the wrapper is gone)

In strict mode, this assignment throws an error:

"use strict";
let str = "hello";
str.custom = "test"; // TypeError: Cannot create property 'custom' on string 'hello'

null and undefined Have No Prototypes

Two primitive types are special: null and undefined have no wrapper objects and no prototypes. You cannot call any methods on them:

try {
null.toString();
} catch (e) {
console.log(e.message); // Cannot read properties of null (reading 'toString')
}

try {
undefined.toString();
} catch (e) {
console.log(e.message); // Cannot read properties of undefined (reading 'toString')
}

Wrapper Constructors: new String() vs. String()

There is a critical difference between calling wrapper constructors with and without new:

// Without new: type conversion (returns primitive)
const str = String(42);
console.log(typeof str); // "string"
console.log(str); // "42"

// With new: creates a wrapper object (almost never what you want)
const strObj = new String(42);
console.log(typeof strObj); // "object"
console.log(strObj); // [String: '42']

// The wrapper object behaves surprisingly in boolean context
if (new String("")) {
console.log("This runs! Empty string OBJECT is truthy!");
}

if ("") {
console.log("This does NOT run. Empty string PRIMITIVE is falsy.");
}
warning

Never use new String(), new Number(), or new Boolean() to create wrapper objects explicitly. They create objects that look like primitives but behave differently (especially in boolean contexts). Always use the type conversion functions without new: String(), Number(), Boolean().

Prototype Chain of Built-In Objects: Visual Diagram

All built-in types form a hierarchy that converges at Object.prototype. Here is the complete picture:

              null

│ [[Prototype]]

Object.prototype
┌──────────────────────┐
│ toString() │
│ valueOf() │
│ hasOwnProperty() │
│ isPrototypeOf() │
│ propertyIsEnumerable()│
│ toLocaleString() │
│ constructor: Object │
└──────────────────────┘
▲ ▲ ▲ ▲ ▲ ▲
│ │ │ │ │ │
│ │ │ │ │ │
Array.proto String.proto Number.proto Function.proto Date.proto RegExp.proto
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ map() │ │ slice() │ │ toFixed()│ │ call() │ │ getTime()│ │ test() │
│ filter() │ │ trim() │ │ toPrec.. │ │ apply() │ │ getYear()│ │ exec() │
│ reduce() │ │ split() │ │ toString │ │ bind() │ │ toISO..()│ │ toString │
│ push() │ │ includes │ │ valueOf()│ │ toString │ │ toString │ │ ... │
│ sort() │ │ replace()│ │ ... │ │ ... │ │ ... │ │ │
│ ... │ │ ... │ │ │ │ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
▲ ▲ ▲ ▲ ▲ ▲
│ │ │ │ │ │
[1, 2, 3] "hello" 42 function(){} new Date() /regex/

Verifying the Hierarchy Programmatically

function tracePrototypeChain(value) {
const chain = [];
let current;

// Handle primitives by getting their wrapper prototype
if (value === null || value === undefined) {
return [`${value} has no prototype chain`];
}

current = Object.getPrototypeOf(
typeof value === "object" || typeof value === "function" ? value : Object(value)
);

while (current !== null) {
// Try to identify the prototype
const name = identifyPrototype(current);
chain.push(name);
current = Object.getPrototypeOf(current);
}

chain.push("null");
return chain;
}

function identifyPrototype(proto) {
const builtins = [
[Array.prototype, "Array.prototype"],
[String.prototype, "String.prototype"],
[Number.prototype, "Number.prototype"],
[Boolean.prototype, "Boolean.prototype"],
[Function.prototype, "Function.prototype"],
[Date.prototype, "Date.prototype"],
[RegExp.prototype, "RegExp.prototype"],
[Map.prototype, "Map.prototype"],
[Set.prototype, "Set.prototype"],
[Error.prototype, "Error.prototype"],
[Object.prototype, "Object.prototype"]
];

for (const [builtinProto, name] of builtins) {
if (proto === builtinProto) return name;
}
return "Unknown prototype";
}

console.log(tracePrototypeChain([1, 2, 3]).join(" → "));
// Array.prototype → Object.prototype → null

console.log(tracePrototypeChain("hello").join(" → "));
// String.prototype → Object.prototype → null

console.log(tracePrototypeChain(42).join(" → "));
// Number.prototype → Object.prototype → null

console.log(tracePrototypeChain(function() {}).join(" → "));
// Function.prototype → Object.prototype → null

console.log(tracePrototypeChain(new Date()).join(" → "));
// Date.prototype → Object.prototype → null

console.log(tracePrototypeChain(/abc/).join(" → "));
// RegExp.prototype → Object.prototype → null

console.log(tracePrototypeChain(new Map()).join(" → "));
// Map.prototype → Object.prototype → null

Error Inheritance Chain

Error types have an extra level. Built-in error types like TypeError and RangeError inherit from Error.prototype:

const typeErr = new TypeError("wrong type");

console.log(Object.getPrototypeOf(typeErr) === TypeError.prototype); // true
console.log(Object.getPrototypeOf(TypeError.prototype) === Error.prototype); // true
console.log(Object.getPrototypeOf(Error.prototype) === Object.prototype); // true

// Chain: typeErr → TypeError.prototype → Error.prototype → Object.prototype → null
TypeError instance


TypeError.prototype


Error.prototype ← (message, stack, name)


Object.prototype


null

Method Resolution: Closest Wins

When multiple prototypes in the chain define a method with the same name, the closest one in the chain wins. This is the same shadowing principle from the previous guide, but now applied to built-in types:

const arr = [1, 2, 3];

// Array.prototype.toString: joins elements with commas
console.log(arr.toString()); // "1,2,3"

// Object.prototype.toString: produces type tag
console.log(Object.prototype.toString.call(arr)); // "[object Array]"

// Array.prototype.toString is closer in the chain, so it wins
console.log(Array.prototype.hasOwnProperty("toString")); // true
console.log(Object.prototype.hasOwnProperty("toString")); // true

Monkey-Patching: Extending Native Prototypes (and Why Not)

Monkey-patching means adding new methods to built-in prototypes. Since prototypes are regular objects, nothing in JavaScript prevents you from modifying them. The question is whether you should.

How It Works

// Add a method to all strings
String.prototype.reverse = function() {
return [...this].reverse().join("");
};

console.log("hello".reverse()); // "olleh"
console.log("JavaScript".reverse()); // "tpircSavaJ"

// Add a method to all arrays
Array.prototype.last = function() {
return this[this.length - 1];
};

console.log([1, 2, 3].last()); // 3
console.log(["a", "b", "c"].last()); // "c"

// Add a method to all numbers
Number.prototype.isPrime = function() {
const n = this.valueOf();
if (n < 2) return false;
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) return false;
}
return true;
};

console.log((7).isPrime()); // true
console.log((10).isPrime()); // false

This is technically simple and immediately powerful. Every string, array, or number in your entire application gets the new method. So why is it almost universally considered a bad practice?

Why You Should Not Extend Native Prototypes

1. Name collisions with future standards

The most dangerous problem. If you add Array.prototype.last() today and a future version of JavaScript introduces an official last() method with different behavior, your code breaks silently:

// You added this in 2020
Array.prototype.flat = function() {
return this.reduce((acc, val) => acc.concat(val), []);
};

// Then ES2019 added Array.prototype.flat() with different behavior
// (supports depth parameter, handles sparse arrays differently)

// Now your custom version shadows the built-in one
// Every library expecting the standard flat() is broken

This has actually happened in the real world. MooTools, a popular library, added Array.prototype.flatten(), which forced the TC39 committee to rename the standard method to Array.prototype.flat() to avoid breaking millions of websites.

2. Name collisions between libraries

If two libraries both add a method with the same name to the same prototype, the last one loaded wins, and the first one breaks:

// Library A
Array.prototype.unique = function() {
return [...new Set(this)];
};

// Library B (loaded later)
Array.prototype.unique = function() {
return this.filter((v, i, a) => a.indexOf(v) === i);
};

// Library A's unique is gone. If it relied on Set behavior, it may break.

3. Enumeration problems

Properties added to prototypes show up in for...in loops:

Object.prototype.customMethod = function() {
return "custom";
};

const user = { name: "Alice", age: 30 };

for (let key in user) {
console.log(key);
}
// "name"
// "age"
// "customMethod" (unexpected!)

This breaks any code that uses for...in without hasOwnProperty checks, which is a lot of existing code.

You can mitigate this by making the property non-enumerable:

Object.defineProperty(Object.prototype, "customMethod", {
value: function() { return "custom"; },
enumerable: false,
writable: true,
configurable: true
});

const user = { name: "Alice", age: 30 };

for (let key in user) {
console.log(key);
}
// "name"
// "age"
// No "customMethod" (better, but still risky)

4. Hard to debug and maintain

When methods appear "magically" on built-in types, it is difficult for other developers to find where they are defined. IDE autocomplete and documentation do not know about your custom additions.

The One Acceptable Exception: Polyfills

A polyfill is code that implements a standard feature in environments that do not support it yet. This is the one widely accepted reason to modify native prototypes:

// Polyfill for Array.prototype.at(): only add if missing
if (!Array.prototype.at) {
Array.prototype.at = function(index) {
index = Math.trunc(index) || 0;
if (index < 0) index += this.length;
if (index < 0 || index >= this.length) return undefined;
return this[index];
};
}

console.log([1, 2, 3].at(-1)); // 3 (works even in older environments)

The key difference: a polyfill implements the exact specification of a standard method. It checks whether the method already exists (if (!Array.prototype.at)) and only adds it when missing. It does not invent new, non-standard methods.

tip

For production code, use established polyfill libraries like core-js instead of writing your own. They handle edge cases, follow the specification precisely, and are thoroughly tested.

Better Alternatives to Monkey-Patching

Instead of modifying native prototypes, use these patterns:

Utility functions:

// Instead of String.prototype.reverse
function reverseString(str) {
return [...str].reverse().join("");
}

console.log(reverseString("hello")); // "olleh"

Wrapper classes:

class SuperArray extends Array {
last() {
return this[this.length - 1];
}

unique() {
return new SuperArray(...new Set(this));
}
}

const arr = SuperArray.from([1, 2, 2, 3, 3, 3]);
console.log(arr.last()); // 3
console.log(arr.unique()); // SuperArray [1, 2, 3]
console.log(arr.map(x => x * 2)); // SuperArray [2, 4, 4, 6, 6, 6]

// Regular arrays are not affected
console.log([1, 2, 3].last); // undefined

Utility modules:

// stringUtils.js
export function reverse(str) {
return [...str].reverse().join("");
}

export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}

export function truncate(str, maxLength, suffix = "...") {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - suffix.length) + suffix;
}

Borrowing from Prototypes

One powerful and legitimate use of native prototypes is method borrowing: taking a method from one prototype and using it on a different type of object. This works because most built-in methods operate on this, and you can control what this is with call or apply.

Borrowing Array Methods for Array-Like Objects

Array-like objects (objects with numeric indices and a length property, but not actual arrays) cannot use array methods directly. You can borrow them:

function showArguments() {
// arguments is array-like but not an array
console.log(arguments.length); // Works
// console.log(arguments.map(...)); // TypeError: arguments.map is not a function

// Borrow Array.prototype.map
const doubled = Array.prototype.map.call(arguments, x => x * 2);
console.log(doubled); // A real array
}

showArguments(1, 2, 3);
// 3
// [2, 4, 6]

Other common examples of borrowing array methods:

// Borrowing join for an array-like object
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };

const joined = Array.prototype.join.call(arrayLike, "-");
console.log(joined); // "a-b-c"

const sliced = Array.prototype.slice.call(arrayLike);
console.log(sliced); // ["a", "b", "c"] (now a real array)

const filtered = Array.prototype.filter.call(arrayLike, char => char !== "b");
console.log(filtered); // ["a", "c"]
info

In modern JavaScript, Array.from() and the spread operator often replace method borrowing for array-like objects:

const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };

// Modern alternatives
const arr1 = Array.from(arrayLike);
const arr2 = [...arrayLike]; // Only works if arrayLike is iterable

console.log(arr1.filter(char => char !== "b")); // ["a", "c"]

However, method borrowing remains useful in specific scenarios and is important to understand for reading existing code.

Borrowing Object.prototype.toString for Type Detection

The most widely used method borrowing pattern is using Object.prototype.toString.call() for reliable type detection. Built-in types override toString() with their own versions, but the original Object.prototype.toString produces a type tag:

// Each type has its own toString that hides Object's
console.log([1, 2].toString()); // "1,2" (Array's toString)
console.log((42).toString()); // "42" (Number's toString)
console.log(true.toString()); // "true" (Boolean's toString)

// Borrowing Object's toString reveals the internal type tag
console.log(Object.prototype.toString.call([1, 2])); // "[object Array]"
console.log(Object.prototype.toString.call(42)); // "[object Number]"
console.log(Object.prototype.toString.call("hello")); // "[object String]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
console.log(Object.prototype.toString.call(/regex/)); // "[object RegExp]"
console.log(Object.prototype.toString.call(new Map())); // "[object Map]"
console.log(Object.prototype.toString.call(new Set())); // "[object Set]"
console.log(Object.prototype.toString.call(function(){})); // "[object Function]"

You can build a universal type checker from this:

function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1);
}

console.log(getType([])); // "Array"
console.log(getType({})); // "Object"
console.log(getType("hello")); // "String"
console.log(getType(42)); // "Number"
console.log(getType(null)); // "Null"
console.log(getType(undefined)); // "Undefined"
console.log(getType(new Date())); // "Date"
console.log(getType(/abc/)); // "RegExp"
console.log(getType(new Map())); // "Map"

This is more reliable than typeof (which returns "object" for arrays, null, and dates) and works across different types consistently.

Borrowing String Methods for Other Objects

You can borrow string methods for any object that has a meaningful toString():

const numLike = {
toString() {
return "12345";
}
};

// Borrow String methods
const includes = String.prototype.includes.call(numLike, "234");
console.log(includes); // true

const upper = String.prototype.toUpperCase.call("hello");
console.log(upper); // "HELLO"

Borrowing in Real-World Code

A practical example: converting a NodeList (from DOM queries) to work with array methods:

// In browser environments
// const elements = document.querySelectorAll("div");
// elements is a NodeList, not an Array

// Borrow forEach (NodeList has its own forEach, but older browsers didn't)
// Array.prototype.forEach.call(elements, el => {
// el.classList.add("processed");
// });

// Borrow filter (NodeList does NOT have filter)
// const visible = Array.prototype.filter.call(elements, el => {
// return el.offsetHeight > 0;
// });

When Method Borrowing Fails

Not all methods can be borrowed universally. Methods that rely on internal slots specific to their type will throw errors:

// Map.prototype.get relies on internal Map slots
try {
Map.prototype.get.call({}, "key");
} catch (e) {
console.log(e.message);
// Method Map.prototype.get called on incompatible receiver #<Object>
}

// The same applies to Set, WeakMap, TypedArrays, etc.
try {
Set.prototype.has.call([], 1);
} catch (e) {
console.log(e.message);
// Method Set.prototype.has called on incompatible receiver
}

Methods that operate on generic properties like length and numeric indices (most Array methods) can be borrowed. Methods that require specific internal data structures (Map, Set, Promise, etc.) cannot.

Summary

Native prototypes are the backbone of JavaScript's standard library. Every built-in method you use daily lives on a prototype object, and the entire system forms a clean hierarchy rooted at Object.prototype.

ConceptKey Point
Object.prototypeThe root of almost all prototype chains; provides toString, valueOf, hasOwnProperty
Type-specific prototypesArray.prototype, String.prototype, Number.prototype, etc. hold type-specific methods
Inheritance chainEvery built-in prototype inherits from Object.prototype, which inherits from null
Primitive auto-boxingPrimitives temporarily wrap in objects to access prototype methods
null and undefinedHave no prototypes and no methods
Method shadowingCloser prototypes in the chain override methods from further up
Monkey-patchingModifying native prototypes is possible but almost always harmful
PolyfillsThe one accepted use case for modifying native prototypes
Method borrowingUsing call/apply to use a method from one prototype on a different object type
Object.prototype.toString.call()The most reliable way to detect the type of any value

Key rules to remember:

  • Do not add custom methods to native prototypes unless you are writing a standards-compliant polyfill
  • Use utility functions, wrapper classes, or modules instead of monkey-patching
  • Method borrowing with call/apply is a legitimate and powerful technique, especially for array-like objects and type detection
  • typeof is unreliable for many types; Object.prototype.toString.call() is the gold standard for type checking
  • Wrapper constructors (new String(), new Number()) create objects, not primitives, and should be avoided