编辑注:本文由 Chris Heilmann 发布,但作者是 Jeff Walden - 应归功于应得的人。
Mozilla 社区的开发者对 Firefox 4 中的 JavaScript 引擎进行了重大改进。我们投入了大量精力来提高性能,但也致力于开发新功能。我们尤其关注 ECMAScript 5,这是 JavaScript 底层标准的最新更新。
严格模式可以说是 ECMAScript 5 中最有趣的新功能。它是一种选择加入 JavaScript 受限变体的模式。严格模式不仅仅是一个子集:它有意与普通代码具有不同的语义。不支持严格模式的浏览器会以与支持严格模式的浏览器不同的方式运行严格模式代码,因此不要依赖严格模式,而无需对严格模式的相关方面进行功能测试。
严格模式代码和非严格模式代码可以共存,因此脚本可以逐步选择加入严格模式。严格模式为未来 ECMAScript 版本铺平了道路,在未来版本中,使用特定 <script type="...">
的新代码可能会自动以严格模式执行。
严格模式有什么作用?首先,它消除了 JavaScript 中一些不会导致错误的缺陷,将其更改为产生错误。其次,它修复了一些错误,这些错误使得 JavaScript 引擎难以执行优化:严格模式代码有时可以比相同的非严格模式代码运行得更快。Firefox 4 通常还没有优化严格模式,但后续版本将进行优化。第三,它禁止了一些可能在未来 ECMAScript 版本中定义的语法。
调用严格模式
严格模式适用于整个脚本或单个函数。它不适用于用 {}
大括号括起来的块语句;尝试将其应用于此类上下文不会有任何效果。eval
代码、事件处理程序属性、传递给 setTimeout
的字符串等等都是完整的脚本,在其中调用严格模式可以按预期工作。
脚本的严格模式
要为整个脚本调用严格模式,请在任何其他语句之前放置精确语句 "use strict";
(或 'use strict';
)。
// Whole-script strict mode syntax
"use strict";
var v = "Hi! I'm a strict mode script!";
此语法有一个陷阱,该陷阱已经困扰 一个主要网站:无法盲目地连接不冲突的脚本。考虑将严格模式脚本与非严格模式脚本连接:整个连接看起来很严格!反之亦然:非严格模式加上严格模式看起来是非严格模式。严格模式脚本之间的连接是可以的,非严格模式脚本之间的连接也是可以的。只有交叉流通过连接严格模式和非严格模式脚本,才会出现问题。
函数的严格模式
同样,要为函数调用严格模式,请在函数体内的任何其他语句之前放置精确语句 "use strict";
(或 'use strict';
)。
function strict()
{
// Function-level strict mode syntax
'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."; }
严格模式中的更改
严格模式更改了语法和运行时行为。更改通常分为以下几类
- 将错误转换为错误(作为语法错误或在运行时)
- 简化计算特定变量对给定名称的使用方式
- 简化
eval
和arguments
- 使编写“安全”JavaScript 更容易
- 预测未来的 ECMAScript 演变
将错误转换为错误
严格模式将一些先前接受的错误更改为错误。JavaScript 的设计初衷是为了让新手开发者易于使用,因此有时它会对应该导致错误的操作提供非错误语义。有时这会修复直接问题,但有时会导致将来出现更严重的问题。严格模式将这些错误视为错误,以便发现并及时修复。
首先,严格模式使得意外创建全局变量成为不可能。在普通 JavaScript 中,在赋值中误写变量会在全局对象上创建一个新属性,并继续“工作”(尽管将来可能会失败:在现代 JavaScript 中,这很可能)。在严格模式中,意外创建全局变量的赋值会抛出错误
"use strict";
mistypedVaraible = 17; // throws a ReferenceError
其次,严格模式使得会静默失败的赋值抛出异常。例如,NaN
是一个不可写的全局变量。在普通代码中,对 NaN
赋值不会有任何效果;开发者不会收到任何失败反馈。在严格模式中,对 NaN
赋值会抛出异常。任何在普通代码中静默失败的赋值都会在严格模式中抛出错误
"use strict";
NaN = 42; // throws a TypeError
var obj = { get x() { return 17; } };
obj.x = 5; // throws a TypeError
var fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // throws a TypeError
第三,如果您尝试删除不可删除的属性,严格模式会抛出错误(之前,此尝试只是没有效果)
"use strict";
delete Object.prototype; // throws a TypeError
第四,严格模式要求对象文字中命名的所有属性都是唯一的。普通代码可能会重复属性名称,最后一个属性名称决定属性的值。但由于只有最后一个属性名称起作用,因此重复只是错误的载体,如果代码被修改以更改属性值(而不是更改最后一个实例),就会发生这种情况。在严格模式中,重复的属性名称是语法错误
"use strict";
var o = { p: 1, p: 2 }; // !!! syntax error
第五,严格模式要求函数参数名称是唯一的。在普通代码中,最后一个重复的参数会隐藏先前具有相同名称的参数。这些先前的参数仍然可以通过 arguments[i]
访问,因此它们并非完全无法访问。尽管如此,这种隐藏意义不大,可能也不理想(例如,它可能会隐藏错别字),因此在严格模式中,重复的参数名称是语法错误
function sum(a, a, c) // !!! syntax error
{
"use strict";
return a + b + c; // wrong if this code ran
}
第六,严格模式禁止八进制语法。八进制语法不是 ECMAScript 的一部分,但它在所有浏览器中都支持,方法是在八进制数前面加上一个零:0644 === 420
和 "\045" === "%"
。新手开发者有时会认为,以零开头的前缀没有语义意义,因此他们将其用作对齐设备 - 但这会改变数字的含义!八进制语法很少有用,可能会被误用,因此严格模式将八进制语法视为语法错误
"use strict";
var sum = 015 + // !!! syntax error
197 +
142;
简化变量使用
严格模式简化了变量使用与代码中特定变量定义之间的映射方式。许多编译器优化依赖于能够说这个变量存储在这个位置的能力:这对于完全优化 JavaScript 代码至关重要。JavaScript 有时会使得这种基本名称到代码中变量定义的映射无法在运行时以外执行。严格模式消除了大多数发生这种情况的情况,因此编译器可以更好地优化严格模式代码。
首先,严格模式禁止使用 with
。with
的问题在于,其中的任何名称都可能在运行时映射到传递给它的对象的属性,或者映射到周围代码中的变量:无法事先知道。严格模式将 with
视为语法错误,因此 with
中的名称不可能在运行时引用未知位置
"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.
x;
}
将对象赋值给一个变量,然后访问该变量上的相应属性,这种简单的替代方案可以用来代替 with
。
其次,在严格模式代码中使用 eval
不会向周围代码中引入新变量。在普通代码中,eval("var x;")
会向周围函数或全局范围引入一个变量 x
。这意味着,一般来说,在一个包含对 eval
的调用的函数中,每个不引用参数或局部变量的名称都必须在运行时映射到一个特定的定义(因为该 eval
可能引入了一个新的变量,该变量会隐藏外部变量)。在严格模式中,eval
只为要评估的代码创建变量,因此 eval
不会影响名称是引用外部变量还是一些局部变量。
var x = 17;
var evalX = eval("'use strict'; var x = 42; x");
assert(x === 17);
assert(evalX === 42);
相关地,如果函数 eval
是通过严格模式代码中 eval(...)
形式的表达式调用的,那么该代码将作为严格模式代码进行评估。该代码可以显式调用严格模式,但这样做是不必要的。
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 iff it invokes strict mode
}
function nonstrict(str)
{
return eval(str); // str is strict iff 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!'");
第三,严格模式禁止删除普通名称。因此,严格模式 eval
代码中的名称的行为与没有作为 eval
结果进行评估的严格模式代码中的名称相同。在严格模式中使用 delete name
是语法错误
"use strict";
eval("var x; delete x;"); // !!! syntax error
使 eval
和 arguments
更简单
严格模式使得 arguments
和 eval
变得不那么神奇了。在普通代码中,两者都涉及大量的魔幻行为:eval
添加或删除绑定,并更改绑定值,arguments
通过其索引属性对命名参数进行别名。严格模式在将 eval
和 arguments
视为关键字方面取得了很大进展,尽管完整的修复要等到 ECMAScript 的未来版本。
首先,名称 eval
和 arguments
不能在语言语法中被绑定或赋值。所有这些尝试都是语法错误
"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]
的值不会跟踪相应命名参数的值,反之亦然。
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
,就必须能够提供对未内联函数的引用。对于严格模式函数,arguments.callee
是一个不可删除的属性,在设置或检索时会抛出错误。
"use strict";
var f = function() { return arguments.callee; };
f(); // throws a TypeError
“保护”JavaScript
严格模式使编写“安全”JavaScript变得更容易。一些网站现在为用户提供了一种方式,让他们编写将在网站上运行的JavaScript,代表其他用户。浏览器中的JavaScript可以访问用户的私有信息,因此此类JavaScript必须在运行之前进行部分转换,以屏蔽对禁止功能的访问。JavaScript的灵活性使得在没有许多运行时检查的情况下,有效地做到这一点是不可能的。某些语言功能非常普遍,以至于执行运行时检查会产生相当大的性能成本。一些严格模式调整,加上要求用户提交的JavaScript是严格模式代码,并且以某种方式调用,大大减少了对这些运行时检查的需求。
首先,在严格模式下作为this
传递给函数的值不会被封装到对象中。对于普通函数,this
始终是一个对象:如果使用对象值的this
调用,则为提供的对象;如果使用布尔值、字符串或数字this
调用,则为该值(被封装);或者如果使用undefined
或null
的this
调用,则为全局对象。(使用call
、apply
或bind
来指定特定的this
。)自动封装是一个性能成本,但在浏览器中暴露全局对象是一个安全隐患,因为全局对象提供了对“安全”JavaScript环境必须始终具有的功能的访问权限。因此,对于严格模式函数,指定的this
将保持不变。
"use strict";
function fun() { return this; }
assert(fun() === undefined);
assert(fun.call(2) === 2);
assert(fun.apply(null) === null);
assert(fun.call(undefined) === undefined);
assert(fun.bind(true)() === true);
(与主题相关的是,如果this
为null
或undefined
,内置方法现在也不会将this
封装到对象中。[此更改与严格模式无关,但出于对暴露全局对象的相同担忧而导致的。]历史上,将null
或undefined
传递给像Array.prototype.sort()
这样的内置方法,就像指定了全局对象一样。现在,将这两个值中的任何一个作为this
传递给大多数内置方法都会抛出TypeError
。布尔值、数字和字符串仍然会被这些方法封装:只有在这些方法原本会对全局对象进行操作时,它们才被更改。)
其次,在严格模式下,无法通过通常实现的ECMAScript扩展来“遍历”JavaScript堆栈。在使用这些扩展的普通代码中,当一个函数fun
正在被调用时,fun.caller
是最近调用fun
的函数,而fun.arguments
是该fun
调用的arguments
。这两个扩展对于“安全”JavaScript来说都有问题,因为它们允许“安全”代码访问“特权”函数及其(可能不安全)参数。如果fun
是严格模式的,那么fun.caller
和fun.arguments
都是不可删除的属性,在设置或检索时会抛出错误。
function restricted()
{
"use strict";
restricted.caller; // throws a TypeError
restricted.arguments; // throws a TypeError
}
function privilegedInvoker()
{
return restricted();
}
privilegedInvoker();
第三,严格模式函数的arguments
不再提供对相应函数调用的变量的访问权限。在一些旧的ECMAScript实现中,arguments.caller
是一个对象,其属性是该函数中变量的别名。这是一个安全隐患,因为它破坏了通过函数抽象隐藏特权值的能力;它也排除了大多数优化。由于这些原因,最近的浏览器都没有实现它。然而,由于其历史功能,严格模式函数的arguments.caller
也是一个不可删除的属性,在设置或检索时会抛出错误。
"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版本可能会引入新的语法,而ECMAScript 5中的严格模式应用了一些限制,以简化过渡。如果严格模式禁止这些更改的基础,那么进行某些更改将更容易。
首先,在严格模式下,一小部分标识符将变成保留关键字。这些词是implements
、interface
、let
、package
、private
、protected
、public
、static
和yield
。那么,在严格模式下,就不能使用这些名称来命名或使用变量或参数。Mozilla特有的注意事项:如果你的代码是JavaScript 1.7或更高版本(你是chrome代码,或者你使用了正确的<script type="">
),并且是严格模式代码,那么let
和yield
将具有自这些关键字首次引入以来的功能。但是,在网络上加载的严格模式代码,使用<script src="">
或<script>...</script>
,将无法使用let
/yield
作为标识符。
其次,严格模式禁止不在脚本或函数顶层的函数语句。在浏览器中的普通代码中,函数语句在“任何地方”都是允许的。这不是ES5的一部分!它是一个扩展,在不同的浏览器中具有不兼容的语义。未来的ECMAScript版本希望为不在脚本或函数顶层的函数语句指定新的语义。在严格模式下禁止此类函数语句为在未来的ECMAScript版本中进行规范“扫清障碍”。
"use strict";
if (true)
{
function f() { } // !!! syntax error
f();
}
for (var i = 0; i < 5; i++)
{
function f2() { } // !!! syntax error
f2();
}
function baz() // kosher
{
function eit() { } // also kosher
}
这种禁止不是严格模式本身,因为此类函数语句是一个扩展。但这是ECMAScript委员会的建议,浏览器将实现它。
浏览器中的严格模式
Firefox 4是第一个完全实现严格模式的浏览器。许多WebKit浏览器中使用的Nitro引擎也不甘落后,几乎完全支持严格模式。Chrome也开始实现严格模式。Internet Explorer和Opera尚未开始实现严格模式;请随时向这些浏览器制造商发送反馈,要求他们支持严格模式。
浏览器不稳定地实现严格模式,因此不要盲目依赖它。严格模式改变了语义。依赖这些更改会导致在不实现严格模式的浏览器中出现错误和错误。在使用严格模式时要谨慎,并通过功能测试来支持对严格模式的依赖,以检查严格模式的相关功能是否已实现。
要测试严格模式,请下载Firefox nightly并开始使用。在编写新代码和更新现有代码时,也要考虑它的限制。(但是,为了绝对安全,最好等到它在浏览器中发布后才将其用于生产环境。)
关于 Chris Heilmann
HTML5和开放网络的布道者。让我们解决这个问题!
26条评论