Chapter 14

Ordinary and Exotic Objects: [[Get]], [[Set]], [[Call]] Internal Methods

The ECMAScript specification describes every object's behavior through a unified set of internal methods. Ordinary objects implement these methods by default rules; arrays, functions, and Proxies โ€” the "exotic objects" โ€” override certain methods to gain special capabilities. This is not syntactic magic: it is behavior divergence that is explicitly defined in the spec. Understanding these differences is what explains why array length updates automatically, why arrow functions cannot be new'd, and what causes the famous arguments synchronization behavior.

๐Ÿ”น Level 1 ยท What You Need to Know

Object Internal Methods

Every JavaScript object implements the following internal methods (spec ยง6.1.7.2):

Internal Method Signature Triggered By
[[GetPrototypeOf]] () โ†’ Object | Null Object.getPrototypeOf(obj)
[[SetPrototypeOf]] (Object | Null) โ†’ Boolean Object.setPrototypeOf(obj, proto)
[[IsExtensible]] () โ†’ Boolean Object.isExtensible(obj)
[[PreventExtensions]] () โ†’ Boolean Object.preventExtensions(obj)
[[GetOwnProperty]] (propertyKey) โ†’ Descriptor | undefined Object.getOwnPropertyDescriptor
[[DefineOwnProperty]] (propertyKey, Descriptor) โ†’ Boolean Object.defineProperty
[[HasProperty]] (propertyKey) โ†’ Boolean in operator
[[Get]] (propertyKey, Receiver) โ†’ any property read
[[Set]] (propertyKey, value, Receiver) โ†’ Boolean property assignment
[[Delete]] (propertyKey) โ†’ Boolean delete operator
[[OwnPropertyKeys]] () โ†’ List Object.keys, for...in

Callable objects also implement:

Internal Method Signature Triggered By
[[Call]] (any, List) โ†’ any func()
[[Construct]] (List, Object) โ†’ Object new func()

Ordinary Objects vs Exotic Objects

Ordinary Object: implements all internal methods using the default spec algorithms. Objects created with {} are ordinary objects.

Exotic Object: overrides at least one internal method. Main types:

Exotic object types and their overridden internal methods:

Array exotic objects:
  โ””โ”€โ”€ [[DefineOwnProperty]] โ€” auto-maintains length

Function objects:
  โ””โ”€โ”€ [[Call]], [[Construct]] โ€” make them callable/constructable

Arrow function objects:
  โ””โ”€โ”€ [[Call]] present, but NO [[Construct]] โ€” cannot be new'd

Bound functions (created by .bind()):
  โ””โ”€โ”€ [[Call]], [[Construct]] delegate to target function

arguments object (non-strict mode):
  โ””โ”€โ”€ [[Get]], [[Set]], [[DefineOwnProperty]] โ€” sync with formal params

String exotic objects (new String()):
  โ””โ”€โ”€ [[GetOwnProperty]], [[OwnPropertyKeys]] โ€” expose character indices

Proxy objects:
  โ””โ”€โ”€ All internal methods can be intercepted by traps (see ch15)

Module Namespace objects:
  โ””โ”€โ”€ [[Get]], [[Set]] โ€” read-only, special get behavior

Array's Auto-Updating length

This is one of the most common "magic" behaviors, and it's simply the overridden [[DefineOwnProperty]]:

const arr = [1, 2, 3]; // length = 3

arr[5] = 99;
// What happens:
// 1. [[DefineOwnProperty]]('5', {value: 99, ...}) is called
// 2. Array exotic [[DefineOwnProperty]] checks:
//    '5' is a valid array index (non-negative integer string)
//    5 >= arr.length (3), so length is auto-updated to 6
console.log(arr.length); // 6
console.log(arr); // [1, 2, 3, empty ร— 2, 99] (sparse array)

// Reverse: shrinking length deletes elements
arr.length = 2;
// [[DefineOwnProperty]]('length', {value: 2, ...})
// Deletes all properties with index >= 2
console.log(arr); // [1, 2]

Function [[Call]] vs [[Construct]]

