瘦人说

JavaScript严格模式

本文是我对《Strict mode》的翻译,文中的描述含我个人的理解,有可能和原文有所出入,望理解。

使用ECMAScript 5的严格模式(“strict mode”)会进入受限制的JavaScript模式。严格模式下的JavaScript并不是标准模式的一个子集,而是直接使用了不同语意的代码。JavaScript在支持和不支持严格模式的浏览器之间会有着不同的表现,所以功能开发时不要依赖于严格模式。严格模式的代码和非严格模式的代码是可以共存的,在严格模式下,非严格模式代码会递增地想严格模式代码转化。

严格模式下的JavaScript有很多处改变。首先要说说的是,严格模式下的JavaScript会表现得没那么多陷阱,该报错的时候就报错。另外,严格模式修复了JavaScript中的一些错误,使得JavaScript解释器更好地进行代码优化,也就是说严格模式下的代码运行地会快一些。同时,严格模式还阻止开发者使用一些可能会用于以后ECMAScript中的语法。

如何使用严格模式

严格模式可以使用于全局所有脚本代码,也可以单独使用到一个方法中。它可以使用在eval中的代码、Function中的代码、事件处理属性和在setTimeout方法中传入的字符串和全局脚本中。如果在一个以{}括号包裹住的代码块中使用严格模式是没有作用的。

全局脚本中使用严格模式

在全局脚本中使用严格模式其实很简单,只用在所有代码之前加入"use strict;"就可以了。

1
2
3
// Whole-script strict mode syntax
"use strict";
var v = "Hi! I'm a strict mode script!";

这种语法有个陷阱需要注意,它不能盲目地连接不冲突的脚本。用一段严格模式下的脚本去连接一段严格或非严格模式下的脚本,整段脚本看上去是严格的;用一段非严格模式下的脚本连接一段严格或非严格模式下脚本看上去是非严格的。但是问题就出在多段严格和非严格模式下的脚本混合组合在一起,最终无法判断整段脚本是严格的还是非严格的。所以我们推荐你把"use strict;"设置在方法级别(function_by_function)

你可以使用把整段脚本包含在一个function中,然后在方法外部使用严格模式。这可以帮助你避免混合连接脚本的问题,但是你必须把方法中的变量或方法暴露出来供外部使用。

在方法中使用严格模式

同样的,如果想要在function上使用严格模式,只要在方法中其他语句之前写上"use strict;"就可以了。

1
2
3
4
5
6
7
8
9
10
function strict () {
// Function-level strict mode syntas
'use strict';
function nested() { return 'And so am I!'; }
return 'Hi! I'm a strict mode function! ' + nested();
}
function notStrict () {
return 'I'm not strict.';
}

严格模式带来的改变

严格模式带来了语法和脚本运行时新的表现。这些改变大概可以分为两类:把设计上的缺陷转变为语法或者运行时报错,简化了一个特定变量是如何被计算的,简化了evalarguments,同时,这些改变让开发者更容易编写“安全的”JavaScript,也使得下一代的ECMAScript来得更快一些。

把设计上的缺陷转变为报错

严格模式把之前可接受的设计失误转变成了报错,JavaScript当时是为初学开发者设计的,所以有时本来需要报错的地方没有报错。这种做法有时能帮助开发者快速解决问题,但是久而久之会带来更严重的问题。严格模式下这些设计失误会变为报错,他们能被快速的发现并且修复掉。

第一,严格模式不允许偶然的创建全局变量。在非严格模式下,如果赋值语句中变量拼写错误会导致创建了一个全局变量,而且整个程序依然运行(虽然之后会产生逻辑错误)。在严格模式下发生上述情况会抛出异常。

1
2
'use strict';
mistypedVariable = 17; // throws a ReferenceError

