ECMAScript 5的严格模式是采用具有限制性JavaScript变体的一种方式,从而使代码显示地 脱离马虎模式/稀松模式/懒散模式(sloppy)模式。
严格模式不仅仅是一个子集:它的产生是为了形成与正常代码不同的语义。
不支持严格模式与支持严格模式的浏览器在执行严格模式代码时会采用不同行为。
所以在没有对运行环境展开特性测试来验证对于严格模式相关方面支持的情况下,就算采用了严格模式也不一定会取得预期效果。严格模式代码和非严格模式代码可以共存,因此项目脚本可以渐进式地采用严格模式。
严格模式对正常的 JavaScript语义做了一些更改。
静默错误。JavaScript引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快。ECMAScript的未来版本中可能会定义的一些语法。主流浏览器现在实现了严格模式。
但是不要盲目的依赖它,因为市场上仍然有大量的浏览器版本只部分支持严格模式或者根本就不支持(比如IE10之前的版本)。
严格模式改变了语义。依赖这些改变可能会导致没有实现严格模式的浏览器中出现问题或者错误。
谨慎地使用严格模式,通过检测相关代码的功能保证严格模式不出问题。
最后,记得在支持或者不支持严格模式的浏览器中测试你的代码。
如果你只在不支持严格模式的浏览器中测试,那么在支持的浏览器中就很有可能出问题,反之亦然。
严格模式可以应用到整个脚本或个别函数中。
不要在封闭大括弧 {} 内这样做,在这样的上下文中这么做是没有效果的。在 eval 、Function 、内联事件处理属性、 WindowTimers.setTimeout() 方法中传入的脚本字符串,其行为类似于开启了严格模式的一个单独脚本,它们会如预期一样工作。
为整个脚本文件开启严格模式,需要在所有语句之前放一个特定语句 'use strict'; (或 'use strict';)
// 整个脚本都开启严格模式的语法
'use strict';
const v = "Hi! I'm a strict mode script!";
这种语法存在陷阱,Amazon被它坑倒了:不能盲目的合并冲突代码。
试想合并一个严格模式的脚本和一个非严格模式的脚本:合并后的脚本代码看起来是严格模式。
反之亦然:非严格合并严格看起来是非严格的。
合并均为严格模式的脚本或均为非严格模式的都没问题,只有在合并严格模式与非严格模式有可能有问题。
建议按一个个函数去开启严格模式(至少在学习的过渡期要这样做).
您也可以将整个脚本的内容用一个函数包括起来,然后在这个外部函数中使用严格模式。这样做就可以消除合并的问题,但是这就意味着您必须要在函数作用域外声明一个全局变量。
同样的,要给某个函数开启严格模式,得把 "use strict"; (或 'use strict'; )声明一字不漏地放在函数体所有语句之前。
(() => {
const strict = ()=> {
// 函数级别严格模式语法
'use strict';
const nested = () => { return "And so am I!"; }
return "Hi! I'm a strict mode function! " + nested();
}
const notStrict = () => { return "I'm not strict."; }
})();
严格模式同时改变了语法及运行时行为。
变化通常分为这几类
错误(如语法错误或运行时错误)名称的特定变量计算eval 以及 arguments, 将写安全JavaScript的步骤变得更简单,以及改变了预测未来ECMAScript行为的方式。在严格模式下, 某些先前被接受的过失错误将会被认为是异常. JavaScript被设计为能使新人开发者更易于上手, 所以有时候会给本来错误操作赋予新的不报错误的语义(non-error semantics).
有时候这可以解决当前的问题, 但有时候却会给以后留下更大的问题. 严格模式则把这些失误当成错误, 以便可以发现并立即将其改正.
禁用
with
严格模式禁用 with.
with所引起的问题是块内的任何名称可以映射(map)到with传进来的对象的属性, 也可以映射到包围这个块的作用域内的变量(甚至是全局变量), 这一切都是在运行时决定的: 在代码运行之前是无法得知的.
严格模式下, 使用 with 会引起语法错误, 所以就不会存在 with 块内的变量在运行是才决定引用到哪里的情况了:
(() => {
'use strict';
const x = 17;
with (obj) // Uncaught SyntaxError: Strict mode code may not include a with statement
{
// 如果没有开启严格模式,with中的这个x会指向with上面的那个x,还是obj.x?
// 如果不运行代码,我们无法知道,因此,这种代码让引擎无法进行优化,速度也就会变慢。
x;
}
})()
一种取代 with的简单方法是,将目标对象赋给一个短命名变量,然后访问这个变量上的相应属性.
引入新变量
严格模式下的 eval 不再为上层范围(surrounding scope,注:包围eval 代码块的范围)引入新变量.
在正常模式下, 代码 eval("var x;") 会给上层函数(surrounding function)或者全局引入一个新的变量 x .
这意味着, 一般情况下, 在一个包含 eval 调用的函数内所有没有引用到参数或者局部变量的名称都必须在运行时才能被映射到特定的定义 (因为 eval 可能引入的新变量会覆盖它的外层变量).
在严格模式下 eval 仅仅为被运行的代码创建变量, 所以 eval 不会使得名称映射到外部变量或者其他局部变量
(() => {
const {assert, log} = console;
const x = 17;
const evalX = eval("'use strict'; const x = 42; x");
log(assert(x !== 17,'yes it is 17'));// Assertion failed: yes it is 17
log(assert(evalX !== 42,'yes it is 42'));// Assertion failed: yes it is 42
})()
相应的, 如果函数 eval 被在严格模式下的eval(...)以表达式的形式调用时, 其代码会被当做严格模式下的代码执行.
当然也可以在代码中显式开启严格模式, 但这样做并不是必须的.
(() => {
const { log } = console;
const strict1 = (str)=>{
'use strict';
return eval(str); // str中的代码在严格模式下运行
}
const strict2 = (f, str)=>{
'use strict';
return f(str); // 没有直接调用eval(...): 当且仅当str中的代码开启了严格模式时
// 才会在严格模式下运行
}
const nonStrict = (str)=>{
return eval(str); // 当且仅当str中的代码开启了"use strict",str中的代码才会在严格模式下运行
}
log(strict1("'Strict mode code!'")); // Strict mode code!
log(strict1("'use strict'; 'Strict mode code!'")); // Strict mode code!
log(strict2(eval, "'Non-strict code.'")); // Non-strict code.
log(strict2(eval, "'use strict'; 'Strict mode code!'")); // Strict mode code!
log(nonStrict("'Non-strict code.'")); // Non-strict code.
log(nonStrict("'use strict'; 'Strict mode code!'")); // Strict mode code!
})()
因此,在 eval 执行的严格模式代码下,变量的行为与严格模式下非 eval 执行的代码中的变量相同。
严格模式禁止删除声明变量
delete name 在严格模式下会引起语法错误
(() => {
'use strict';
// const x;// Uncaught SyntaxError: Missing initializer in const declaration
let x;
delete x; // Uncaught SyntaxError: Delete of an unqualified identifier in strict mode.
// Uncaught SyntaxError: Delete of an unqualified identifier in strict mode.
eval("let y; delete y;");
})()
eval和arguments #严格模式让arguments和eval少了一些奇怪的行为。
两者在通常的代码中都包含了很多奇怪的行为: eval会添加删除绑定,改变绑定好的值,还会通过用它索引过的属性给形参取别名的方式修改形参.
虽然在未来的ECMAScript版本解决这个问题之前,是不会有补丁来完全修复这个问题,但严格模式下将eval 和arguments作为关键字对于此问题的解决是很有帮助的。
名称
eval和arguments不能通过程序语法被绑定(be bound)或赋值. 以下的所有尝试将引起语法错误:
(() => {
'use strict';
eval = 17; // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
arguments++; // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
++eval; // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
const obj = { set p(arguments) { } }; // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
const eval; // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
try { } catch (arguments) { } // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
function x(eval) { } // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
function arguments() { } // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
const y = function eval() { }; // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
const f = new Function("arguments", "'use strict'; return 17;"); // Uncaught SyntaxError: Unexpected eval or arguments in strict mode
})()
严格模式下,参数的值不会随 arguments 对象的值的改变而变化。
在正常模式下,对于第一个参数是 arg 的函数,对 arg 赋值时会同时赋值给 arguments[0],反之亦然(除非没有参数,或者 arguments[0] 被删除)。严格模式下,函数的 arguments 对象会保存函数被调用时的原始参数。
arguments[i] 的值不会随与之相应的参数的值的改变而变化,同名参数的值也不会随与之相应的 arguments[i] 的值的改变而变化。
(() => {
const {assert} = console;
function f(a){
'use strict';
a = 42;
return [a, arguments[0]];
}
const pair = f(17);
assert(pair[0] === 42);
assert(pair[1] === 17);
})()
arguments对象和函数属性
在严格模式下,访问arguments.callee, arguments.caller, anyFunction.caller以及anyFunction.arguments都会抛出异常.
arguments.callee
正常模式下,arguments.callee 指向当前正在执行的函数。这个作用很小,唯一合法的使用应该是给匿名函数声明并且重用之。
此外,arguments.callee 十分不利于优化,例如内联函数,因为 arguments.callee 会依赖对非内联函数的引用。
在严格模式下,arguments.callee 是一个不可删除属性,而且赋值和读取时都会抛出异常。
// example taken from vanillajs: http://vanilla-js.com/
const s = document.getElementById('thing').style;
s.opacity = 1;
(()=>{
if((s.opacity-=.1) < 0){
s.display="none";
}
else{
setTimeout(arguments.callee, 40);
}
})();
可以重新写成:
(() => {
'use strict';
const s = document.getElementById('thing').style;
s.opacity = 1;
(function fadeOut(){ // name the function
if((s.opacity-=.1) < 0){
s.display = "none";
}else{
setTimeout(fadeOut, 40); // use the name of the function
}
})();
})();
arguments.caller
在一些旧时的ECMAScript实现中arguments.caller曾经是一个对象,里面存储的属性指向那个函数的变量。
这是一个安全隐患,因为它通过函数抽象打破了本来被隐藏起来的保留值;它同时也是引起大量优化工作的原因。
出于这些原因,现在的浏览器没有实现它。但是因为它这种历史遗留的功能,arguments.caller在严格模式下同样是一个不可被删除的属性,在赋值或者取值时会报错
(() => {
'use strict';
function fun(a, b)
{
'use strict';
var v = 12;
return arguments.caller; // 抛出类型错误
}
fun(1, 2); // 不会暴露v(或者a,或者b)
})();
anyFunction.caller
在严格模式中再也不能通过广泛实现的ECMAScript扩展游走于JavaScript的栈中。
在普通模式下用这些扩展的话,当一个叫fun的函数正在被调用的时候,fun.caller是最后一个调用fun的函数,而且fun.arguments包含调用fun时用的形参。
这两个扩展接口对于安全JavaScript而言都是有问题的,因为他们允许安全的代码访问专有函数和他们的(通常是没有经过保护的)形参。
如果fun在严格模式下,那么fun.caller和fun.arguments都是不可删除的属性而且在存值、取值时都会报错
(() => {
function restricted()
{
'use strict';
// Script snippet #6:5 Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
restricted.caller; // 抛出类型错误
restricted.arguments; // 抛出类型错误
}
function privilegedInvoker()
{
return restricted();
}
privilegedInvoker();
})();
严格模式下更容易写出安全的JavaScript。
现在有些网站提供了方式给用户编写能够被网站其他用户执行的JavaScript代码。
在浏览器环境下,JavaScript能够获取用户的隐私信息,因此这类Javascript必须在运行前部分被转换成需要申请访问禁用功能的权限。
没有很多的执行时检查的情况,Javascript的灵活性让它无法有效率地做这件事。
一些语言中的函数普遍出现,以至于执行时检查他们会引起严重的性能损耗。
做一些在严格模式下发生的小改动,要求用户提交的JavaScript开启严格模式并且用特定的方式调用,就会大大减少在执行时进行检查的必要。
在严格模式下通过this传递给一个函数的值不会被强制转换为一个对象。
对一个普通的函数来说,this总会是一个对象:不管调用时this它本来就是一个对象;还是用布尔值,字符串或者数字调用函数时函数里面被封装成对象的this;还是使用undefined或者null调用函数式this代表的全局对象(使用call, apply或者bind方法来指定一个确定的this)。
这种自动转化为对象的过程不仅是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患,因为全局对象提供了访问那些所谓安全的JavaScript环境必须限制的功能的途径。
所以对于一个开启严格模式的函数,指定的this不再被封装为对象,而且如果没有指定this的话它值是undefined。
(() => {
const { assert, log } = console;
'use strict';
function fun() { return this; }
log(fun());// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
assert(fun() === undefined, 'it is not undefined');// Assertion failed: it is not undefined
log(fun.call(2));// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
assert(fun.call(2) === 2, 'it is not 2');// Assertion failed: it is not 2
log(fun.apply(null));// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
assert(fun.apply(null) === null, 'it is not null');// Assertion failed: it is not null
log(fun.call(undefined));// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
assert(fun.call(undefined) === undefined);// Assertion failed: it is not undefined
log(fun.bind(true));// ƒ fun() { return this; }
assert(fun.bind(true)() === true, 'it is not true');// Assertion failed: it is not true
})();
为未来的ECMAScript版本铺平道路
未来版本的ECMAScript很有可能会引入新语法,ECMAScript5中的严格模式就提早设置了一些限制来减轻之后版本改变产生的影响。如果提早使用了严格模式中的保护机制,那么做出改变就会变得更容易。
新增保留关键字
在严格模式中一部分字符变成了保留的关键字。
这些字符包括implements, interface, let, package, private, protected, public, static和yield。
在严格模式下,你不能再用这些名字作为变量名或者形参名。
(() => {
function package(protected){ // !!!
'use strict';
var implements; // !!!
interface: // !!!
while (true)
{
break interface; // !!!
}
function private() { } // !!!
}
function fun(static) { 'use strict'; } // !!!
})();
两个针对Mozilla开发的警告:第一,如果你的JavaScript版本在1.7及以上(你的chrome代码或者你正确使用了<script type="">)并且开启了严格模式的话,因为let和yield是最先引入的关键字,所以它们会起作用。
但是网络上用<script src="">或者<script>...</script>加载的代码,let或者yield都不会作为关键字起作用;
第二,尽管ES5无条件的保留了class, enum, export, extends, import和super关键字,在Firefox 5之前,Mozilla仅仅在严格模式中保留了它们。
禁止内部函数声明
严格模式禁止了不在脚本或者函数层面上的函数声明。
在浏览器的普通代码中,在所有地方的函数声明都是合法的。
这并不在ES5规范中(甚至是ES3)!这是一种针对不同浏览器中不同语义的一种延伸。
未来的ECMAScript版本很有希望制定一个新的,针对不在脚本或者函数层面进行函数声明的语法。
在严格模式下禁止这样的函数声明对于将来ECMAScript版本的推出扫清了障碍:
(() => {
'use strict';
if (true){
function f() { } // !!! 语法错误
f();
}
for (var i = 0; i < 5; i++){
function f2() { } // !!! 语法错误
f2();
}
function baz() { // 合法
function eit() { } // 同样合法
}
})();
这种禁止放到严格模式中并不是很合适,因为这样的函数声明方式从ES5中延伸出来的。但这是ECMAScript委员会推荐的做法,Mozilla浏览器实现了这一点,但是在chrome中没有实现。
如果代码中使用'use strict'开启了严格模式,则下面的情况都会在脚本运行之前抛出SyntaxError异常:
const n = 023和const s = "\047"。with语句。变量名(而不是属性名):delete myVariable。eval或arguments作为变量名或函数名。未来保留字(也许会在ECMAScript 6中使用):implements, interface, let, package, private, protected, public, static,和yield作为变量名或函数名。语句块中使用函数声明:if(a<b){ function f(){} }。对象字面量中使用两个相同的属性名:{a: 1, b: 3, a: 7}。函数形参中使用两个相同的参数名:function f(a, b, b){}。这些错误是有利的,因为可以揭示简陋的错误和坏的实践,这些错误会在代码运行前被抛出
严格模式下无法再意外创建全局变量
在普通的JavaScript里面给一个错误命名的变量名赋值会使全局对象新增一个属性并继续工作(尽管将来可能会失败:在现代的JavaScript中有可能)。
严格模式中意外创建全局变量被抛出错误替代:
(() => {
'use strict';
mistypedVaraible = 17;
// 这一行代码就会抛出 ReferenceError
// Uncaught ReferenceError: mistypedVaraible is not defined
})()
重名属性
在Gecko版本34之前,严格模式要求一个对象内的所有属性名在对象内必须唯一。正常模式下重名属性是允许的, 重名参数名会掩盖之前的重名参数,最后一个重名的属性决定其属性值。
之前的参数仍然可以通过 arguments[i] 来访问, 还不是完全无法访问.
因为只有最后一个属性起作用,当代码要去改变属性值而不是修改最后一个重名属性的时候,复制这个对象就产生一连串的bug。
然而, 这种隐藏毫无意义而且可能是意料之外的 (比如它可能本来是打错了), 所以在严格模式下重名参数被认为是语法错误。
严格模式中意外创建全局变量被抛出错误替代:
'use strict';
// 假如有一个全局变量叫做mistypedVariable
mistypedVaraible = 17; // 因为变量名拼写错误
// 这一行代码就会抛出 ReferenceError
// Uncaught ReferenceError: mistypedVaraible is not defined
JavaScript曾经会在一些上下文的某些情况中静默的失败,严格模式会在这些情况下抛出错误。
如果你的代码包含这样的场景,请务必测试以确保没有代码受到影响。
再说一次,严格模式是可以设置在代码粒度下的。
未声明的变量赋值
(() => {
const f = (x) => {
'use strict';
const a = 12;
b = a + x*35; // Uncaught ReferenceError: b is not defined
}
f();
})()
改变一个全局对象的值可能会造成不可预期的后果。
如果你真的想设置一个全局对象的值,把他作为一个参数并且明确的把它作为一个属性:
(() => {
const global = this;
// in the top-level context,
// "this" always refers the global object
const f = (x) => {
'use strict';
const a = 12;
global.b = a + x * 35;
}
f();
})()
尝试删除一个不可配置的属性
(() => {
'use strict';
delete Object.prototype; // Uncaught TypeError: Cannot delete property 'prototype' of function Object() { [native code] }
})()
在非严格模式中,这样的代码只会静默失败,这样可能会导致用户误以为删除操作成功了.
尝试修改一个不可修改的变量
(() => {
'use strict';
NaN = 111; // 抛出TypeError错误
// Uncaught TypeError: Cannot assign to read only property 'NaN' of object '#<Window>'
})()
在非严格模式中,这样的代码只会静默失败,这样可能会导致用户误以为修改操作成功了.
尝试修改一个不可配置的属性
任何在正常模式下引起静默失败的赋值操作 (给不可写属性赋值, 给只读属性(getter-only)赋值, 给不可扩展对象(non-extensible object)的新属性赋值) 都会抛出异常:
(() => {
'use strict';
// 给不可写属性赋值
const obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9; // 抛出TypeError错误
// Uncaught TypeError: Cannot assign to read only property 'x' of object '#<Object>'
// 给只读属性赋值
const obj2 = { get x() { return 17; } };
obj2.x = 5; // 抛出TypeError错误
// Uncaught TypeError: Cannot set property x of #<Object> which has only a getter
// 给不可扩展对象的新属性赋值
const fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // 抛出TypeError错误
// Uncaught TypeError: Cannot add property newProp, object is not extensible
})()
在非严格模式中,这样的代码只会静默失败,这样可能会导致用户误以为设置操作成功了.
严格模式要求函数的参数名唯一
在正常模式下, 最后一个重名参数名会掩盖之前的重名参数.
之前的参数仍然可以通过 arguments[i] 来访问, 还不是完全无法访问.
然而, 这种隐藏毫无意义而且可能是意料之外的 (比如它可能本来是打错了), 所以在严格模式下重名参数被认为是语法错误:
(() => {
// Uncaught SyntaxError: Duplicate parameter name not allowed in this context
const sum = (a, a, c) => { // !!! 语法错误
'use strict';
return a + a + c; // 代码运行到这里会出错
}
})()
ECMAScript 6中的严格模式禁止设置primitive值的属性.
不采用严格模式,设置属性将会简单忽略(no-op),采用严格模式,将抛出TypeError错误
(() => {
'use strict';
false.true = ""; // Uncaught TypeError: Cannot create property 'true' on boolean 'false'
(14).sailing = "home"; // Uncaught TypeError: Cannot create property 'sailing' on number '14'
"with".you = "far away"; // Uncaught TypeError: Cannot create property 'you' on string 'with'
})();
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
(() => {
// 报错
const doSomething = (a, b = a) => {
'use strict';
// code
// Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list
}
// 报错
const doSomething = ({a, b}) => {
'use strict';
// code
// Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list
};
// 报错
const doSomething = (...a) => {
'use strict';
// code
// Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list
};
const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
// Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list
}
};
})()
这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。
但是,函数执行的时候,先执行函数参数,然后再执行函数体。
这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
这些差异都是一些微小的差异。
有可能单元测试没办法捕获这种微小的差异。你很有必要去小心地审查你的代码,来确保这些差异不会影响你代码的语义。
幸运的是,这种小心地代码审查可以很大程度上避免。
在普通的函数调用f()中,this的值会指向全局对象.在严格模式中,this的值会指向undefined.
当函数通过call和apply调用时,如果传入的thisValue参数是一个null和undefined除外的原始值(字符串,数字,布尔值),则this的值会成为那个原始值对应的包装对象,如果thisValue参数的值是undefined或null,则this的值会指向全局对象.
在严格模式中,this的值就是thisValue参数的值,没有任何类型转换.
arguments对象属性不与对应的形参变量同步更新
在非严格模式中,修改arguments对象中某个索引属性的值,和这个属性对应的形参变量的值也会同时变化,反之亦然.
这会让JavaScript的代码混淆引擎让代码变得更难读和理解。
在严格模式中arguments 对象会以形参变量的拷贝的形式被创建和初始化,因此 arguments 对象的改变不会影响形参。
在严格模式中,eval不会在当前的作用域内创建新的变量.另外,传入eval的字符串参数也会按照严格模式来解析.
你需要全面测试来确保没有代码受到影响。另外,如果你并不是为了解决一个非常实际的解决方案中,尽量不要使用eval。