跳到主要内容

严格模式

严格模式是 ECMAScript5ES5)发布的语言新特性。使用严格模式可以限制 JavaScript 的一些语言特性,使用严格模式可以去除在书写代码时的一些“骚操作”(有些特性在严格模式下是不可用的),使代码更整洁高效。

严格模式对正常的 JavaScript 语义做了一些更改:

  • 严格模式通过抛出错误来消除了一些原有静默错误;
  • 严格模式修复了一些导致 JavaScript引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快;
  • 严格模式禁用了在ECMAScript的未来版本中可能会定义的一些语法;

使用严格模式

使用严格模式也很简单,只要在 js 文件中写入 "use strict" 这一行文字即可。写了之后,该字符串所在的作用域中的代码就会遵循严格模式。

比如:

"use strict"

function aa(){
// ...
}
// ....

use strict 可以写在文件的许多位置,下面说一下在不同位置它起到的作用与作用范围。

  1. 将该字符串写在文件最顶部,这时整个文件的代码都会进入严格模式;
  2. 将字符放在函数顶部,那么严格模式只会给这个函数开启严格模式;
function strict(a,b){
"use strict"; // 给 strict 开启严格模式
return a + b;
}
  1. 不要将 use strict 单独放在 {} 中,在这样的上下文中这么做是没有效果的。

比如下面的代码,其实并没有开启严格模式:

{ "use strict" };

// some code ...

这是因为 {} 相当于一个作用域,上面相当于在一个作用域中使用严格模式,作用域外面的代码是不受约束的。因此可以看出,严格模式对它所在的作用域中的代码有效。

一个函数中的内容({}里的)就是一个作用域,ES6 类里的内容也是一个作用域;for 循环中也是一个作用域。

  1. 当打包文件时(多个文件打包成一个),你最好将所有的文件(打包之前的)都设置成严格模式或非严格模式,因为如果不这样做,打包后的文件可能并不是严格模式,可能会背离你的目的。或者将严格模式定义在函数中,这样或许能避免模式冲突。

严格模式中的变化

首先看几个例子,在不使用严格模式下运行代码是什么结果,而使用了严格模式又是什么结果。

1. 变量声明

考虑下面代码,运行后会出现什么结果:

"use strict";

a = 123;
console.log(a);

运行后会发现报错(a is not defined),这是因为 在严格模式下你必须声明变量,然后再赋值

这样做的好处是,当你想使用一个局部变量时,却没有声明它,这时如果不使用严格模式,改变量默认会挂载到上层作用域中。有了严格模式可以帮助你检查这样的错误,同时也可以提高编码规范。

2. 相同的形参变量

考虑下面的代码:

function loose(a,a){
return a + a;
}

console.log(loose(2,4));

结果可能出乎意料,并没有报错,而且输出的是 8。只是因为在正常模式下,最后一个重名参数名会掩盖之前的重名参数。因此这个函数其实只有一个形参 a,4 才是它真正的实参。

在严格模式下这是会报错的。

3. arguments

arguments 是存储函数形参的类数组,还有一个特殊的函数: arguments.callee,它可以用于引用该函数的函数体内当前正在执行的函数。arguments.callee() 相当于执行函数体本身,当你想对匿名函数进行递归调用时就可以使用 callee 函数。

比如下面的代码:

[1,2,3,4,5].map(function (n) {
// 使用 callee 实现递归
return !(n > 1) ? 1 : arguments.callee(n - 1) * n;
// [1, 2, 6, 24, 120]
});

而在严格模式下,arguments.callee 是不能使用的(会报错)。那又如何实现上面的递归呢?可以使用具名回调函数:

"use strict";
// 使用具名函数
var arr = [1, 2, 3, 4, 5].map(function fn(n) {
return !(n > 1) ? 1 : fn(n - 1) * n;
});

当然你也可以直接将 fn 函数直接写成一个独立的函数,然后传入 map 中即可:[1,2,3,4,5].map(fn);

考虑下面的代码:

function loose(a,b,c){
a = 100;
console.log(arguments);
}

loose("a","b");

打印后会发现,a 的值已经变成 100。在严格模式下函数形参是只读的,修改了形参的值并不对报错,当然值还是原来的值,并没有被你修改掉。如果你真的有这个需求,可以声明一个变量,然后将形参赋给改变量。或者使用 arguments[0] = 100 的方式去修改,在严格模式下通过 arguments 下标的方式是可以修改传入的值的。

4. this 指向

考虑下面两个函数打印的 this 值:

function fn1(){
console.log(this);
}

function fn2(){
"use strict"; // 启用严格模式
console.log(this);
}

fn1(); fn2();

运行后会发现,fn2 的 this 是 undefined。这是因为在严格模式下通过 this 传递给一个函数的值不会被强制转换为一个对象;一个开启严格模式的函数,指定的 this 不再被封装为对象,而且如果没有指定 this 的话它值是 undefined,可以使用 call, apply 或者 bind 方法来指定一个确定的 this

