Chapter 21

Function Object Internals: [[Call]] vs [[Construct]], arguments, bind, Tail Calls

Every function in JavaScript is an object, but not every function object carries the same internal capabilities — arrow functions lack [[Construct]], bound functions carry extra internal slots, and these differences are not syntactic conventions but hard distinctions encoded in the object structure of the specification.

🔹 Level 1 · What You Need to Know

Three Core Properties of Functions

Function objects expose three observable properties: length, name, and prototype.

Property Meaning Special Cases
length Number of formal parameters (excludes default and rest params) function f(a, b=1, ...c){}f.length === 1
name Function name string Anonymous functions get inferred names: const fn = () => {}fn.name === 'fn'
prototype When used as constructor, becomes [[Prototype]] of new object Arrow functions have no prototype property
function greet(name, greeting = 'Hello', ...extras) {
  return `${greeting}, ${name}!`
}

console.log(greet.length)  // 1, only 'name' counts as required
console.log(greet.name)    // 'greet'

const arrow = (x, y) => x + y
console.log(arrow.length)     // 2
console.log(arrow.prototype)  // undefined — arrow functions have no prototype

Three Hard Restrictions on Arrow Functions

Arrow functions are not shorthand for ordinary functions. They carry three design-level hard restrictions:

  1. No prototype property → cannot be used as constructors (new arrow() throws TypeError)
  2. No arguments object → use rest parameters ...args instead
  3. this is lexically bound → inherited from the enclosing scope at definition time, not overridable by call/apply/bind
const Counter = (start) => {
  this.count = start  // 'this' is lexical — not the newly created object
}

// new Counter(0)  // TypeError: Counter is not a constructor

// Correct approach:
function Counter(start) {
  this.count = start
}
const c = new Counter(0)
console.log(c.count)  // 0

What bind Does

Function.prototype.bind(thisArg, ...partialArgs) returns a new function with this and some leading arguments permanently fixed. Subsequent call/apply cannot override the bound this.

function multiply(a, b) {
  return a * b
}

const double = multiply.bind(null, 2)  // first argument fixed to 2
console.log(double(5))   // 10
console.log(double(10))  // 20

// bound function has reduced length
console.log(multiply.length)  // 2
console.log(double.length)    // 1 (one bound argument deducted)

The arguments Object (Non-Strict Mode)

Non-arrow functions in non-strict mode have access to an arguments object containing all arguments actually passed.

function sum() {
  let total = 0
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i]
  }
  return total
}

console.log(sum(1, 2, 3, 4))  // 10, accepts any number of arguments

Modern code should prefer rest parameters. The arguments object is a legacy feature.


🔸 Level 2 · How It Actually Works

Complete List of Function Object Internal Slots

The ECMAScript spec encodes function capabilities as internal slots. Here are the key internal slots of ECMAScript function objects:

┌─────────────────────────────────────────────────────────────────┐
│             ECMAScript Function Object Internal Slots            │
├──────────────────────┬──────────────────────────────────────────┤
│ Internal Slot        │ Description                              │
├──────────────────────┼──────────────────────────────────────────┤
│ [[Environment]]      │ Lexical environment captured at creation  │
│ [[PrivateEnvironment]]│ Private name environment (for classes)  │
│ [[FormalParameters]] │ Parameter list (AST node)                │
│ [[ECMAScriptCode]]   │ Function body code (AST node)            │
│ [[Realm]]            │ The Realm the function belongs to        │
│ [[ScriptOrModule]]   │ Owning script or module record           │
│ [[ThisMode]]         │ lexical / strict / global                │
│ [[Strict]]           │ Whether the function is strict mode      │
│ [[HomeObject]]       │ Object targeted by super (method only)   │
│ [[Fields]]           │ Class field definition list              │
│ [[PrivateMethods]]   │ Private method list                      │
│ [[ConstructorKind]]  │ base / derived (subclass constructors)   │
└──────────────────────┴──────────────────────────────────────────┘

All callable objects must implement [[Call]]; only constructors also implement [[Construct]]:

Function Type vs Internal Method Presence:

  Ordinary function: function f() {}
  ├── [[Call]]      ✓
  └── [[Construct]] ✓

  Arrow function: () => {}
  ├── [[Call]]      ✓
  └── [[Construct]] ✗ (absent)

  Method shorthand: { m() {} }
  ├── [[Call]]      ✓
  └── [[Construct]] ✗ (absent)

  class constructor
  ├── [[Call]]      ✗ (direct call throws TypeError)
  └── [[Construct]] ✓

  Bound function: f.bind(...)
  ├── [[Call]]      ✓
  └── [[Construct]] ✓ (if the original function has it)