第二,严格模式使得赋值语句当发生潜在错误时抛出异常。比如,NaN是一个全局的只读变量。非严格模式下,对NaN的赋值运算是没有意义的,而且不会报错;而在严格模式下则会抛出异常。严格模式下任何赋值语句如果有潜在错误(对只读的变量赋值,对只读属性赋值,对一个“不可扩展的”的对象新属性进行赋值)都会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';
// Assignment to a non-writable property
var obj1 = {};
Object.defineProperty(obj1, 'x', { value: 42, writable: false });
obj1.x = 9; // throw a TypeError
// Assignment to a getter-only property
var obj2 = { get x() { return 17; } };
obj2.x = 5; // throws a TypeError
// Assignment to a new property on a non-extensible object
var fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = 'ohai'; // throws a TypeError

第三,在严格模式下,如果尝试删除一个不可删除的属性也会抛出异常。

1
2
'use strict';
delete Object.prototype; // throws a TypeError;

第四,严格模式要求一个对象上的所有属性名都必须是唯一的。非严格模式下是允许重名变量的,而且是以最后一次声明的值作为此属性的值。因为只有最后一次声明才会起作用,所以在修改某属性值时,改的不时最后一个声明的属性,那么最终的值也没有被修改。在严格模式下,重复的属性名回报出语法错误:

1
2
'use strict';
var o = { p: 1, p: 2 }; // !!! syntax error

第五,严格模式要求一个方法的所有参数名都必须时唯一的。非严格模式下存在重名参数,在使用时也只有最后一个声明的参数会被调用。但是之前参数依然能通过arguments[i]的方式访问到,所以他们并不是完全不能被访问到。当然,这种设计可能不是我们想要的。在严格模式下重名的参数会报语法错误:

1
2
3
4
function sum(a, a, c) { // !!! syntax error
'use strict';
return a + b + c; // wrong if this code ran
}

第六,严格模式禁止八进制的语法。八进制语法不属于ECMAScript,但是所有浏览器都支持八进制语法,只要在八进制数字之前加上一个00644 === 420"\045" === "%"。初学开发者觉得数字之前的0没有语意,只是为了让数字对齐 —— 但是,这样做确实改变了数字的意义!八进制语法几乎没什么用并且还会被错误的使用,所以在严格模式下会抛出语法错误:

1
2
3
4
'use strict';
var sum = 015 + // !!! syntax error
197 +
142

简化变量的使用

严格模式简化了变量名是如何映射到变量的定义的。很多解释器的优化依赖于解释器了解变量和变量的存储映射关系:这是全面优化JavaScript代码非常关键的一点。JavaScript代码有时在运行时才能找到变量名到变量定义之间的映射。严格模式下移除了大部分这样的情况,解释器也能发挥更好的优化作用。

首先,严格模式下禁止使用with,原因是因为在调用with的代码块中的任何命名有可能指向一个对象的属性,或者指向一个外部的(甚至是全局的)变量,在运行时解释器很难知道该变量指代的对象是什么。这种情况在严格模式下会抛出语法错误,也就避免了在with的代码块中存在不确定指向的变量命名:

1
2
3
4
5
6
7
8
'use strict';
var x = 17;
with(obj) { // !!! syntax error
// if this weren't strict mode, would this be var x, or
// would it instead be obj.x? It's impossible in general
// to say without running the code, so the name can't be
// optimized.
}

替代使用with的简单的方式可以选择直接访问变量上的对应属性进行赋值运算。

另外,严格模式下的eval方法调用不会给当前作用域上添加新的变量。在非严格模式下,eval("var x;")将会创建新的变量x在当前的方法作用域上或者全局作用域上。这就意味着,通常情况下如果一个方法包含了eval语句,并且eval语句创建了新的和方法中某个参数或变量同名的变量,有可能会影响到他它们的值。但是在严格模式下,eval创建出来的变量只属于eval中代码被执行时的作用域,所以eval并不会影响其他局部本两或者参数:

1
2
3
4
var x = 17;
var evalX = eval("'use strict'; var x = 42; x");
assert(x === 17);
aseert(evalX === 42);

