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:
- No
prototypeproperty โ cannot be used as constructors (new arrow()throwsTypeError) - No
argumentsobject โ use rest parameters...argsinstead thisis lexically bound โ inherited from the enclosing scope at definition time, not overridable bycall/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
- All functions have
[[Call]], but only ordinary functions and class constructors have[[Construct]]. Arrow functions and method shorthands lack[[Construct]]and cannot be called withnew. [[Construct]]first creates a new object (whose[[Prototype]]comes fromnewTarget.prototype), then executes the function body; if the body returns an object, that is used; otherwisethisis returned.- Bound functions created by
bindcarry their own[[BoundTargetFunction]]/[[BoundThis]]/[[BoundArguments]]slots; theirthiscannot be overridden by subsequentcall/apply/bind(except when called vianew). Function.prototype.lengthcounts parameters before the first default or rest parameter; afterbind, length decreases by the number of bound arguments (minimum 0).- 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.