How [[Call]] Executes

F.[[Call]](thisArgument, argumentsList) is the internal method triggered by ordinary function calls (f()). Spec section 10.2.1.1 provides the full algorithm:

1. Let callerContext = the running execution context
2. Create new execution context calleeContext
3. Create new FunctionEnvironment via NewFunctionEnvironment(F, thisArgument)
   - If F.[[ThisMode]] === 'lexical' (arrow function):
       do not bind this; it comes from the outer environment
   - If F.[[ThisMode]] === 'strict':
       this = thisArgument (any value, including undefined)
   - Otherwise (global mode):
       this = ToObject(thisArgument) (undefined/null → global object)
4. Bind argumentsList to formal parameters
5. Push calleeContext onto the execution context stack
6. Execute F.[[ECMAScriptCode]]
7. Pop calleeContext
8. Return the execution result

How [[Construct]] Executes

F.[[Construct]](argumentsList, newTarget) is the internal method triggered by new F(). newTarget is the function directly invoked with new (which may differ from F in inheritance scenarios).

[[Construct]] algorithm (base constructor):

1. thisArgument = OrdinaryCreateFromConstructor(newTarget, '%Object.prototype%')
   i.e., create new object with [[Prototype]] = newTarget.prototype
   (note: newTarget.prototype, not necessarily F.prototype)

2. Execute F.[[Call]](thisArgument, argumentsList)

3. If the function body returned an Object: use that return value
   Otherwise: use thisArgument

Return value decision tree:

  new F()
     │
     ├── body returns { ... } (an object)
     │       └── use the returned object
     │
     ├── body returns 42 (a primitive)
     │       └── ignore, use thisArgument (the newly created object)
     │
     └── body has no return / returns undefined
             └── use thisArgument
function Factory() {
  this.type = 'default'
  return { type: 'override' }  // returning an object: new uses this object
}
console.log(new Factory())  // { type: 'override' }

function Factory2() {
  this.type = 'default'
  return 42  // returning a primitive: ignored, new returns this
}
console.log(new Factory2())  // Factory2 { type: 'default' }

Internal Structure of a Bound Function

f.bind(thisArg, ...args) creates a Bound Function Exotic Object — not an ordinary ECMAScript function. It has its own internal slots:

BoundFunction Object Structure:

┌──────────────────────────────────────┐
│         BoundFunctionObject          │
├──────────────────────────────────────┤
│ [[BoundTargetFunction]] → original F │
│ [[BoundThis]]           → thisArg    │
│ [[BoundArguments]]      → [arg1...]  │
├──────────────────────────────────────┤
│ No [[Environment]] of its own        │
│ No [[ECMAScriptCode]] of its own     │
│ All calls delegate to [[BoundTarget]]│
└──────────────────────────────────────┘

The bound function's [[Call]] implementation:

BoundFunction.[[Call]](thisArgument, argumentsList):
1. F = [[BoundTargetFunction]]
2. boundThis = [[BoundThis]]
3. boundArgs = [[BoundArguments]]
4. args = boundArgs + argumentsList
5. call F.[[Call]](boundThis, args)
   note: thisArgument is discarded; boundThis always wins

Three Values of [[ThisMode]]

[[ThisMode]] is determined at function creation time based on syntax and cannot be changed:

[[ThisMode]] Syntax this Binding Behavior
lexical Arrow function () => {} Inherited from enclosing scope; not overridable
strict Strict mode function 'use strict'; function f(){} this is exactly thisArgument, even undefined
global Non-strict ordinary function function f(){} undefined/null replaced with the global object
'use strict'
function strictFn() {
  return this
}
console.log(strictFn.call(undefined))  // undefined (strict mode preserves it)
console.log(strictFn.call(42))         // 42 (primitive values preserved)

function sloppyFn() {
  return this
}
console.log(sloppyFn.call(undefined))  // globalThis (global mode substitutes)
console.log(sloppyFn.call(42))         // Number {42} (ToObject wraps primitives)

The arguments Object Sync Mechanism

In non-strict mode, arguments and named parameters maintain a live bidirectional mapping — the Arguments Exotic Object. Modifying either side reflects in the other.

Non-strict mode arguments ↔ parameter mapping:

function f(a, b) {
  // internal parameter map exists:
  // arguments[0] ↔ a (mirrored)
  // arguments[1] ↔ b (mirrored)
}

