How it works
The spec defines a default for each internal method (OrdinaryGet, OrdinaryDefineOwnProperty, and so on). An exotic object's creation operation (ArrayCreate, StringCreate, TypedArrayCreate, ModuleNamespaceCreate, BoundFunctionCreate, ProxyCreate) installs its own versions for the methods it changes, and the rest stay ordinary.
Array. [[DefineOwnProperty]] special-cases the key "length" (via ArraySetLength, which converts with ToUint32 and throws RangeError if the number isn't a valid length) and array indices (canonical integer strings below 2^32 − 1). Defining index i at or past length also defines length as i + 1, and fails if length is non-writable. Shrinking length deletes elements from the end, stopping at a non-configurable one. Only those keys are special, which is why arr['02'] and arr['-1'] don't change length.
String objects (not primitives) get [[GetOwnProperty]], [[DefineOwnProperty]] and [[OwnPropertyKeys]] overrides: index keys below the string length and "length" appear as own, read-only, enumerable (for indices) properties. In strict code str[0] = 'H' throws TypeError. For primitive strings, 'hey'[0] works through ToObject and the same exotic behaviour.
TypedArray. Eight internal methods are overridden. A key that is a canonical numeric string (the result of ToString of a Number, including "-0" and "1.5") is never looked up as an ordinary property: if it isn't a valid in-bounds integer index, [[Get]] returns undefined, [[HasProperty]] returns false, [[Set]] does nothing, and the prototype chain is skipped. After Object.prototype[7] = 'proto', new Uint8Array(2)[7] is undefined but [][7] is 'proto' in Node.js 24. Object.freeze on a non-empty typed array throws TypeError, because the elements are always writable.
Module namespace. import * as ns yields an object with null prototype, Symbol.toStringTag "Module", string keys sorted by code unit order, each key { writable: true, enumerable: true, configurable: false }, and no extensibility. Reads are live: [[Get]] resolves the binding at access time, so it sees later changes and throws ReferenceError for a binding still in its temporal dead zone. [[Set]] always returns false, and [[Delete]] of an export returns false too.
// ns.mjs — a module that imports its own namespace
import * as ns from './ns.mjs';
export let count = 0;
export function inc() {
count++;
}
console.log(Object.prototype.toString.call(ns), Object.getPrototypeOf(ns), Object.isExtensible(ns));
// → [object Module] null false
inc();
console.log(ns.count, Reflect.set(ns, 'count', 5), Reflect.defineProperty(ns, 'extra', { value: 1 }));
// → 1 false false
try {
ns.count = 5;
} catch (e) {
console.log(e.constructor.name);
}
// → TypeError
Bound function. BoundFunctionCreate stores [[BoundTargetFunction]], [[BoundThis]] and [[BoundArguments]]. [[Call]] ignores the caller's this and prepends the stored arguments. [[Construct]] ignores [[BoundThis]], and if new.target is the bound function, substitutes the target:
class User { constructor(name) { this.name = name; } }
const BoundUser = User.bind(null, 'Ada');
const u = new BoundUser();
console.log(u instanceof User, u instanceof BoundUser, u.name);
// → true true Ada
A bound function has no prototype property, and instanceof with a bound function on the right goes to the target. Its name is "bound " + name.
Arguments (mapped). CreateMappedArgumentsObject is used only for sloppy functions with simple parameter lists. Its [[ParameterMap]] aliases index properties to parameter bindings, so in CommonJS function f(a) { arguments[0] = 'changed'; return a; } returns 'changed'. With 'use strict' or a default value it returns the original argument.
Immutable prototype. Object.prototype is exotic only here: Object.setPrototypeOf(Object.prototype, {}) throws TypeError, and Reflect.setPrototypeOf(Object.prototype, null) returns true because the value is unchanged.
Edge cases
Object.prototype.toString reads internal slots and Symbol.toStringTag, not behaviour: an array, a proxy of an array and an arguments object print [object Array], [object Array] and [object Arguments]. A proxy has none of the target's slots, but IsArray looks through to the target, and typeof follows whether the proxy has [[Call]].
Trade-offs
Knowing the exotic kinds explains surprising behaviours (sparse array length, silent typed-array writes, live read-only namespaces, arguments aliasing) and what a Proxy can and can't fake. In application code, rely on the documented APIs rather than internal-method details, and don't use arguments aliasing or sparse arrays on purpose.