This documents presents the result of the author’s reverse engineering of Function#caller and Function#arguments in latest (at the time of writing) versions of Firefox, Chrome, Safari and Edge, as well as a comparison with the proposed spec’s semantics.
Many of the results have been obtained through the tests published on
simple-tests.js.
The missing ones (most notably cross-realm interactions and the returned value of .arguments
) are left as exercise to the reader.
(For cross-realm stuff, the author didn’t dare publish the horrible hack he has resorted to in order to obtain a foreign Realm. 🤪)
More complete test coverage is found in tc39/test262#2514.
Function#caller and Function#arguments are implemented as follows:
- Magic immutable (non-writable, non-configurable) own data properties on individual functions: Chrome 79, Safari 13, Edge 18. That violates the essential invariants of internal methods, because the actual value varies. 👎
- Deletable accessors on Function.prototype: Firefox 71, proposed spec. However Firefox 71 has a custom setter that throws on censored functions and does nothing on non-censored functions; the proposed spec has no setter.
- non-function: an object that does not have a [[Call]] internal method;
- non-ECMAScript: a function whose implementation is not written in ECMAScript. That includes bound functions.
- strict: An ECMAScript function whose code is in strict mode.
That includes everything defined through the
class
construct. - generator/async: A generator function, an async function, or both.
- non-constructor: An ECMAScript function which is not a constructor, such as arrow functions, methods and accessors in object literals.
- legacy: functions that don’t fall into the previous cases,
i.e. non-strict functions that are constructed with the
function
keyword (excluding generators and asyncs) or theFunction
constructor. - cross-realm: a function that is not of the same Realm as some referenced Realm (depending on context).
Per spec, builtins ought to be either non-ECMAScript or strict.
The interaction with proxies will differ on whether the property is implemented as accessor or as data property. All implementations produce the expected result.
“Non-function” and “cross-realm” make sense only when caller
and arguments
are implemented as accessors.
✔︎ = returns null when not in the stack frame
💥 = always throws a TypeError
When the target falls in several categories (e.g. cross-realm legacy), the more severe outcome is chosen.
type of the target | Firefox 71 | Chrome 79 | Safari 13 | Edge 18 | Proposed spec |
---|---|---|---|---|---|
non-function | 💥 | N/A | N/A | N/A | 💥 |
non-ECMAScript | 💥 | 💥 | 💥 | 💥 | 💥 |
strict | 💥 | 💥 | 💥 | 💥 | 💥 |
generator/async | 💥 | 💥 | 💥 | 💥 | 💥 |
non-constructor | 💥 | 💥 | 💥 | 💥 | 💥 |
legacy | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
cross-realm | ✔︎ | N/A | N/A | N/A | 💥 |
When queried on non-censored functions, all implementations return either null (when the function is not in the stack frame), or an ordinary arguments object.
Each integer-indexed value of that object reflects:
- (A) either the value of the corresponding argument at the time of invocation,
- (B) or the current value of the corresponding parameter.
In the most basic cases, semantics (B) is chosen. The implementation switches to semantics (A) in various situations; the following table lists some of them:
✔︎ = implementations switches to semantics (A)
case | Firefox 73 | Chrome 80 | Safari 13 |
---|---|---|---|
function code references arguments , excluding delete arguments |
✔︎ | ✔︎ | ✔︎ |
function code contains delete arguments |
✔︎ | ✔︎ | |
function code contains direct eval | ✔︎ | ✔︎ | |
function code contains with statement |
✔︎ | ✔︎ | |
function has a parameter with a default value | ✔︎ | ✔︎ | ✔︎ |
function has a rest parameter | ✔︎ | ✔︎ | |
function is invoked as constructor | ✔︎ | ✔︎ | |
the corresponding parameter is used in a closure | ✔︎ | ✔︎ | ✔︎ |
function is called with less or more arguments than expected | ✔︎ | ||
function is executed many times | ✔︎ | ||
developer tools are open | ✔︎ | ||
and I bet I missed other funky cases | ✔︎ | ✔︎ |
(See Issue 12 for discussion and tests-arguments-wild.html for tests.)
This object is distinct from the one available through the arguments
binding available inside the function, and
modifications made on that arguments
binding are not reflected on the returned object. Even, every access to the .arguments property yields a distinct object (so that (function f() { return f.arguments === f.arguments })()
returns false
).
In all tested implementations, whether the .callee property of the Arguments object produced by .arguments is poisoned or not, matches whether the same condition holds on the object available through the arguments
binding inside the function. (According to the ECMA-262 spec, that should happen when the parameter list is non-simple, but not all implementations observe that.)
The returned value (when one is returned) seems to be determined by the execution context stack; i.e., it will be the function attached to the execution context that is just below the topmost execution context corresponding to the target. This can be tested in the following ways:
- it is not the “last caller” of the target when the corresponding invocation has been completed, see: ecma262#562-comment for a test;
- it is never a proxy or a bound function; instead it will be either the object they wrap, or (for proxies) the corresponding handler;
- the true caller will not be returned when the call has occurred at Proper tail call position (in implementations that support this feature).
Also, the result is independant on whether the function object has been invoked as plain function or as constructor.
✔︎ = returns the purported caller (which may be the caller of the caller, or... when PTC is at work 🤥)
⛔ = returns null
💥 = throws a TypeError
When the purported caller falls in several categories (e.g. strict non-constructor), the more severe outcome is chosen.
type of the purported caller | Firefox 71 | Chrome 79 | Safari 13 | Edge 18 | Proposed spec |
---|---|---|---|---|---|
non-ECMAScript | skipped(*) | ⛔ | ⛔ | ✔︎ 👎 | ⛔ |
strict | 💥 | ⛔ | 💥 | 💥 | ⛔ |
generator/async | ✔︎ | ✔︎ | 💥 | ✔︎ | ⛔ |
non-constructor | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
legacy | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
cross-realm | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ⛔ |
(*) When the caller is a built-in function, the caller of the caller is returned.
In the proposed spec, we purposefully remove any potential way to distinguish between non-ECMAScript functions, strict functions, and cross-realm functions.
In all circumstances, the assignments func.caller = 42
and func.arguments = 42
have no effect; in some cases, a TypeError is thrown as feedback. The precise behaviour is described by the following table:
uncensored — a function object on which getting .caller or .arguments is permitted
censored — a function object for which attempting to get .caller or .arguments throws a TypeError
⛔️ = assignment fails silently
💥 = a TypeError is thrown
operation | “own-property” or “uncensored-shadowing” Chrome 79, Safari 13, Edge 18 |
“shared-setter” Firefox 71 |
“no-setter” Proposed spec |
---|---|---|---|
uncensored.caller = 42 | ⛔️ | ⛔️ | ⛔️ |
censored.caller = 42 | 💥 | 💥 | ⛔️ |
"use strict"; uncensored.caller = 42 | 💥 | ⛔️ | 💥 |
"use strict"; censored.caller = 42 | 💥 | 💥 | 💥 |
“own-property” = a poisoning mechanism is placed on individual functions.
“uncensored-shadowing” = a setter that throws unconditionally is placed on Function.prototype, and uncensored functions have their own property that shadows the default setter.
“shared-setter” = a setter placed on Function.prototype selectively throws depending on the receiver.
“no-setter” = an accessor property without setter is placed on Function.prototype.
The migration from own properties to shared accessor without exotic behaviour implies some trade-off. The proposed spec chooses to rely on the default behaviour of failing assignment in both strict and non-strict mode.