如果eval方法被另一个严格模式下的eval方法调用,那么被调用的eval表达式运行时也会被当作在严格模式下运行,而且这段代码可以显示的设置上严格模式,但是不是必须的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function strict1 (str) {
'use strict';
return eval(str); // str will be treated as strict mode code
}
function strict2 (f, str) {
'use strict';
return f(str); // not eval(..): str is strict if and only if it invokes strict mode
}
function nonstrict (str) {
return eval(str); // str is strict if and only if it invokes strict mode
}
strict1("'Strict mode code!'");
strict1("'use strict'; 'Strict mode code!'");
strict2(eval, "'Non-strict code.'");
strict2(eval, "'use strict'; 'Strict mode code!'");
nonstrict("'Non-strict code.'");
nonstrict("'use strict'; 'Strict mode code!'");

最后,严格模式下禁止删除一个变量名称。delete name在严格模式下会抛出语法错误:

1
2
'use strict';
eval('var x; delete x;'); // !!! syntax error

或者

1
2
3
4
5
function strict1 () {
'use strict';
var x = 10;
delete x; // !!! syntax error
}

让使用evalarguments更加简单

严格模式让argumentseval变得没那么神秘莫测。过去在非严格模式下它们都表现得比较诡异:eval有可能影响外部作用域的变量值,而arguments可以通过索引可以访问到参数,而且只是作为参数的一个别名而已。严格模式尽可能的把evalarguments只是当成关键字来使用,完整的修复可能需要等到新版本的ECMAScript的出现。

首先,从语法层面就限制了evalarguments不能被使用到声明和赋值语句中。所有的尝试都会当成语法错误:

1
2
3
4
5
6
7
8
9
10
11
'use strict';
eval = 17;
arguments++;
++eval;
var obj = { set p(arguments) {} };
var eval;
try {} catch (arguments) {}
function x (eval) {}
function arguments () {}
var y = function eval () {};
var f = new Function('arguments', '"use strict"; return 17;');

另外,严格模式下的arguments通过索引访问到的参数并不是参数的别名。在非严格模式下,假设有一个方法,方法的第一个参数叫做arg,对arg的修改同时也会修改了arguments[0],反之亦然(除非没有设置参数或者arguments[0]被删除了)。在严格模式下,arguments只是保存了方法调用时候初始的参数值。arguments[i]不会跟踪对应的参数值,反过来一个参数的改变也不会影响对应的arguments[i]

1
2
3
4
5
6
7
8
9
function f (a) {
'use strict';
a = 42;
return [a, arguments[0]];
}
var pair = f(17);
assert(pair[0] === 42);
assert(pair[1] === 17);

还有,严格模式下不再支持arguments.callee。在非严格模式下arguments.callee指向了当前调用的方法。arguments.callee本质上阻碍的解释器的优化,因为它必须保持了一个指向当前方法的应用。在严格模式下,arguments.callee变成一个不能被删除的属性,当赋值和取值时都会抛出异常。

1
2
3
'use strict';
var f = function () { return arguments.callee; };
f(); // throws a TypeError

“安全的”JavaScript

严格模式让开发者更容易编写“安全的”JavaScript。有些网站可以让用户编写JavaScript,这些脚本会被其他用户的浏览器或网站运行。浏览器中的JavaScript将能够访问到用户的私密信息,所以这段JavaScript能被部分地在运行前转变,去连接被禁止的功能。为了做到“安全的”JavaScript,需要有多次的运行时检查灵活多变的JavaScript代码。但是多次的运行时检查可能带来大量的性能损失。在严格模式下的一些调整,用户提交的JavaScript代码会以某种方式执行,有效的减少了运行时检查的需要。

首先,在严格模式下,当值作为this传入一个方法时(调用call, apply, bind方法时),不会被装箱成一个对象。非严格模式下,对于一个方法来说,如果作为this传入的值是一个对象,那么方法中的this就是一个对象;如果传入的是Booleanstringnumber类型的话,方法中的this就是装箱之后的值;如果传入的值是undefined或者null,那么方法中的this就是全局对象。自动装箱会造成性能损失,而且暴露出来的浏览器全局对象又会造成安全隐患,因为全局对象可以连接功能“安全的”JavaScript环境,这是需要被限制的。因此,对于一个严格模式下的方法,指定的this被视为未改变的:

1
2
3
4
5
6
7
'use strict';
function fun () { return this; }
assert(fun() === undefined); // this is unchanged, before is global variable
assert(fun.call(2) === 2);
assert(fun.apply(null) === null);
assert(fun.call(undefined) === undefined);
assert(fun.bind(true)() === true);