function normalFn() { return 'called'; }
const arrowFn = () => 'called';
class MyClass {}

// [[Call]]: direct invocation
normalFn();      // 'called'
arrowFn();       // 'called'
// MyClass()     // TypeError: Class constructor cannot be invoked without 'new'

// [[Construct]]: new invocation
new normalFn();  // creates a new object
// new arrowFn();// TypeError: arrowFn is not a constructor
new MyClass();   // creates a MyClass instance

// Detecting [[Construct]] capability in user code:
function isConstructor(fn) {
  try {
    new fn();
    return true;
  } catch (e) {
    return e instanceof TypeError && e.message.includes('is not a constructor');
  }
}

The arguments Synchronization Behavior

This is one of JavaScript's most surprising features:

// Non-strict mode: arguments and formal params share the same "slot"
function test(a, b) {
  console.log(arguments[0]); // 1
  a = 99;                    // modify formal param
  console.log(arguments[0]); // 99 โ€” arguments updated too!

  arguments[1] = 88;         // modify arguments
  console.log(b);            // 88 โ€” formal param updated too!
}
test(1, 2);

// Strict mode: completely decoupled
function strictTest(a, b) {
  'use strict';
  a = 99;
  console.log(arguments[0]); // 1 โ€” unaffected
  arguments[1] = 88;
  console.log(b);            // 2 โ€” unaffected
}
strictTest(1, 2);

5 Common Mistakes

Mistake 1: Using typeof to check if a function can be new'd

typeof function(){};  // 'function'
typeof (() => {});    // 'function' โ€” but can't be new'd!
typeof class C {};    // 'function' โ€” can be new'd, but not called directly

// Correct approach: use try-catch

Mistake 2: Thinking arr.length is always O(1)

arr.length;     // O(1) read โ€” fast

arr.length = 0; // O(n) write โ€” deletes all elements!
// Equivalent to deleting all numeric-indexed properties

arr[1000000] = 1; // length becomes 1000001, but no 1MB allocation
// V8 uses sparse storage (HashTable/Dictionary mode), not contiguous array

Mistake 3: Incomplete switch from arguments to rest params

// Rest params are NOT in arguments
function test(...args) {
  console.log(arguments[0]); // 1
  console.log(args[0]);       // 1
  // They are separate! args is a real Array, arguments is array-like
  Array.isArray(args);       // true
  Array.isArray(arguments);  // false
}
test(1, 2, 3);

function mix(first, ...rest) {
  console.log(arguments.length); // total args passed
  console.log(rest.length);       // args after first
}
mix(1, 2, 3); // arguments.length = 3, rest.length = 2

Mistake 4: new Array(n) creates a sparse array

const arr = new Array(3);
console.log(arr.length);    // 3
console.log(arr[0]);        // undefined
console.log(0 in arr);      // false! โ€” sparse array, index 0 doesn't exist

// Contrast:
const arr2 = [undefined, undefined, undefined];
console.log(0 in arr2);     // true โ€” index exists, value is undefined

// Danger: map/filter skip holes in sparse arrays
new Array(3).map((_, i) => i); // [empty ร— 3], not [0, 1, 2]!
// Correct:
Array.from({length: 3}, (_, i) => i); // [0, 1, 2]
[...new Array(3)].map((_, i) => i);   // [0, 1, 2]

Mistake 5: delete on an array element doesn't update length

const arr = [1, 2, 3];
delete arr[1]; // removes element, creates sparse array
console.log(arr);         // [1, empty, 3]
console.log(arr.length);  // 3 โ€” unchanged
console.log(1 in arr);    // false โ€” index 1 no longer exists

// To truly remove and shift:
arr.splice(1, 1);
// Or filter:
arr.filter((_, i) => i !== 1);

๐Ÿ”ธ Level 2 ยท How It Works Internally

Internal Methods Comparison: Ordinary vs Exotic

Internal method implementations across object types:

Internal Method        โ”‚ Ordinary โ”‚ Array  โ”‚ Function โ”‚ Arrow  โ”‚ arguments(non-strict) โ”‚
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
[[GetPrototypeOf]]     โ”‚ default  โ”‚default โ”‚ default  โ”‚ defaultโ”‚ default              โ”‚
[[SetPrototypeOf]]     โ”‚ default  โ”‚default โ”‚ default  โ”‚ defaultโ”‚ default              โ”‚
[[IsExtensible]]       โ”‚ default  โ”‚default โ”‚ default  โ”‚ defaultโ”‚ default              โ”‚
[[GetOwnProperty]]     โ”‚ default  โ”‚default โ”‚ default  โ”‚ defaultโ”‚ special (index map)  โ”‚
[[DefineOwnProperty]]  โ”‚ default  โ”‚SPECIAL โ”‚ default  โ”‚ defaultโ”‚ special (index map)  โ”‚
[[HasProperty]]        โ”‚ default  โ”‚default โ”‚ default  โ”‚ defaultโ”‚ default              โ”‚
[[Get]]                โ”‚ default  โ”‚default โ”‚ default  โ”‚ defaultโ”‚ special (index map)  โ”‚
[[Set]]                โ”‚ default  โ”‚default โ”‚ default  โ”‚ defaultโ”‚ special (index map)  โ”‚
[[Delete]]             โ”‚ default  โ”‚default โ”‚ default  โ”‚ defaultโ”‚ special (remove map) โ”‚
[[OwnPropertyKeys]]    โ”‚ default  โ”‚default โ”‚ default  โ”‚ defaultโ”‚ default              โ”‚
[[Call]]               โ”‚  none    โ”‚ none   โ”‚ present  โ”‚present โ”‚ none                 โ”‚
[[Construct]]          โ”‚  none    โ”‚ none   โ”‚ present  โ”‚ NONE   โ”‚ none                 โ”‚

Array Exotic [[DefineOwnProperty]] in Detail

Array's [[DefineOwnProperty]] adds two special cases on top of the ordinary implementation:

ArrayDefineOwnProperty(A, P, Desc) flow:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                                                                 โ”‚
โ”‚  Is P 'length'?                                                 โ”‚
โ”‚  โ”œโ”€โ”€ Yes โ†’ ArraySetLength(A, Desc)                             โ”‚
โ”‚  โ”‚    โ””โ”€โ”€ If new length < old length, delete out-of-range      โ”‚
โ”‚  โ”‚        index properties                                      โ”‚
โ”‚  โ”‚                                                              โ”‚
โ”‚  โ””โ”€โ”€ No โ†’ Is P a valid array index (non-negative integer str)? โ”‚
โ”‚       โ”œโ”€โ”€ Yes โ†’ index assignment logic:                         โ”‚
โ”‚       โ”‚    index = ToUint32(P)                                  โ”‚
โ”‚       โ”‚    If index >= A.length                                 โ”‚
โ”‚       โ”‚      โ†’ update length to index + 1                      โ”‚
โ”‚       โ”‚    โ†’ call ordinary [[DefineOwnProperty]] to set prop    โ”‚
โ”‚       โ”‚                                                         โ”‚
โ”‚       โ””โ”€โ”€ No โ†’ call ordinary [[DefineOwnProperty]]             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

What is a "valid array index"? The spec defines: P is a string that is a valid array index if and only if ToString(ToUint32(P)) === P and ToUint32(P) !== 2^32 - 1.

// Valid array indices:
'0', '1', '1000000'

// Not valid:
'-1'          // negative
'1.5'         // floating point
'4294967295'  // equals 2^32 - 1, excluded
'hello'       // non-numeric

Function [[Call]] and [[Construct]]

[[Call]] execution:

OrdinaryCallEvaluateBody(F, argumentsList):
1. Create a new Function Execution Context
2. Bind this to Receiver (or globalThis, depending on strict mode)
3. Bind arguments to parameter list
4. Execute function body
5. Return Return value (or undefined)

Key differences between [[Construct]] and [[Call]]:

new F(...args) execution (OrdinaryConstruct):
1. Create new object: thisValue = OrdinaryCreateFromConstructor(F, "%Object.prototype%")
   โ””โ”€โ”€ new object's [[Prototype]] is set to F.prototype
2. Call [[Call]](thisValue, args)
   โ””โ”€โ”€ 'this' inside the function body refers to the new object
3. If function returns an object โ†’ use that object as new expression result
4. Otherwise โ†’ use the object created in step 1

Key point: step 3 means a constructor can "hijack" new's return value
           by explicitly returning an object
function Tricky() {
  this.x = 1;
  return { y: 2 }; // explicit object return
}

const t = new Tricky();
console.log(t.x); // undefined โ€” returned {y:2}, not this
console.log(t.y); // 2

function NotTricky() {
  this.x = 1;
  return 42; // returning a primitive โ€” ignored
}

const nt = new NotTricky();
console.log(nt.x); // 1 โ€” this is returned normally

The arguments Internal Mapping Mechanism

Non-strict arguments objects implement parameter synchronization through the Arguments Exotic Object mechanism:

arguments object internal structure (non-strict mode):

arguments object
โ”œโ”€โ”€ numeric index properties (0, 1, 2, ...)
โ”œโ”€โ”€ [[ParameterMap]]: stores index โ†’ formal param variable name mappings
โ””โ”€โ”€ overridden internal methods:
    [[Get]](P):
      If P is in [[ParameterMap]] โ†’ read current value of corresponding param variable
      Otherwise โ†’ ordinary [[Get]]
    
    [[Set]](P, V):
      If P is in [[ParameterMap]] โ†’ set value of corresponding param variable
      Then ordinary [[Set]] (also updates own property)
function example(x, y, z) {
  // Internally: [[ParameterMap]] = { '0': x variable, '1': y variable, '2': z variable }

  x = 10;
  // [[Set]] finds '0' in ParameterMap โ†’ modifies x variable
  // arguments[0] read: [[Get]] finds '0' in ParameterMap โ†’ returns x's current value: 10

  arguments[1] = 20;
  // [[Set]] finds '1' in ParameterMap โ†’ modifies y variable
  // y is now 20

  // Severing the mapping:
  delete arguments[2];
  // [[Delete]] removes '2' from ParameterMap
  z = 30;
  // z variable changes, but arguments[2] no longer syncs
  console.log(arguments[2]); // undefined โ€” mapping severed
}

String Exotic Objects

Objects created with new String('hello') have special behavior:

const s = new String('hello');

// Characters accessible via numeric indices
s[0]; // 'h'
s[1]; // 'e'

// These index properties are read-only and non-configurable
Object.getOwnPropertyDescriptor(s, '0');
// { value: 'h', writable: false, enumerable: true, configurable: false }

// Trying to modify:
s[0] = 'X'; // silent fail (non-strict)

// OwnPropertyKeys includes numeric indices
Object.getOwnPropertyNames(s);
// ['0', '1', '2', '3', '4', 'length']

// Comparison with primitive string:
typeof 'hello';               // 'string' (primitive)
typeof new String('hello');   // 'object' (wrapper object)
'hello' instanceof String;    // false (primitive is not an object)
new String('hello') instanceof String; // true

๐Ÿ”บ Level 3 ยท How the Spec Defines It

Spec ยง10: Ordinary and Exotic Object Behaviours

Section ยง10 of the spec is the core chapter on object behavior:

ยง10 Ordinary and Exotic Object Behaviours
โ”œโ”€โ”€ ยง10.1 Ordinary Object Internal Methods (default, 11 methods)
โ”œโ”€โ”€ ยง10.2 Function Objects (ordinary functions)
โ”œโ”€โ”€ ยง10.3 Built-in Function Objects
โ”œโ”€โ”€ ยง10.4 Built-in Exotic Object Internal Methods and Slots
โ”‚   โ”œโ”€โ”€ ยง10.4.1 Bound Function Exotic Objects
โ”‚   โ”œโ”€โ”€ ยง10.4.2 Array Exotic Objects          โ† this chapter
โ”‚   โ”œโ”€โ”€ ยง10.4.3 String Exotic Objects
โ”‚   โ”œโ”€โ”€ ยง10.4.4 Arguments Exotic Objects      โ† this chapter
โ”‚   โ””โ”€โ”€ ยง10.4.5 Integer-Indexed Exotic Objects (TypedArray)
โ””โ”€โ”€ ยง10.5 Proxy Object Internal Methods (see ch15)

