ECMA-262-5 in detail. Chapter 2. Strict Mode.
- Introduction
- Strict mode features
- Reasons
- Definition
- Strictness scope
- Strict mode restrictions
- Future reserved keywords
- Octal extension for literals
- Assignment to an undeclared identifier
- Assignment to read-only properties
- Shadowing inherited read-only properties
- Creating a new property of non-extensible objects
- eval and arguments restrictions
- callee and caller restrictions
- Duplications
- delete operator restrictions
- with statement
- this value restrictions
- Indirect eval call
- Conclusion
- Additional literature
Introduction
In this chapter we will concentrate on another innovation of the ECMA-262-5. The topic of the discussion is a strict variant (or strict mode) of an ECMAScript program. We’ll discuss the reasons for providing this feature, how it affects on some already existing semantic aspects and what can it restrict.
Strict mode features
Below the general provisions regarding strict variant of the ECMAScript language are described. Let’s start from the reasons for which strictness was made and definition of this mode.
Reasons
The current version of the specification — ES5 doesn’t change the syntax much. As one of the incompatible syntactic innovations can be mentioned e.g. an accessor property (getter/setter) defined in the declarative view, with using object initialiser. Although, even this new syntactic feature isn’t new for some implementations (e.g. for SpiderMonkey), which had such constructions before standardizing the ES5. Some other new syntax constructions and ideological aspects which can be based on that new syntax are leaved for the future ECMAScript version — ES6 aka Harmony. Instead, all innovations of the ES5 are made in the context of already existed ES3 syntax constructs. From this viewpoint, ES5 and namely all new meta-methods of the Object constructor (such as e.g. Object.create
and other) can be treated as just an additional library (a framework) which is “loaded” before any other library used in a project.
However, without changing the syntax, ES5 provides some important semantic innovations, as e.g. ability to control property attributes, objects extensibility, already mentioned accessor properties, etc. All these features are the good addition for the ES3.
At the same time some features of ES3 were recognized as error-prone, not enough secure or having poor errors checking at parsing stage. In addition, some ES3 features were marked as deprecated and possibly will be removed from the next version of the language. From this point of view, ES5 is a transitional version of the specification.
For that purpose, ECMA-262-5 defines a strict variant of the language. This variant excludes some specific syntactic and semantic features of the regular ECMAScript language and modifies the detailed semantics of some features. The strict variant also specifies additional error conditions that must be reported by throwing error exceptions in some situations that are not specified as errors by the non-strict form.
The strict variant of ECMAScript is commonly referred to as the strict mode of the language.
Definition
Strict mode syntax and semantics of ECMAScript is explicitly made at the level of individual ECMAScript code units. That means we can choose whether to process some code unit (e.g. a function) with the strict mode or not.
To define a strict mode we should use a Use Strict Directive of so-called a Directive Prologues.
A Directive Prologues is a sequence of directives represented entirely as string literals which followed by an optional semicolon. A Directive Prologue may be an empty sequence.
More generally (regardless ECMAScript), a Directive Prologues corresponds to a prologue code i.e. some automatically generated code, preprocessing options, or directives.
For example in ECMAScript:
// start of a directive prologue "first directive"; 'the second in the single quotes'; // end of a directive prologue
The meaning of some such directives is special. Currently, ES5 defines only one special directive — already mentioned a Use Strict Directive.
A Use Strict Directive is a directive in a Directive Prologue whose string literal is either the exact character sequences "use strict"
or 'use strict'
. A Use Strict Directive may not contain an escape sequence or line continuation. This directive is used for specifying a strict mode of a code unit.
So, to define a strict mode of some code unit we use simply:
"use strict"; // implementation of a module
Notice, that a Directive Prologue may contain more than one Use Strict Directive. However, an implementation may issue a warning if this occurs.
"use strict"; "use strict"; // may be a warning
In addition to the specification, an implementation can use any other directive with specific meaning for the particular implementation. If some directive is not recognized as a special, a warning can be issued. For example, if the implementation reserves additionally “check arguments” directives, then a warning is issued only for the third directive in the following example:
"use strict"; "check arguments"; "use paranoid"; // may be a warning
Strictness scope
As we mentioned, we can choose a strictness for a particular code unit, defining a scope of the strict variant. In the following example, we do not use the strict mode for the whole program, but use it only for the foo
function:
var eval = 10; // OK function foo() { "use strict"; alert(eval); // 10 eval = 20; // SyntaxError } foo();
We see the SyntaxError
in the second case — that’s because eval
identifier is not allowed for assignment in strict mode, although the first assignment, being evaluated in non-strict mode, occurred normally. But all such restrictions we will discuss shortly in detail.
Another thing to note, is that a Directive Prologue is required to be placed exactly at the top of a context code:
// OK, non-strict mode (function foo() { arguments = 10; "use strict"; })(); // SyntaxError, strict-mode (function foo() { "use strict"; arguments = 10; })();
It though doesn’t require the "use strict";
directive to be placed at the first position (again — it’s only the whole Directive Prologue should be placed as initial statement):
// also strict mode "use restrict"; "use strict"; a = 10; // ReferenceError
Besides, the affecting of a strict mode prolongs to all inner contexts. I.e. the context code is a strict code if either its context or any surrounding context contains a Use Strict Directive:
// define strict mode in the global context, // i.e. for the whole program "use strict"; (function foo() { // strictness is "inherited" from // the global context eval = 10; // SyntaxError (function bar() { // the same - from the global context arguments = 10; // SyntaxError })(); })();
At the same time a strict mode is specified lexically (statically) for a context — just as a closure — where this context is created, but not executed. I.e. the strictness of the context being called is not dependent on the strictness of a caller:
// globally use a non-strict mode var foo = (function () { "use strict"; return function () { alert(this); }; })(); function bar() { alert(this); } // for both functions, a caller is the global context // but "foo" is evaluated in the strict mode foo(); // undefined // meanwhile "bar" is not bar(); // object
Another important point to notice, that functions created via the Function
constructor do not inherit the strictness from the surrounding context. The following example does not throw an exception:
"use strict"; var f = Function("eval", "arguments", " \ eval = 10; arguments = 20; \ with ({a: 30}) { \ alert(a + eval + arguments); \ }" ); f(); // OK, 60
But if a strict mode directive is inside such a function, then all restrictions are available:
// non-strict globally var f = Function("eval", "'use strict'; alert(eval);"); // SyntaxError
And one more important feature related with the strictness scope is an indirect eval call, which in detail will be discussed below. As we’ll see shortly, direct eval
cannot create a variable in the environment of the calling context. However, indirect eval
call, may create a variable in the global scope, even if the global code is strict. In other words, global strictness scope does not prolong and does not affect indirect eval
s:
"use strict"; eval("var x = 10"); // direct "eval" ("indirect", eval)("var y = 20"); // indirect "eval" console.log(typeof x, typeof y); // "undefined", "number"
Repeat, the indirect eval
itself is discussed in detail below, so don’t worry if for now the code above with this ("indirect", eval)(...)
looks a bit unfamiliar; we’ll clarify it.
Now we are ready to discuss all the cases applied to the strict mode.
Strict mode restrictions
Here you can find all restrictions provided by the strict mode of the ECMA-262-5. Some of them, having similar semantics (e.g. eval
and arguments
identifiers are not allowed for assignment or function argument names) are combined in one subsection.
At the moment of writing this article (2010-06-01), the only available implementation for testing the strict mode (and the complete implementation of the ES5) — is the BESEN engine written by Benjamin Rosseaux. It of course still has some bugs (some of them I’ve reported while was writing this article and glad to see that several bugs are already fixed), but it is a good candidate for ES5 programming and testing. Implementation is open source, written in Object Pascal. You can download the sources and the IDE from which you can run your tests. Hope, some major implementation will follow soon also.
So, let’s see which restrictions we have in the strict mode of the ES5.
Future reserved keywords
The following set of identifiers is classified as future reserved keywords and cannot be used as variable or function names: implements
, interface
, let
, package
, private
, protected
, public
, static
, and yield
.
// non-strict mode var lеt = 10; // OK console.log(lеt); // 10
"use strict"; var let = 10; // SyntaxError
Notice though, that some of the identifiers are already were used as special keywords in several implementations. For example, yield
and let
are the keywords of the SpiderMonkey since 1.7 version:
// SpiderMonkey 1.7 let x = 10; console.log(x); // 10
Octal extension for literals
A conforming implementation, when processing strict mode code, may not extend the syntax of numeric literals and escape sequence with octal semantics.
In ES3, if an implementation extended the numeric literal syntax, we had the following case:
// ES3 with octal extension var x = 010; // octal number print(x); // 8 - in octal radix
Notice, that even in ES3 octal extension for numeric and string literals was only for backward compatibilities and was defined only in the Annex B (see B.1 Additional Syntax) but not in the main section of numeric or string literals. The same in the ES5 specification — octal extension may be used only for backward compatibilities and only in non-strict mode. It’s also defined in the Annex B of ES5. In strict mode, as we said, octal extension is forbidden:
"use strict"; var x = 010; // SyntaxError
The same with escape sequence, e.g. “\0<octal number>”.
This restriction mostly relates to well-known confusion with often used parseInt
function, when programmers get “unexpected” result for e.g. “08″ value:
// ES3 parseInt("07"); // 7 parseInt("08"); // 0 ? parseInt("09"); // 0 ? parseInt("010"); // 8 ?
That exactly related with the octal extension — 08
isn’t a valid octal number, so it is treated as 0
in parseInt
(notice, however, that parseInt
had in ES3 its own, independent from numeric literals, handling of octal numbers). To solve the issue a needed radix should be specified in ES3:
parseInt("08", 10); // 8
In ES5, handling of octal case in parseInt
algorithm has been removed, so now we can use it without specifying a radix in this case. Notice again, that new parseInt
algorithm is applied regardless strict mode and regardless numeric grammar extension:
// ES5, no matter, strict // or non-strict mode parseInt("08"); // 8
But, I want to remind, that parseInt
is for parsing (that’s why it’s named so) a number from a string. That means, this method does something more than just a type conversion:
parseInt("08Gb", 10); // 8
If you need just a type conversion, an alternative way can be used, e.g. applying the Number
constructor as a function — because it exactly performs ToNumber
conversion in this case. If the value of an argument is known and can be converted to number, it can be (and could in ES3) safely used without specifying any radix:
Number("08"); // 8 // alternative way +"08"; // 8
Of course, it won’t parse an argument as parseInt
does:
Number("08Gb"); // NaN
Assignment to an undeclared identifier
As we know, in ES3 assignment to an undeclared identifier creates a new property of the global object (which are often confused with global variables). This causes also well-known issue related with unintentional polluting of the global scope:
// non-strict mode (function foo() { // local vars var x = y = 20; })(); // unfortunately, "y" wasn't local // for "foo" function alert(y); // 20 alert(x); // "x" is not defined
In strict mode this feature has been removed:
"use strict"; a = 10; // ReferenceError var x = y = 20; // also a ReferenceError
There is a pair of subtle cases related with this restriction. According to 11.13.1 Simple Assignment algorithm, first — the left part of the expression is evaluated. This means, that even the grouping operator cannot help to manage such an assignment to an undeclared identifier, which actually can be already declared after the work of the grouping operator:
"use strict"; // The left part is evaluated first and // returns an unresolved reference, i.e. // at the moment - "undeclared" does not exist. // // However, in the grouping operator, which // logically should form a priority of an evaluation, // we create "undeclared" property (and even see // that alert correctly shows 10). // // But even it's created, the assignment cannot // work because we already evaluated an unresolved // reference according to the 11.13.1 algorithm // ReferenceError undeclared = (this.undeclared = 10, alert(undeclared), 10);
On the other hand, the similar case with delete
operator works logically in the correct precedence order, i.e. first — a property is deleted inside the grouping operator, and then we already try to assign a value to the already undeclared identifier:
"use strict"; // if use "prefix" (base), we may // easily assign to a new property, because // now the reference is resolved this.declared = 10; // OK // since it's delacred, we may even // change it without base declared = 20; // OK // However, with the next evaluation // we also have an error, but now // according to 8.7.2. declared = (delete this.declared, 30); // ReferenceError
In the example above, again, first — the left part is evaluated (and in this case it’s already resolved, because “declared” property exists). Then we delete the property and at the last step — try to assign a value 30 to the already unresolved reference. This case is caught by the step 3-a of the 8.7.2 PutValue algorithm.
However, regarding the delete
operator, there is one more subtle case related with removing properties of the environment records. This case we’ll consider shortly below in the section devoted to delete operator restrictions.
Assignment to read-only properties
The same situation is with read-only properties, i.e. if a property being either a data property with [[Writable]]
attribute set to false
or an accessor property without a [[Set]]
method. In the following example a TypeError
should be thrown:
"use strict"; var foo = Object.defineProperties({}, { bar: { value: 10, writable: false // by default }, baz: { get: function () { return "baz is read-only"; } } }); foo.bar = 20; // TypeError foo.baz = 30; // TypeError
However, if a property is configurable, then we can change it via Object.defineProperty
:
"use strict"; var foo = Object.defineProperty({}, "bar", { value: 10, writable: false, // read-only configurable: true // but still configurable }); // change the value Object.defineProperty(foo, "bar", { value: 20 }); console.log(foo.bar); // OK, 20 // but still can't via assignment foo.bar = 30; // TypeError
Shadowing inherited read-only properties
Notice, that it relates to inherited properties as well. If we try to shadow some read-only inherited property via assignment we also get TypeError
. And again, if we shadow the property via Object.defineProperty
it’s made normally — thus, a configurable
attribute of the inherited property doesn’t matter in this case:
"use strict"; var foo = Object.defineProperty({}, "x", { value: 10, writable: false }); // "bar" inherits from "foo" var bar = Object.create(foo); console.log(bar.x); // 10, inherited // try to shadow "x" via assignment bar.x = 20; // TypeError console.log(bar.x); // still 10, if non-strict mode // however shadowing works // if we use "Object.defineProperty" Object.defineProperty(bar, "x", { // OK value: 20 }); console.log(bar.x); // 20
In ES3 and non-strict ES5 such assignments to read-only properties failure silently.
Creating a new property of non-extensible objects
Restricted assignment also relates to augmenting non-extensible objects, i.e. objects having [[Extensible]] property as false:
"use strict"; var foo = {}; foo.bar = 20; // OK Object.preventExtensions(foo); foo.baz = 30; // TypeError
eval
and arguments
restrictions
In strict mode these names — eval
and arguments
are treated as kind of “keywords” (while they are not) and not allowed in several cases.
They cannot appear as a variable declaration or as a function name:
"use strict"; // SyntaxError in both cases var arguments; var eval; // also SyntaxError function eval() {} var foo = function arguments() {};
They are not allowed as function argument names:
"use strict"; // SyntaxError function foo(eval, arguments) {}
It is impossible to assign new values to them (thus, arguments is restricted in the global scope also, but not only in functions):
"use strict"; // SyntaxError eval = 10; arguments = 20; (function (x) { alert(arguments[0]); // 30 arguments = 40; // TypeError })(30);
They cannot be used with prefix and postfix increment/decrement expressions:
"use strict"; // SyntaxError ++eval; arguments--;
Regarding objects and their properties, eval
and arguments
are allowed in assignment expressions and may be used as property names of objects:
"use strict"; // OK var foo = { eval: 10, arguments: 20 }; // OK foo.eval = 10; foo.arguments = 20;
However, eval
and arguments
are not allowed as parameter names of declarative view of a setter:
"use strict"; // SyntaxError var foo = { set bar (eval, arguments) { ... } };
An implementation may not associate special meanings within strict mode functions to properties named caller or arguments of function instances. It is impossible to create or modify properties with these names on function objects in strict mode (caller and callee restrictions will be described in the following section):
"use strict"; function foo() { alert(foo.arguments); // SyntaxError alert(foo.caller); // SyntaxError } foo();
Also, neither eval
, nor arguments
cannot be used as an identifier of a catch
clause:
"use strict"; try { throw Error("..."); } catch (arguments) {} // SyntaxError, the same for "eval" name
Besides restrictions for these names, arguments
has additional restriction in semantics. In the strict mode it does not dynamically share its array indexed property values with the corresponding formal parameter bindings of a function. So, array indexed property values of the arguments object are just static copies:
"use strict"; (function foo(x) { alert(arguments[0]); // 10 arguments[0] = 20; alert(x); // 10, but not 20 x = 30; alert(arguments[0]); // 20, but not 30 })(10);
Implementation of eval
also has restriction in semantics in this case — it cannot instantiate a new binding (a variable or function) in the calling context (as it does in non-strict mode). Instead, strict code for eval
is executed in its own “sandbox environment”, which is being destroyed after the eval
ends:
"use strict"; eval("var x = 10; alert(x);"); // 10 alert(x); // "x" is not defined
Notice, there is an important exception from this rule — an indirect eval (will be discussed below), which allows to create a global variable regardless even that the global code is strict:
"use strict"; ("indirect", eval)("var x = 10; alert(x);"); // 10 alert(x); // 10
callee
and caller
restrictions
Just like eval
and arguments
names, callee
and caller
identifiers because of some security reasons have been restricted. For example:
// non-strict mode function foo(x, y) { alert(x); // 10 bar(); alert(x); // 100 } function bar() { console.dir(bar.caller.arguments); // 10, 20 bar.caller.arguments[0] = 100; } foo(10, 20);
In non-strict mode, arguments.callee
still can be used to reference an anonymous function from inside, e.g. for a recursive call. Unfortunately, in strict mode if we want to call a function expression recursively (or just to reference it) we should define a NFE (name function expression):
"use strict"; (function foo(bar) { if (!bar) { arguments.callee(true); // SyntaxError foo(true); // OK } })();
There is one unpleasant consequence related with removing arguments.callee
from the strict mode — now it’s not possible to reference a function created via the Function
constructor from the inside (e.g. at recursive call) if such function created not in the global context. As we know, the scope chain of Function
functions consists only of the global object. Therefore, if such function created inside other function, no any binding name of surrounding function is available for the Function function. We could call such functions in ES3 only via the arguments.callee
reference. In ES5 — there is no (at least normal) way to call such function recursively:
(function () { // outer name is not available, // regardless strictness var foo = Function("alert(foo);'"); foo(); // "foo" is not defined (no such name in the global context) // error in strict mode for arguments.callee Function("'use strict; alert(arguments.callee);'")(); // TypeError // OK in non-strict for arguments.callee Function("alert(arguments.callee);'")(); // OK, function })();
However, if such a strict function is created in the global context, we can use outer variable name to reference it:
var foo = Function("'use strict; alert(foo);'"); foo(); // function
As a variant of solving the issue, a function can be passed as an argument to itself:
(function () { var foo = Function("foo", "'use strict'; alert(foo);"); foo(foo); // OK, function })();