JavaScript

[ECMAScript] strict mode

2017-12-19  本文已影响11人  何幻

1. 背景

严格模式(strict mode)是ES 5引入的概念,
它提供了一种方式,让人们可以采用受限的方式书写JavaScript。

严格模式不仅仅是JavaScript的一个子集,
它故意地具有与正常代码不同的语义。

严格模式的代码和非严格模式的代码可以并存,
因此,已有代码可以通过渐进的方式,逐渐切换到严格模式。

2. 动机

严格模式对正常的JavaScript语义做了一些更改,
(1)严格模式消除了JavaScript的一些静默错误,取而代之的是让它们抛出异常。
(2)有一些特性会影响JavaScript引擎进一步优化,严格模式修复了它们。因此,严格模式可能会运行的更快。
(3)严格模式禁用了在ECMAScript的未来版本中可能会定义的一些语法。

3. 如何切换到严格模式

严格模式可以应用到整个脚本或个别函数中。

3.1 为某个script标签开启严格模式

为整个script标签开启严格模式,
需要在所有语句之前放一个特定语句 "use strict";(或 'use strict';

"use strict";
var v = "Hi!  I'm a strict mode script!";

注:
这样开启严格模式,在合并两块代码的时候,可能会出现问题。

3.2 为某个函数开启严格模式

要给某个函数开启严格模式,得把 "use strict";(或 'use strict';
放在函数体所有语句之前。

function strict() {
    '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."; }

注:
(1)不要在封闭的大括号{ }内这样做,在这样的上下文中是没有效果的
(2)在eval代码,Function代码,event handler attribute,以及传入setTimeout()的字符串中,都是有效的

4. 严格模式带来了什么

4.1 将静默错误转换成异常

JavaScript被设计为使新人更容易于上手,
所以某些错误操作,具有不报错误的语义(non-error semantics)。
但是有时候这样做,反而会制造出更大的问题。

在严格模式下,先前被接受的拼写错误,将会被认为是异常。
以便可以被立即发现,而将其改正。

(1)创建全局变量
在普通的JavaScript里面,给一个拼写错误的变量名赋值会使全局对象新增一个属性并继续“工作”。
严格模式中意外创建全局变量,会抛出错误,

"use strict";
                       // 假如有一个全局变量叫做mistypedVariable
mistypedVaraible = 17; // 因为变量名拼写错误
                       // 这一行代码就会抛出 ReferenceError

- - -
> Uncaught ReferenceError: mistypedVaraible is not defined

(2)赋值操作
在普通的JavaScript中,某些赋值操作会静默失败(silently fail)。

例如,NaN是一个不可写的全局变量,
在正常模式下,给NaN赋值不会产生任何作用,
开发者也不会接收到任何反馈。

但是在严格模式下,给NaN赋值会抛出一个异常。

'use strict';
NaN = 1;

- - -
> Uncaught TypeError: Cannot assign to read only property 'NaN' of object '#<Window>'

任何在正常模式下引起静默失败的赋值操作,例如,
给不可写(non-writable)属性赋值,给只读属性(getter-only)赋值赋值,
给不可扩展对象(non-extensible object)添加属性,
都会抛出异常。

"use strict";

// 给不可写属性赋值
var obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9; // 抛出TypeError错误

// 给只读属性赋值
var obj2 = { get x() { return 17; } };
obj2.x = 5; // 抛出TypeError错误

// 给不可扩展对象的新属性赋值
var fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // 抛出TypeError错误

(3)删除操作
在严格模式下,试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果)。

"use strict";
delete Object.prototype; // 抛出TypeError错误

(4)函数的重复参数名
严格模式要求函数的参数名唯一。

在正常模式下,最后一个重名参数名会掩盖之前的重名参数。
之前的参数仍然可以通过arguments[i]来访问,还不是完全无法访问。

在严格模式下重名参数会被认为是语法错误,

var f = function(x,x){
  'use strict';
  return x;
}

- - -
> Uncaught SyntaxError: Duplicate parameter name not allowed in this context

(5)八进制数字语法
ECMAScript并不包含八进制语法,但所有的浏览器都支持这种以零开头的八进制语法,

0644 === 420
"\045" === "%"

在严格模式下,这样书写会报语法错误,

'use strict';
012

- - -
> Uncaught SyntaxError: Octal literals are not allowed in strict mode.

注:
在ECMAScript 6中支持为一个数字加0o的前缀来表示八进制数。

'use strict';
0o12
> 10

(6)为原始值添加属性
ECMAScript 6中的严格模式,禁止设置原始值(primitive value)的属性。
不采用严格模式,设置属性将会简单忽略,
采用严格模式,将抛出异常。

"use strict";

false.true = "";              //TypeError
(14).sailing = "home";        //TypeError
"with".you = "far away";      //TypeError

4.2 简化变量的使用

普通的JavaScript代码,在有些情况下,变量的绑定关系只有在运行时才能确定,
严格模式避免了大多数这种情况发生,从而编译器可以做出进一步的优化。

(1)with
with块内的任何变量,首先会在块作用域中查找,
如果找不到定义,则会到with传进来的对象属性中查找,
如果还是找不到,接着再向外部按词法作用域规则进行查找。

这一切都是在运行时决定的,在代码运行之前是无法得知的。
严格模式下,使用with会引起语法错误。

"use strict";
var x = 17;
with (obj) // !!! 语法错误
{
  // 如果没有开启严格模式,with中的这个x会指向with上面的那个x,还是obj.x?
  // 如果不运行代码,我们无法知道,因此,这种代码让引擎无法进行优化,速度也就会变慢。
  x;
}

(2)eval
严格模式下的eval不再为外层作用域(surrounding scope)引入新变量。

在正常模式下, 代码eval("var x;")
会给外层函数作用域(surrounding function)或者全局作用域,引入一个新的变量x

这意味着,一般情况下,在一个包含eval调用的函数内,
如果一个变量既不是函数参数,也不是局部变量,本来是要按照词法作用域规则来决定它的值,
现在都必须在运行时才能决定,
因为eval可能会引入的一个新变量,从而覆盖外层词法作用域中的同名变量。

在严格模式下,eval仅仅为被运行的代码创建变量,
不会创建外部变量,或者影响其他局部变量。

var x = 17;
var evalX = eval("'use strict'; var x = 42; x");
console.assert(x === 17);
console.assert(evalX === 42);

注:
在严格模式下,只有直接调用eval(...)表达式,eval中的代码才会在严格模式下执行。
否则,eval中的代码是否启用严格模式,取决于eval字符串中是否声明了'use strict';

function strict1(str){
  "use strict";
  return eval(str); // str中的代码在严格模式下运行
}
function strict2(f, str){
  "use strict";
  return f(str); // 没有直接调用eval(...): 当且仅当str中的代码开启了严格模式时
                 // 才会在严格模式下运行
}
function nonstrict(str){
  return eval(str); // 当且仅当str中的代码开启了"use strict",str中的代码才会在严格模式下运行
}

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!'");

(3)delete
严格模式禁止删除声明变量。
delete xxx在严格模式下会引起语法错误

"use strict";

var x;
delete x; // !!! 语法错误

eval("var y; delete y;"); // !!! 语法错误

4.3 简化eval和arguments

(1)eval和arguments关键字
名字evalarguments不能作为变量绑定为其他的值,不能被赋值。
以下所有行为都会报语法错误,

"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;");

(2)函数参数
严格模式下,参数的值不会随arguments对象的值的改变而变化。

在正常模式下,对于第一个参数是arg的函数,
arg赋值时会同时赋值给arguments[0]
反之亦然(除非没有参数,或者arguments[0]被删除)。

严格模式下,函数的arguments对象会保存函数被调用时的原始参数。
arguments[i]的值不会随相应参数值的改变而变化,
参数的值也不会随相应的arguments[i]的值的改变而变化。

function f(a){
  "use strict";
  a = 42;
  return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);

(3)arguments.callee
不再支持arguments.callee

正常模式下,arguments.callee指向当前正在执行的函数。
(这个作用很小,因为直接给当前执行的函数命名就可以了。)

此外,arguments.callee十分不利于优化,例如内联函数优化,
函数中如果使用了arguments.callee,就不能被内联,因为不得不建立该函数的一个引用。

在严格模式下,arguments.callee是一个不可删除属性,
赋值和读取时都会抛出异常。

"use strict";
var f = function() { return arguments.callee; };
f(); // 抛出类型错误

4.4 “安全的”JavaScript

(1)this
在严格模式下通过this传递给一个函数的值,不会被强制转换为一个对象。

对一个普通的JavaScript来说,this总会是一个对象,
如果使用原始值进行调用,会自动转换成它们的包装对象,
如果用undefined或者null进行调用,this就是全局对象。

这种自动转化为对象的过程不仅是一种性能上的损耗,
同时在浏览器中暴露出全局对象也会成为安全隐患。

所以对于一个开启严格模式的函数,指定的this不再被封装为对象,
而且如果没有指定this的话它值是undefined

"use strict";
function fun() { return this; }
console.assert(fun() === undefined);
console.assert(fun.call(2) === 2);
console.assert(fun.apply(null) === null);
console.assert(fun.call(undefined) === undefined);
console.assert(fun.bind(true)() === true);

(2)caller
在普通模式下,当一个名为fun的函数正在被调用的时候,
fun.caller是时间上最后一个调用fun的函数,
而且fun.arguments包含调用fun时用的形参。

它们都是有安全问题的,
因为他们允许“安全的”代码,访问其他“专有”函数以及它们的形参。

在严格模式下,fun.callerfun.arguments都是不可删除的属性,
而且在赋值、取值时都会报错,

function restricted()
{
  "use strict";
  restricted.caller;    // 抛出类型错误
  restricted.arguments; // 抛出类型错误
}
function privilegedInvoker()
{
  return restricted();
}
privilegedInvoker();

(3)arguments.caller
严格模式下的arguments不会再提供当前函数的调用者相关的信息。
在一些旧的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)