Array Exotic [[DefineOwnProperty]] Algorithm (ยง10.4.2.1)

Key parts of the spec (ยง10.4.2.1 ArrayDefineOwnProperty):

ArrayDefineOwnProperty ( A, P, Desc )

1. If P is "length":
   a. If Desc has no [[Value]] field โ†’ use ordinary object handling
   b. newLen โ† ToUint32(Desc.[[Value]])
   c. numberLen โ† ToNumber(Desc.[[Value]])
   d. If newLen โ‰  numberLen โ†’ throw RangeError("Invalid array length")
   e. Replace Desc's [[Value]] with newLen
   f. If newLen >= oldLen โ†’ use ordinary handling (only growing, no deletions needed)
   g. Otherwise (newLen < oldLen):
      If oldLenDesc.[[Writable]] is false โ†’ return false
      ...loop: delete properties with index from oldLen-1 down to newLen...
      If any property is non-deletable (configurable:false) โ†’ return false

2. Otherwise P is an array index:
   index โ† ToUint32(P)
   If index >= oldLen and oldLenDesc.[[Writable]] is false โ†’ return false
   Call ordinary [[DefineOwnProperty]] to set the index property
   If index >= oldLen โ†’ update length to index + 1
   Return true
// Verify: cannot extend array when length is non-writable
const arr = [1, 2, 3];
Object.defineProperty(arr, 'length', { writable: false });
arr[3] = 4; // TypeError (strict) or silent fail (non-strict)

// Verify: invalid length throws RangeError
arr.length = -1;       // RangeError: Invalid array length
arr.length = 2 ** 32;  // RangeError: Invalid array length
arr.length = 'hello';
// ToUint32('hello') = 0, ToNumber('hello') = NaN, 0 โ‰  NaN โ†’ RangeError

Function [[Call]] and [[Construct]] Spec Definitions

[[Call]] (ยง10.2.1):

F.[[Call]](thisArgument, argumentsList):
1. callerContext โ† current running execution context
2. Create new ECMAScript code execution context calleeContext
3. Call PrepareForOrdinaryCall(F, undefined)
   โ†’ set up function's [[Environment]] (scope chain)
4. If F is a ClassConstructor โ†’ throw TypeError
   (class constructors must be called with new)
5. Call OrdinaryCallBindThis(F, calleeContext, thisArgument)
   โ†’ bind this to execution context
6. Call OrdinaryCallEvaluateBody(F, argumentsList)
   โ†’ execute function body
7. Pop calleeContext, restore callerContext
8. Return execution result

[[Construct]] (ยง10.2.2):

F.[[Construct]](argumentsList, newTarget):
1. If F's [[ConstructorKind]] is "base" (not derived class):
   thisArgument โ† OrdinaryCreateFromConstructor(newTarget, "%Object.prototype%")
   โ†’ creates new object with [[Prototype]] = newTarget.prototype
2. calleeContext โ† PrepareForOrdinaryCall(F, newTarget)
3. If [[ConstructorKind]] is "base" โ†’ OrdinaryCallBindThis binds this
4. result โ† OrdinaryCallEvaluateBody(F, argumentsList)
5. If result [[Type]] is return:
   If result.[[Value]] is an object โ†’ return that object (override behavior)