f(1, 2):
  a = 1, b = 2, arguments[0] = 1, arguments[1] = 2
  
  executing arguments[0] = 99:
    → a becomes 99 (map synchronizes)
    
  executing a = 777:
    → arguments[0] becomes 777 (map synchronizes)
function f(a) {
  arguments[0] = 99
  return a          // returns 99, not 1
}
console.log(f(1))   // 99

// strict mode: no sync
function g(a) {
  'use strict'
  arguments[0] = 99
  return a          // still returns 1
}
console.log(g(1))   // 1

// default parameters present: no sync even in non-strict mode
function h(a = 0) {
  arguments[0] = 99
  return a          // still returns 1
}
console.log(h(1))   // 1

Tail Call Optimization (TCO)

In strict mode, when a function call is in a tail position, the spec allows engines to reuse the current stack frame instead of creating a new one. This makes deep functional recursion safe.

Rules for determining tail position (spec 14.6):

Ordinary recursion (no TCO) — stack grows:

fact(5)
  └── fact(4)
        └── fact(3)
              └── fact(2)
                    └── fact(1)
                          └── fact(0) → 1
Every frame must be preserved while waiting for the result

Tail recursion (with TCO) — constant stack:

fact(5, 1)
  ↓ reuse frame
fact(4, 5)
  ↓ reuse frame
fact(3, 20)
  ↓ reuse frame
fact(2, 60)
  ↓ reuse frame
fact(1, 120)
  ↓ reuse frame
fact(0, 120) → 120
Only one frame at any time
// Non-tail recursion (stack overflow for large n)
function factorial(n) {
  if (n <= 1) return 1
  return n * factorial(n - 1)  // multiplication happens AFTER the call
}

// Tail recursion (spec allows TCO, but V8 doesn't implement it)
'use strict'
function factorial(n, acc = 1) {
  if (n <= 1) return acc
  return factorial(n - 1, n * acc)  // the call IS the last operation
}

TCO implementation status: Safari (JavaScriptCore) implements TCO in strict mode. V8 (Chrome/Node.js) explicitly refuses to implement it, citing difficulty in preserving stack traces for debugging. Do not rely on TCO in production code.


🔺 Level 3 · What the Spec Says

10.2 ECMAScript Function Objects

Spec section 10.2 defines how ECMAScript function objects are created. OrdinaryFunctionCreate is the core operation:

OrdinaryFunctionCreate(functionPrototype, sourceText, ParameterList, Body, thisMode, env, privateEnv):

1. F = OrdinaryObjectCreate(functionPrototype)
   // create ordinary object with functionPrototype as [[Prototype]]
   
2. Initialize internal slots:
   F.[[Call]] = ECMAScript function [[Call]] implementation
   F.[[FormalParameters]] = ParameterList
   F.[[ECMAScriptCode]] = Body
   F.[[Realm]] = current Realm
   F.[[Environment]] = env
   F.[[PrivateEnvironment]] = privateEnv
   F.[[ScriptOrModule]] = current script or module
   F.[[Strict]] = strict (determined by presence of 'use strict')
   
3. If thisMode === 'lexical-this':
     F.[[ThisMode]] = 'lexical'
   Else if F.[[Strict]]:
     F.[[ThisMode]] = 'strict'
   Else:
     F.[[ThisMode]] = 'global'
     
4. F.[[IsClassConstructor]] = false
5. F.[[Fields]] = []
6. F.[[PrivateMethods]] = []
7. F.[[ClassFieldInitializer]] = false
8. F.[[ConstructorKind]] = 'base'
9. Return F

At this point the function has no [[Construct]]. Only when a function declaration or expression is recognized as a non-arrow function does the spec call MakeConstructor(F) to add [[Construct]] capability:

MakeConstructor(F):
1. prototype = OrdinaryObjectCreate(%Object.prototype%)
2. Define prototype.constructor = F (non-enumerable, writable, configurable)
3. Define F.prototype = prototype (non-enumerable, writable, non-configurable)
4. Set F.[[Construct]] to the OrdinaryConstruct implementation

10.2.1.1 [[Call]] Full Algorithm

F.[[Call]](thisArgument, argumentsList):

1. callerContext = the running execution context
2. calleeContext = PrepareForOrdinaryCall(F, undefined)
   // PrepareForOrdinaryCall:
   //   a. Create new execution context
   //   b. Create new FunctionEnvironment
   //   c. Push onto execution context stack
   
3. Assert: calleeContext is now the running execution context

