Situation:
I found something strange concerning strict mode in Javascript.
- I am using an external, third-party Javascript library which
- was minified,
- has over 4000 lines of code,
- is not using
use strict
at all, and - is using
arguments.callee
.
- I am using
use strict
in my own code, scoped within a function.
When I call one of the functions provided by the library, it throws an error. However,
- the error is thrown only if I am using
use strict
- the error is thrown in all browsers except Chrome
Code:
I've removed all the unrelated stuff and reduced the code into this (online demo on jsFiddle):
// This comes from the minified external JS library.
// It creates a global object "foo".
(function () {
foo = {};
foo.bar = function (e) {
return function () {
var a5 = arguments.callee;
while (a5) {
a5 = a5.caller // Error on this line in all browsers except Chrome
}
}
}("any value here");
})();
// Here's my code.
(function() {
"use strict"; // I enable strict mode in my own function only.
foo.bar();
alert("done");
})();
Test result:
+-----------------------+-----+--------------------------------------------------------------+
| Browser | OS | Error |
+-----------------------+-----+--------------------------------------------------------------+
| Chrome 27.0.1453.94 m | Win | <<NO ERROR!>> |
| Opera 12.15 | Win | Unhandled Error: Illegal property access |
| Firefox 21.0 | Win | TypeError: access to strict mode caller function is censored |
| Safari 5.1.7 | Win | TypeError: Type error |
| IE 10 | Win | SCRIPT5043: Accessing the 'caller' property of a function or |
| | | arguments object is not allowed in strict mode |
| Chrome 27.0.1543.93 | Mac | <<NO ERROR!>> |
| Opera 12.15 | Mac | Unhandled Error: Illegal property access |
| Firefox 21.0 | Mac | TypeError: access to strict mode caller function is censored |
| Safari 6.0.4 | Mac | TypeError: Function.caller used to retrieve strict caller |
+-----------------------+-----+--------------------------------------------------------------+
Note: for OS
, Win
= Windows 7, Mac
= Mac OS 10.7.5
My understanding:
- All modern desktop browsers support
use strict
(see Can I use). - The
use strict
is scoped within my function, so everything defined outside its scope is not affected (see this Stack Overflow question).
Question:
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?
I have to use an older Telerik JS library that I cant't readily update and encountered this error this morning. One possible workaround for some people might be to use the 'setTimeout' JS function to leave strict mode before calling the loose mode function.
e.g. Change this:
To something like this:
I'm guessing this probably works because setTimeout reverts the context to the global namespace and/or leaves the scope of functionInStrictMode. I don't fully understand all the particulars. There might be better ways; I did not research this thoroughly, but I thought I'd post it here for discussion.
Preface
A couple of quick points before we get into the meat of this:
No, not at all.
IE8 is a fairly modern browser(not anymore, in 2015), and IE9 is aquitefairly 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.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
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
...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'sa shockingly bad idea(sorry, that was subjective, wasn't it?) an implementation issue (even more of one thanarguments.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:
How Chrome Gets It Right
On V8 (Chrome's JavaScript engine), the code above gives us this:
So we got a reference to the
foo.bar
function fromarguments.callee
, but then accessingcaller
on that non-strict function gave usnull
. 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 tocaller
onfoo.bar
. Returningnull
is perfectly reasonable (although I was surprised to seenull
rather thanundefined
). (We'll come back to thatnull
in the conclusions below...)How Firefox Gets It Right
SpiderMonkey (Firefox's JavaScript engine) does this:
We start out getting
foo.bar
fromarguments.callee
, but then accessingcaller
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:
As with the others, we get the
foo.bar
function fromarguments.callee
. Then trying to access that non-strict function'scaller
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:
It allows
Function#caller
on the non-strict function, and then allows it on a strict function, returningnull
. The spec is clear that that second access should have thrown an error, as it was accessingcaller
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 usingcaller
to get a reference to a strict function, even when accessingcaller
on a non-strict function. Firefox does this by throwing an error. Chrome and IE10 do it by returningnull
. They all support getting a reference to a non-strict function viacaller
(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