6. If result [[Type]] is not normal โ†’ throw exception
7. If [[ConstructorKind]] is "base" โ†’ return thisArgument
8. If thisArgument is undefined โ†’ throw ReferenceError
   (derived constructor that didn't call super())
9. Return thisArgument

Arguments Exotic Objects Spec Definition (ยง10.4.4)

The spec creates mapped arguments objects in CreateMappedArgumentsObject:

CreateMappedArgumentsObject(func, formals, argumentsList, env):
1. Create ordinary object obj
2. Set [[ParameterMap]] = a fresh ordinary object
3. For each formal parameter name (traversed in reverse, by index):
   If name is not duplicated:
     mappedNames.add(name)
     Define accessor property on [[ParameterMap]] for ToString(index):
       get: return env.GetBindingValue(name)  โ† reads param variable directly
       set: env.SetMutableBinding(name, v)    โ† writes param variable directly
4. obj.[[Get]] = MakeArgGetter(...)   โ† overridden to use ParameterMap
5. obj.[[Set]] = MakeArgSetter(...)
6. ...set length, callee, etc...
7. Return obj

๐Ÿ’Ž Level 4 ยท Edge Cases and Traps

Trap 1: new Array(3) Is a Sparse Array, Not [3]

new Array(3)    // [empty ร— 3], not [3]
new Array(1, 2) // [1, 2]

// The Array constructor has two overloads:
// new Array(len) โ€” single numeric arg โ†’ creates array with length len
// new Array(v1, v2, ...) โ€” multiple args โ†’ creates array with those values

// The trap:
new Array(3).map(x => x * 2); // [empty ร— 3], not [0, 0, 0]!
// map skips holes in sparse arrays

// Verifying the difference:
0 in new Array(3); // false โ€” index 0 doesn't exist
0 in [undefined, undefined, undefined]; // true โ€” index exists, value is undefined

// Correct ways to create filled arrays:
Array.from({length: 3}, (_, i) => i * 2); // [0, 2, 4]
new Array(3).fill(0).map((_, i) => i * 2); // [0, 2, 4]
[...new Array(3)].map((_, i) => i * 2);    // [0, 2, 4]
// Note: spread converts empty to undefined, then map processes them

// Also watch out for:
new Array(3).fill([]); // [[], [], []] โ€” all refs to the SAME array!
new Array(3).fill(null).map(() => []); // [[], [], []] โ€” independent arrays

Trap 2: The Calculation Rules for Function length

A function's length property is the formal parameter count, with several non-obvious rules:

// Basic rule:
function f(a, b, c) {}
f.length; // 3

// Rest parameters are NOT counted:
function f2(a, b, ...rest) {}
f2.length; // 2, not 3

// Default parameters are NOT counted (from the first default onward):
function f3(a, b = 1, c) {}
f3.length; // 1 (only a; b has default, so b and c after it don't count)

function f4(a = 1, b, c) {}
f4.length; // 0 (a has default, so nothing counts)

// Destructuring:
function f5({ x, y }) {}
f5.length; // 1 (destructuring counts as one parameter)

function f6([a, b, c]) {}
f6.length; // 1

// Effect of bind:
function f7(a, b, c) {}
const bound = f7.bind(null, 1); // binds one argument
bound.length; // 2 (3 - 1 = 2)
// Never goes below 0:
const bound2 = f7.bind(null, 1, 2, 3, 4);
bound2.length; // 0 (minimum is 0)

// Class and arrow functions:
class C {
  constructor(a, b) {}
}
C.length; // 2 (constructor parameter count)

const arrow = (a, b, c) => {};
arrow.length; // 3

Trap 3: Arrow Functions Have No [[Construct]] โ€” new Throws TypeError

const Arrow = () => {};
new Arrow(); // TypeError: Arrow is not a constructor

// Why?
// Arrow functions are defined in the spec without allocating the [[Construct]] method
// They also have no prototype property (which new needs to set the instance's [[Prototype]])
Arrow.prototype; // undefined

// Contrast with ordinary functions:
function Normal() {}
Normal.prototype; // {constructor: Normal}

// Rough detection via prototype:
function isArrowOrBound(fn) {
  return !fn.prototype;
}

// Note: bound functions also have no prototype:
const bound = (function(){}).bind(null);
bound.prototype; // undefined
// But bound functions DO have [[Construct]] (delegated to original):
new bound(); // works if original function was constructable

Why can't arrow functions be new'd?

Arrow functions have no this of their own โ€” they capture this from the surrounding lexical scope. The new operation needs to create a new this object and bind it to the function. Since an arrow's this is fixed, this is a conceptual contradiction. If new were allowed, this would refer to some outer object rather than the newly created instance, producing unpredictable behavior.

Trap 4: The Full arguments Synchronization Story

function analyze(x, y) {
  console.log('Initial state:');
  console.log('x =', x, '  arguments[0] =', arguments[0]); // 1, 1

  // Case 1: modifying formal param syncs to arguments
  x = 100;
  console.log('After x = 100:');
  console.log('x =', x, '  arguments[0] =', arguments[0]); // 100, 100

  // Case 2: modifying arguments syncs to formal param
  arguments[0] = 999;
  console.log('After arguments[0] = 999:');
  console.log('x =', x, '  arguments[0] =', arguments[0]); // 999, 999

  // Case 3: destructuring severs the binding
  let [a] = arguments; // copy to new variable, severs sync
  arguments[0] = 777;
  console.log('a =', a, '  x =', x, '  arguments[0] =', arguments[0]);
  // a = 999 (snapshot), x = 777 (still syncing), arguments[0] = 777

  // Case 4: extra arguments (no corresponding formal param)
  console.log('arguments[2] =', arguments[2]); // 3
  // arguments[2] has no formal param โ€” it's a plain property, no sync
}

analyze(1, 2, 3);

Performance note: V8 blocks certain optimizations for functions that use arguments. In performance-critical code, replace arguments with rest params ...args.

Trap 5: delete arr[0] vs arr.splice(0, 1)

const arr = [1, 2, 3, 4, 5];

// delete: removes the element, leaves a hole, length unchanged
delete arr[1];
console.log(arr);        // [1, empty, 3, 4, 5]
console.log(arr.length); // 5 โ€” unchanged
console.log(1 in arr);   // false โ€” index 1 no longer exists

// splice: removes element, shifts subsequent elements, decrements length
const arr2 = [1, 2, 3, 4, 5];
arr2.splice(1, 1);
console.log(arr2);        // [1, 3, 4, 5]
console.log(arr2.length); // 4

// Effect on array methods when sparse:
const sparse = [1, , 3]; // sparse array (index 1 doesn't exist)

sparse.forEach(x => console.log(x));  // 1, 3 (skips hole)
sparse.map(x => x * 2);               // [2, empty, 6] (preserves hole position)
sparse.filter(x => x > 0);            // [1, 3] (skips hole)
[...sparse];                           // [1, undefined, 3] (spread converts hole to undefined)
Array.from(sparse);                    // [1, undefined, 3]

Why doesn't delete change length?

The spec's [[Delete]] internal method: for arrays, deleting a numeric-indexed property only removes that property โ€” it does not trigger ArraySetLength. This is because delete means "remove the property", not "remove an element from the sequence". These are conceptually different for arrays, though frequently confused in practice.


Chapter Summary

  1. All JS objects share the same internal method interface: ordinary objects use default implementations; exotic objects (Array, functions, arguments) override some methods โ€” this is the root source of all "magical" behavior.

  2. Array's auto-updating length comes from overriding [[DefineOwnProperty]]: index assignment auto-updates length; shrinking length auto-deletes out-of-range elements. delete arr[i] skips this logic, which is why length doesn't change.

  3. [[Construct]] is not present on all functions: arrow functions, method shorthands (obj.method(){}), and async arrow functions don't have it. Function length only counts parameters up to the first one with a default value.

  4. Non-strict mode arguments and formal params share bindings: implemented through [[ParameterMap]]; delete arguments[i] severs the map entry. This sync behavior does not exist in strict mode.

  5. new Array(n) creates a sparse array: holes (empty) and undefined are different โ€” 0 in new Array(3) is false, but 0 in [undefined, undefined, undefined] is true. Use Array.from, fill, or spread to create properly filled arrays.

Rate this chapter
4.6  / 5  (21 ratings)

๐Ÿ’ฌ Comments