比如下面的代码会全部返回 true。而如果是非严格模式,除了最后 obj 的那一个,其余都返回 false。这是因为非严格模式下,this 会被包装成一个对象。

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

"use strict";
var obj = {
// fn1 就在一个对象里,因此 this 指向 obj
fn1(){
return this;
}
}
console.log(obj.fn1() === obj); // true

对于箭头函数

箭头函数与普通函数略有不同,**箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。**而且箭头函数还有以下特点:

  • 由于箭头函数没有自己的this指针,通过 call() apply()bind() 方法调用一个函数时,只能传递参数(不能绑定 this),他们的第一个参数会被忽略。

  • 严格模式中与 this 相关的规则都将被忽略。也就是说严格模式对箭头函数的 this 指向无效,顶层的箭头函数还是指向 window

  • 箭头函数不绑定 arguments

    比如下面的代码:

      const fn = (a,b,c,d) => arguments;
    // 会报错:arguments is not defined
    console.log(fn(1,2,3,4));

    因此,使用箭头函数可以接收上层普通函数的 arguments 参数。

      function fn(a,b,c){
    let func = () => {
    console.log(arguments);
    }
    func();
    }
    fn(1,2,3); // 1,2,3
  • 箭头函数不能用作构造器,和 new 一起用会抛出错误,箭头函数也没有 prototype 属性。

  • yield 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。

5. 静默失败以及禁用

使用严格模式会引起静默失败,静默就是有些操作是不能完成的,但运行代码不报错也没有任何效果。例如 Object.defineProperty 方法可以给对象的键设置一些属性,比如设置不可写属性:

var obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9;

如果是非严格模式,运行代码并不会报错,而且 obj.x 的值也不能被改变。而如果开启了严格模式,就会出现错误。

类似的静默失败还有这么几个方法:

  • Object.preventExtensions(object) 给不可扩展对象的新属性赋值(例如:object.aa = 123);
  • Object.defineProperties() 在一个对象上定义新的属性或修改现有属性的状态(可以一次修改多个属性);
  • 试图删除不可删除的属性时会抛出异常。例如:delete Object.prototype
  • 不能将变量名命名为 eval 或者 arguments,严格模式下会报错;
  • 严格模式禁用 with 语句(一般也不用);
  • 严格模式下禁止使用 021 这种表示八进制(普通模式下表示八进制),可以使用 0o21 表示八进制;
  • 严格模式下禁止给基本类型设置属性(string,number,bigint,boolean,null,undefined,symbol),例如:true.a = 1"aa".a = 1(123).b = "aa",这些都会报错在严格模式下。
  • 严格模式禁止删除声明的变量。比如:var a; delete a; 会报错;
  • 在严格模式中一部分字符变成了保留的关键字。这些字符包括 implements, interface, let, package, private, protected, public, static 和 yield。在严格模式下,你不能再用这些名字作为变量名或者形参名。
  • 严格模式下不能在循环语句以及条件语句中声明函数;

6. eval 函数

eval 函数可以将字符串解析成 js 代码然后执行,因此 eval 很强大,严格模式对 eval 函数做了一些限制。

eval 会将传入的字符串执行,然后将返回(或者赋值)的变量返回。而且 "use strict" 严格模式标志可以写进 eval 函数中执行。

  • 严格模式下的 eval 不再为上层范围(包围 eval 代码块的范围)引入新变量。

比如下面的代码,如果不使用严格模式,变量 a 和 b 的值会相同。

var a = 123;

// var a = 12 然后将 a 赋给 b
var b = eval("var a = 12;a");

console.log(a,b); // 12 12

如果使用了严格模式(var b = eval("'use strict';var a = 12;a");),则打印的变量 a 仍然是 123。

还有一种的情况:

strict2(eval,"Non-strict code.");

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

总结

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上 "use strict;"。

严格模式的限制:

  • 变量名必须声明后再使用;
  • 函数的参数不能有同名属性,否则报错;
  • 不能使用 with 语句;
  • 不能对只读属性赋值,否则报错;
  • 不能使用前缀 0 表示八进制,否则报错;
  • 不能删除不可删除的属性,否则报错;
  • 不能删除变量 delete prop,会报错,只能删除属性 delete global[prop]
  • evel 不会在它的外层作用域引入变量;
  • evelarguments 不能被重新赋值;
  • arguments 不会自动反映函数参数的变化;
  • 不能使用 arguments.calleearguments.caller
  • 禁止 this 指向全局对象,顶层的 this 指向 undefined
  • 不能使用 fn.callerfn.arguments 获取函数调用的堆栈;
  • 增加了保留字(比如 protectedstaticinterface);