另外,严格模式下不能使用ECMAScript的通用实现的扩展去“查看”JavaScript的堆栈。通常实现的扩展程序中,当方法fun被另一个方法调用时,fun.caller指向了最近调用fun的方法,fun.arguments指向了fun方法调用时候的参数。两个属性的访问都时被严格模式限制的,因为它们允许“安全的”代码访问“特权函数”及其(有隐患的)参数。如果fun处于严格模式下,fun.callerfun.arguments都是不可删除的属性,在设值和取值的时候会抛出异常:

1
2
3
4
5
6
7
8
9
10
11
function restricted () {
'use restrict';
restricted.caller; // throws a TypeError
restricted.arguments; // throws a TypeError
}
function privilegedInvoker () {
return restricted();
}
privilegedInvoker();

还有,在严格模式下,方法的arguments不能访问到方法调用时的变量。在较老版本的ECMAScript实现的arguments.caller是一个对象,对象的属性是指向了该方法调用时参数的别名。这也是一个安全隐患,因为它不能通过方法抽象来隐藏特权值;也会给解释器优化带来困难。目前来说,还没有近代浏览器实现它。因为一些历史原因,严格模式下的方法中arguments.caller是一个不可被删除的属性,并且设值和取值时都会抛出异常:

1
2
3
4
5
6
7
8
'use strict';
function fun (a, b) {
'use strict';
var v = 12;
return arguments.caller; // throws a TypeError
}
fun(1, 2); // doesn't expose v (or a or b)

为未来的ECMAScript铺路

未来的ECMAScript版本将可能带来新的语法,ECMAScript5中的严格模式应用了多种限制来缓和即将来临的变化。严格模式限制了这些改变的核心部分,使得ECMAScript改变起来更加容易一些。

首先,严格模式下一些标识变成了保留关键字:implementsinterfaceletpackageprivateprotectedprotectedpublicstaticyield。所以你不能把它们用作变量名和参数名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function package (protected) { // !!!
'use strict';
var implements; // !!!
interface: // !!!
while (true) {
break interface; // !!!
}
function private () {} // !!!
}
function fun (static) { // !!!
'use strict';
}

在Firefox下有两个特殊的地方:第一,如果你的代码使用的是JavaScript1.7或以上(你使用的时Chrome,或者你使用了正确的<script type="">),并且使用了严格模式,letyield在它们第一次引入的时候就已经可以使用。但是如果脚本是使用<script src=""><script>…</script>加载的,那么你不可以使用let/yield作为标识符。第二,ES5还保留了更多的关键字:classenumexportexportextendimportsuper,在Firefox5之前,它们只在严格模式下保留。

另外,严格模式禁止在一段脚本或一个方法内有不标准的方法声明。在非严格模式下,浏览器允许在任何地方声明方法。这种方式并不是来自ES5,甚至不是来自ES3!,它是不同浏览器不同的实现导致的。未来的ECMAScript版本将会为不标准的方法声明定义新的语意。严格模式下禁止了这种方法申明,为未来的ECMAScript标准的定制扫清障碍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'use strict';
if (true) {
function f () { } // !!! syntax error
f();
}
for (var i = 0 ; i < 5 ; i ++) {
function f2 () {} // !!! syntax error
f2();
}
function baz (){ // kosher, clear
function eit () {} // also kosher, clear
}

这种限制不是严格模式特有的,因为这样的方法申明是基础的ES5的一个扩展。并且它已经成为ECMAScript委员会推荐的方式,浏览器在不久的将来会实现这种行为。

浏览器下的严格模式

浏览起还没有完全实现严格模式,所以不要盲目的依赖它。严格模式会改变脚本的语意。如果浏览器还没有实现某种严格模式下的行为,依赖这些行为的地方将会导致错误。使用的时候需要保持练习的心态,通过功能测试来保证你使用的严格模式都被浏览器实现了。最后,确保你测试过你的代码在不支持严格模式的浏览器也是可以运行的,否则,你可能会遇到些问题,反之亦然。

更多阅读

Comments

Proudly published with Hexo