4. OrdinaryCallBindThis(F, calleeContext, thisArgument)
   // OrdinaryCallBindThis:
   //   If F.[[ThisMode]] === 'lexical': return (no binding)
   //   If F.[[ThisMode]] === 'strict': envRec.BindThisValue(thisArgument)
   //   Else:
   //     If thisArgument is undefined or null:
   //       thisValue = F.[[Realm]].[[GlobalEnv]].[[GlobalThisValue]]
   //     Else:
   //       thisValue = ToObject(thisArgument)
   //     envRec.BindThisValue(thisValue)
   
5. result = OrdinaryCallEvaluateBody(F, argumentsList)
   // Executes function body; handles parameter binding

6. Remove calleeContext from the execution context stack

7. If callerContext is no longer on top (TCO scenario), restore it

8. If result.[[Type]] is return: return result.[[Value]]
   Otherwise: return undefined

10.2.1.2 [[Construct]] Full Algorithm

F.[[Construct]](argumentsList, newTarget):

1. kind = F.[[ConstructorKind]]

2. If kind === 'base':
     a. thisArgument = OrdinaryCreateFromConstructor(newTarget, '%Object.prototype%')
        // thisArgument.[[Prototype]] = newTarget.prototype
        // (if newTarget.prototype is not an object, use %Object.prototype%)

3. calleeContext = PrepareForOrdinaryCall(F, newTarget)

4. If kind === 'base':
     OrdinaryCallBindThis(F, calleeContext, thisArgument)

5. result = OrdinaryCallEvaluateBody(F, argumentsList)

6. Remove calleeContext from the execution context stack

7. If result.[[Type]] is return:
     If Type(result.[[Value]]) is Object: return result.[[Value]]
     If kind === 'base': return thisArgument
     If result.[[Value]] is not undefined: throw TypeError
     // derived constructor explicitly returning a non-undefined primitive throws

8. If result.[[Type]] is not normal: propagate the error

9. If kind === 'base': return thisArgument
   // no explicit object returned → return this

10. thisBinding = calleeContext's envRec.GetThisBinding()
    If thisBinding is undefined: throw ReferenceError
    // derived constructor didn't call super() → this is uninitialized
    Return thisBinding

14.6 Tail Position Call Specification

Spec 14.6.1 defines the syntax positions that constitute tail positions. Key rules:

IsInTailPosition(call) is true when:

For FunctionBody:
  - strict code
  - call is in the Expression of a ReturnStatement in the FunctionBody

For ConciseBody (arrow function shorthand):
  - strict code
  - call IS the AssignmentExpression of the ConciseBody

For conditional expressions `a ? b : c`:
  - call is in tail position of b, or tail position of c

For logical expressions `a || b`, `a && b`, `a ?? b`:
  - call is in tail position of b (the right operand)

NOT tail positions:
  - `return f() + 1`  (addition happens after the call)
  - `return f().toString()`  (method call happens after)
  - `return [f()]`  (array construction happens after)
  - `return f(), g()`  (only the final comma expression is in tail position)

Spec 10.2.1.1 implements TCO at step 8 via PrepareForTailCall(): the current stack frame is discarded and the same slot is reused, preventing stack growth for recursive tail calls.


💎 Level 4 · Edge Cases and Traps

Trap 1: new (()=>{})() Throws TypeError

Arrow functions have no [[Construct]] internal method. The new operator checks for [[Construct]] before doing anything else; if absent, it throws TypeError immediately without executing the function body.

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

// Also throws:
// new ({method() {}}.method)  — method shorthands have no [[Construct]]
// new (f.bind(null))  where f is an arrow function

class Cls {}
const BoundCls = Cls.bind(null)
new BoundCls()  // OK — Cls has [[Construct]]; bound function delegates to it

Root cause: spec 13.3.5.1 step 7 — new expr(args) calls GetValue(expr).[[Construct]](args, expr), and if the object lacks [[Construct]], a TypeError is thrown.

Trap 2: Four Rules for Calculating length

Function.prototype.length is not a count of all parameters — it is the count of parameters before the first default parameter or rest parameter:

// Rule: length = count of parameters before the first default or rest param

function f1(a, b, c) {}           // 3
function f2(a, b = 1, c) {}       // 1 (b is default; b and after don't count)
function f3(a, ...rest) {}        // 1 (rest doesn't count)
function f4(a, b = 1, ...rest) {} // 1
function f5({ a, b }, c) {}       // 2 (destructured param counts as one)

console.log(f1.length, f2.length, f3.length, f4.length, f5.length)
// 3 1 1 1 2