4.5 为未来的ECMAScript版本铺平道路

未来版本的ECMAScript很有可能会引入新语法,
ECMAScript 5中的严格模式就提早设置了一些限制,来减轻之后版本改变产生的影响。

如果提早使用了严格模式中的保护机制,
那么以后再做出改变就会变得更容易。

(1)关键字
在严格模式中,一部分字符变成了保留的关键字。
这些字符包括,implementsinterfaceletpackage
privateprotectedpublicstaticyield

在严格模式下,不能再用这些名字作为变量名或者形参名。

function package(protected){ // !!!
  "use strict";
  var implements; // !!!

  interface: // !!!
  while (true)
  {
    break interface; // !!!
  }

  function private() { } // !!!
}
function fun(static) { 'use strict'; } // !!!

(2)函数声明
严格模式禁止了不在脚本或者函数层面上的函数声明。

在浏览器的普通代码中,在“所有地方”的函数声明都是合法的。
这并不在ES5规范中(甚至是ES3)。

在严格模式下禁止这样的函数声明,
对于将来ECMAScript版本的推出扫清了障碍。

"use strict";
if (true){
  function f() { } // !!! 语法错误
  f();
}

for (var i = 0; i < 5; i++){
  function f2() { } // !!! 语法错误
  f2();
}

function baz() { // 合法
  function eit() { } // 同样合法
}

注:
市场上仍然有大量的浏览器版本,只部分支持或者根本就不支持严格模式。
由于严格模式改变了语义,依赖严格模式的代码,
在没有实现严格模式的浏览器中,可能会出现问题。

因此,不要盲目依赖它,
应该记得在支持以及不支持严格模式的浏览器中都测试一下代码。


参考

MDN: strict mode
MDN: 严格模式

上一篇 下一篇

猜你喜欢

热点阅读