Preface
A couple of quick points before we get into the meat of this:
- All modern desktop browsers support
use strict
…
No, not at all. IE8 is a fairly modern browser (not anymore, in 2015), and IE9 is a quite fairly modern browser. Neither of them supports strict mode (IE9 supports parts of it). IE8 is going to be with us a long time, because it’s as high as you can go on Windows XP. Even though XP is now flatly end-of-lifed (well, you can buy a special “Custom Support” plan from MS), people will continue to use it for a while.
- The
use strict
is scoped within my function, so everything defined outside its scope is not affected
Not quite. The specification imposes restrictions on how even non-strict code uses functions created in strict mode. So strict mode can reach outside its box. And in fact, that’s part of what’s going on with the code you’re using.
Overview
So, are all browsers except Chrome wrong? Or is it the other way round? Or is this undefined behaviour so the browsers may choose to implement it in either way?
Looking into it a bit, it looks like:
-
Chrome is getting it right one way,
-
Firefox is getting it right a different way,
-
…and IE10 is getting it very slightly wrong. 🙂 (IE9 definitely gets it wrong, although not in a particularly harmful way.)
I didn’t look at the others, I figured we’d covered the ground.
The code fundamentally causing the trouble is this loop
var a5 = arguments.callee;
while (a5) {
a5 = a5.caller // Error on this line in all browsers except Chrome
}
…which relies on the caller
property of function objects. So let’s start there.
Function#caller
The Function#caller
property was never defined in the 3rd edition specification. Some implementations provided it, others didn’t. It’s a shockingly bad idea (sorry, that was subjective, wasn’t it?) an implementation issue (even more of one than arguments.caller
), particularly in multi-threaded environments (and there are multi-threaded JavaScript engines), as well as with recursive code, as Bergi pointed out in the comments on the question.
So in the 5th edition they explicitly got rid of it, by specifying that referencing the caller
property on a strict function would throw an error. (This is in §13.2, Creating Function Objects, Step 19.)
That’s on a strict function. On a non-strict function, though, the behavior is unspecified and implementation-dependent. Which is why there are so many different ways to get this right.
Instrumented Code
It’s easier to refer back to instrumented code than a debugging session, so let’s use this:
console.log("1. Getting a5 from arguments.callee");
var a5 = arguments.callee;
console.log("2. What did we get? " +
Object.prototype.toString.call(a5));
while (a5) {
console.log("3. Getting a5.caller");
a5 = a5.caller; // Error on this line in all browsers except Chrome
console.log("4. What is a5 now? " +
Object.prototype.toString.call(a5));
}
How Chrome Gets It Right
On V8 (Chrome’s JavaScript engine), the code above gives us this:
1. Getting a5 from arguments.callee 2. What did we get? [object Function] 3. Getting a5.caller 4. What is a5 now? [object Null]
So we got a reference to the foo.bar
function from arguments.callee
, but then accessing caller
on that non-strict function gave us null
. The loop terminates and we don’t get any error.
Since Function#caller
is unspecified for non-strict functions, V8 is allowed to do anything it wants for that access to caller
on foo.bar
. Returning null
is perfectly reasonable (although I was surprised to see null
rather than undefined
). (We’ll come back to that null
in the conclusions below…)
How Firefox Gets It Right
SpiderMonkey (Firefox’s JavaScript engine) does this:
1. Getting a5 from arguments.callee 2. What did we get? [object Function] 3. Getting a5.caller TypeError: access to strict mode caller function is censored
We start out getting foo.bar
from arguments.callee
, but then accessing caller
on that non-strict function fails with an error.
Since, again, the access to caller
on a non-strict function is unspecified behavior, the SpiderMonkey folks can do what they want. They decided to throw an error if the function that would be returned is a strict function. A fine line, but as this is unspecified, they’re allowed to walk it.
How IE10 Gets It Very Slightly Wrong
JScript (IE10’s JavaScript engine) does this:
1. Getting a5 from arguments.callee 2. What did we get? [object Function] 3. Getting a5.caller SCRIPT5043: Accessing the 'caller' property of a function or arguments object is not allowed in strict mode
As with the others, we get the foo.bar
function from arguments.callee
. Then trying to access that non-strict function’s caller
gives us an error saying we can’t do that in strict mode.
I call this “wrong” (but with a very lower-case “w”) because it says that we can’t do what we’re doing in strict mode, but we’re not in strict mode.
But you could argue this is no more wrong that what Chrome and Firefox do, because (again) the caller
access is unspecified behavior. So the IE10 folks decided that their implementation of this unspecified behavior would throw a strict-mode error. I think it’s misleading, but again, if it’s “wrong,” it certainly isn’t very wrong.
BTW, IE9 definitely gets this wrong:
1. Getting a5 from arguments.callee 2. What did we get? [object Function] 3. Getting a5.caller 4. What is a5 now? [object Function] 3. Getting a5.caller 4. What is a5 now? [object Null]
It allows Function#caller
on the non-strict function, and then allows it on a strict function, returning null
. The spec is clear that that second access should have thrown an error, as it was accessing caller
on a strict function.
Conclusions and Observations
What’s interesting about all of the above is that in addition to the clearly-specified behavior of throwing an error if you try to access caller
on strict functions, Chrome, Firefox, and IE10 all (in various ways) prevent your using caller
to get a reference to a strict function, even when accessing caller
on a non-strict function. Firefox does this by throwing an error. Chrome and IE10 do it by returning null
. They all support getting a reference to a non-strict function via caller
(on a non-strict function), just not a strict function.
I can’t find that behavior specified anywhere (but then, caller
on non-strict functions is entirely unspecified…). It’s probably the Right Thing(tm), I just don’t see it specified.
This code is also fun to play with: Live Copy | Live Source
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Strict and Loose Function#caller</title>
<style>
p {
font-family: sans-serif;
margin: 0.1em;
}
.err {
color: #d00;
}
</style>
</head>
<body>
<script>
function display(msg, cls) {
var p = document.createElement('p');
if (cls) {
p.className = cls;
}
p.innerHTML = String(msg);
document.body.appendChild(p);
}
// The loose functions
(function () {
function loose1() {
display("loose1 calling loose2");
loose2();
}
loose1.id = "loose1"; // Since name isn't standard yet
function loose2() {
var c;
try {
display("loose2: looping through callers:");
c = loose2;
while (c) {
display("loose2: getting " + c.id + ".caller");
c = c.caller;
display("loose2: got " +
((c && c.id) || Object.prototype.toString.call(c)));
}
display("loose2: done");
}
catch (e) {
display("loose2: exception: " +
(e.message || String(e)),
"err");
}
}
loose2.id = "loose2";
window.loose1 = loose1;
window.loose2 = loose2;
})();
// The strict ones
(function() {
"use strict";
function strict1() {
display("strict1: calling strict2");
strict2();
}
strict1.id = "strict1";
function strict2() {
display("strict2: calling loose1");
loose1();
}
strict2.id = "strict2";
function strict3() {
display("strict3: calling strict4");
strict4();
}
strict3.id = "strict3";
function strict4() {
var c;
try {
display("strict4: getting strict4.caller");
c = strict4.caller;
}
catch (e) {
display("strict4: exception: " +
(e.message || String(e)),
"err");
}
}
strict4.id = "strict4";
strict1();
strict3();
})();
</script>
</body>
</html>