// After bind: length = max(0, original length - count of bound args)
const f = function(a, b, c) {}  // length = 3
const b1 = f.bind(null)         // length = 3 (binding 'this' doesn't count)
const b2 = f.bind(null, 1)      // length = 2
const b3 = f.bind(null, 1, 2, 3, 4)  // length = 0 (can't go negative)

console.log(b1.length, b2.length, b3.length)
// 3 2 0

This rule comes from spec 10.2.10 SetFunctionLength: count parameters before the first default-value parameter or rest parameter.

Trap 3: Four Conditions That Break arguments Sync

Non-strict mode arguments syncs with named parameters, but three conditions break the sync:

// Condition 1: default parameters present — no sync even in non-strict mode
function f1(a = 0) {
  arguments[0] = 99
  return a
}
console.log(f1(1))  // 1, not 99

// Condition 2: parameters not passed — no mapping established
function f2(a, b) {
  arguments[1] = 99
  return b  // b is undefined
}
console.log(f2(1))  // undefined — only passed arguments get mapped

// Condition 3: strict mode — sync completely disabled
function f3(a) {
  'use strict'
  arguments[0] = 99
  return a  // still 1
}
console.log(f3(1))  // 1

// Condition 4: rest parameters present — arguments exists but no sync
function f4(a, ...rest) {
  arguments[0] = 99
  return a  // still 1 (has rest param → no live map)
}
console.log(f4(1))  // 1

The spec implements this sync via the Arguments Exotic Object (spec 10.4.4): the object has a [[ParameterMap]] that maps arguments indices to Environment Record bindings. Only actually-passed named parameters get entries; when default or rest parameters are present (spec 10.2.11 step 20), the entire arguments object degrades to an ordinary object with no live mappings.

Trap 4: bind Permanently Locks this

The [[BoundThis]] of a bound function is fixed permanently. Subsequent call/apply/bind cannot change it:

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

const alice = { name: 'Alice' }
const bob = { name: 'Bob' }

const greetAlice = greet.bind(alice)
console.log(greetAlice())               // 'Hello, Alice'
console.log(greetAlice.call(bob))       // 'Hello, Alice' (call has no effect)
console.log(greetAlice.apply(bob, []))  // 'Hello, Alice' (apply has no effect)

const greetAliceToo = greetAlice.bind(bob)  // re-binding has no effect
console.log(greetAliceToo())            // 'Hello, Alice' (still Alice)

One exception: when a bound function is called with new, [[BoundThis]] is ignored. Spec 10.3.2 BoundFunctionExoticObject.[[Construct]] step 6 passes newTarget to the original function's [[Construct]], which creates its own this:

function Point(x, y) {
  this.x = x
  this.y = y
}

const OriginPoint = Point.bind({ name: 'ignored' }, 0)
const p = new OriginPoint(5)
console.log(p)  // Point { x: 0, y: 5 } — not { name: 'ignored', x: 0, y: 5 }
console.log(p instanceof Point)  // true

Trap 5: Common Tail Call Misidentifications

These look like tail calls but are not:

'use strict'

// Wrong 1: operation after the call
function a(n) {
  return n > 0 ? a(n - 1) + 1 : 0  // +1 happens after a(n-1) — not tail position
}

// Wrong 2: call is inside an array literal
function b(n) {
  return [b(n - 1)]  // array construction happens after — not tail position
}

// Wrong 3: call is an argument to another function
function c(n) {
  return console.log(c(n - 1))  // c(n-1) is an argument — not tail position
}

// Correct tail call
function d(n, acc = 0) {
  if (n === 0) return acc
  return d(n - 1, acc + 1)  // the call IS the final operation — tail position
}

Chapter Summary

  1. All functions have [[Call]], but only ordinary functions and class constructors have [[Construct]]. Arrow functions and method shorthands lack [[Construct]] and cannot be called with new.
  2. [[Construct]] first creates a new object (whose [[Prototype]] comes from newTarget.prototype), then executes the function body; if the body returns an object, that is used; otherwise this is returned.
  3. Bound functions created by bind carry their own [[BoundTargetFunction]]/[[BoundThis]]/[[BoundArguments]] slots; their this cannot be overridden by subsequent call/apply/bind (except when called via new).
  4. Function.prototype.length counts parameters before the first default or rest parameter; after bind, length decreases by the number of bound arguments (minimum 0).
  5. Tail Call Optimization (TCO) is specified in section 14.6 and applies in strict mode, but V8 refuses to implement it; currently only Safari supports TCO. Do not rely on it in production.
Rate this chapter
4.7  / 5  (8 ratings)

💬 Comments