# 严格模式

ECMAScript 5 引入了严格模式，它通过特意改变 JavaScript 的部分语义和运行时行为来提高语言的弹性。更改的内容通常分为以下几类：

1. 把一些 JavaScript 的静默错误（mistakes）改成了直接抛出错误（errors）
2. 修复了一些让 JavaScript 引擎难以进行优化的错误（所以严格模式下的代码有时会运行得更快）
3. 禁止了一些可能会在未来的 ECMAScript 版本中定义的语法

严格模式有浏览器的兼容性问题，在使用之前最好做下特性检测（feature-testing），其中 IE10+, Node 0.6+, Chrome 13+ 都已经全方位地支持了严格模式，详见 [Strict Mode Compatibility Table](https://kangax.github.io/compat-table/es5/)。

<figure><img src="https://2598460105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FGNkDWo1TzHEOBRUxCRfy%2Fuploads%2F802wqaDZwxCgV1nParEX%2F%E5%85%BC%E5%AE%B9%E6%80%A7.jpeg?alt=media&#x26;token=154f6e4e-d66c-44df-9ee2-9af3daeeab3f" alt=""><figcaption></figcaption></figure>

严格模式和非严格模式可以共存，所以可以选择让 JavaScript 代码逐步支持严格模式，比如从单个文件，甚至是单个函数开始。

> 非严格模式有时也称草率模式（sloppy mode），但这不是官方术语，知道即可

## 1. 开启严格模式

`"use strict";` 或 `'use strict';` 语句就能开启严格模式，写在代码的最开头。

严格模式可以用于整个脚本或是单个函数，但不能单独作用于块级结构 `{}`。&#x20;

### 1.1 整个脚本

为整个脚本启用严格模式。

```javascript
'use strict';
const s = 'Hi! I\'m a strict mode script!';
```

### 1.2 特定函数

为特定函数启用严格模式。

```javascript
function myStrictFunction() {
    // 函数级的严格模式
    'use strict';
    function nested() {
        return 'And so am I!';
    }
    return `Hi! I'm a strict mode function! ${nested()}`;
}

function myNotStrictFunction() {
    return 'I\'m not strict.';
}
```

### 1.3 类和模块

JavaScript 类和模块中的所有代码都会自动开启严格模式，无需开发者手动声明去开启。

## 2. 更改的内容

更改的内容通常分为以下几类：

1. 把一些 JavaScript 的静默错误（mistakes）改成了直接抛出错误（errors）
2. 修复了一些让 JavaScript 引擎难以进行优化的错误（所以严格模式下的代码有时会运行得更快）
   1. 简化了变量的特定用法
   2. 简化了 `eval` 和 `arguments`
   3. 让编写安全的 JavaScript 代码变得更加容易
3. 禁止了一些可能会在未来的 ECMAScript 版本中定义的语法

### 2.1 将 mistakes 改成 errors

JavaScript 被设计为对新手友好，因此它有时会把本应是 errors 的操作视为 non-error 的语义，这种处理方式是可以解决一些眼前的问题，但有可能在未来造成更严重的问题。

所以，严格模式将一些以前可接受的 mistakes 变成了 errors，以便提前发现问题并及时修复。

第一，不能再无意间创建全局变量了。

```javascript
'use strict';

mistypeVarible = 33; // ReferenceError: mistypeVarible is not defined
function f() {
    a = 1; // ReferenceError: a is not defined
}
f();
console.log(this.a);

// non-strict mode 会创建一个全局变量
```

第二，之前是默默失败的赋值操作，现在会抛出异常。包括但不限于：给不可写的全局变量或者属性赋值、给仅 getter 的属性赋值、给不可扩展对象的新属性赋值等等。

```javascript
'use strict';

// 给全局变量赋值
NaN = 2;          // TypeError: Cannot assign to read only property 'NaN' of object '#<Window>'
undefined = true; // TypeError: Cannot assign to read only property 'undefined' of object '#<Window>'

// 给不可写的属性赋值
const o = {};
Object.defineProperty(o, "x", { value: 1, writable: false });
o.x = 2; // TypeError: Cannot assign to read only property 'x' of object '#<Object>'
console.log(o.x);

// 给仅 getter 的属性赋值
const o1 = {
    get x() {
        return 1;
    }
};
o1.x = 2; // TypeError: Cannot set property x of #<Object> which has only a getter
console.log(o1.x);

// 给不可扩展对象的新属性赋值
const fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = 'hi'; // TypeError: Cannot add property newProp, object is not extensible
console.log(fixed);

// non-strict mode 会依次输出：
//   1
//   1
//   {}
```

第三，删除不可删除的属性。

```javascript
'use strict';

// TypeError: Cannot delete property 'prototype' of function Object() { [native code] }
delete Object.prototype;
```

第四，函数的参数名必须唯一。

```javascript
// SyntaxError: Duplicate parameter name not allowed in this context
function sum(a, b, c, a) {
    'use strict';
    return a + b + c;
}
```

第五，禁止以 0 为前缀的八进制文字或八进制转义序列。

```javascript
'use strict';
var n = 010;  // SyntaxError: Octal literals are not allowed in strict mode.
console.log(n); 

// non-strict mode 会输出 8，因为被当成了八进制
```

第六，禁止在原始值上设置属性。

```javascript
'use strict';

false.true = '';         // TypeError: Cannot create property 'true' on boolean 'false'
(14).sailing = 'home';   // TypeError: Cannot create property 'sailing' on number '14'
'with'.you = 'far away'; // TypeError: Cannot create property 'you' on string 'with'

// non-strict mode 虽没报错，但其实是执行失败了——没啥效果
```

### 2.2 更多保留字

2.1 部分的修改（将一些以前可接受的 mistakes 变成了 errors），也为语言未来的语义变化留下了空间，从而更好地向后兼容。

未来的保留字是不能作为变量名或函数名的，比如：`implements`, `interface`, `let`, `package`, `private`, `protected`, `public`, `static`, `yield` 。

### 2.3 变量相关

很多编译器优化都依赖从变量名到变量定义（在哪存怎么存）的映射，而 JavaScript 代码有时需要到运行时才能明确这种基本的映射关系，进而在一定程度上阻碍了编译器的优化。严格模式的引入就消除了导致这类结果的大部分情况，以便让编译器更彻底地优化代码。

1. 禁止使用 `with`
2. `eval` 不会给周围作用域引入新的变量
3. 禁止删除正常的变量

```javascript
'use strict';

let a;
delete a; // SyntaxError: Delete of an unqualified identifier in strict mode.
```

```javascript
'use strict';

var a = 1;
var evalA = eval('var a = 2; a;');
console.log(a, evalA);
// non-strict mode: 2 2
//     strict mode: 1 2
```

```javascript
var a = 1;
var evalA = eval('"use strict"; var a = 2; a;');
console.log(a, evalA); // 1 2
```

### 2.4 `eval` 和 `arguments`

严格模式将 `eval` 和 `arguments` 视为关键字。

1. `eval` 和 `arguments` 不能被绑定（即当标识符），也不能被赋值
2. 不会给 `arguments` 对象的属性设置别名，即函数的形参和 `arguments` 对象是完全独立的
3. 不再支持 `arguments.callee`（因为不利于内联函数的优化）
   * `arguments.callee` 是一个不可删除的属性，访问和设置时都会报错

```javascript
'use strict';

eval = '';      // SyntaxError: Unexpected eval or arguments in strict mode
arguments = []; // SyntaxError: Unexpected eval or arguments in strict mode
```

```javascript
'use strict';

function add(a, b) {
    arguments[0] = 11;
    b = 22;
    console.log(a, b);
    return a + b;
}
add(1, 2);
// non-strict mode: 11 22
//     strict mode:  1 22
```

```javascript
'use strict';

// TypeError: 
//    'caller', 'callee', and 'arguments' properties may not be accessed 
//     on strict mode functions or the arguments objects for calls to them
function fn() {
    return arguments.callee;
}
fn();
```

### 2.5 安全的代码

严格模式让编写安全的 JavaScript 代码变得更容易。

#### 2.5.1 `this` 值

在非严格模式中，`this` 值始终是个对象：`undefined` 和 `null` 会被自动设置成全局对象，其它原始值会被自动转成相应的对象，当通过 `call()`, `apply()` 或 `bind()` 来指定特定 `this` 的时候。然而，自动装箱的操作不仅会降低性能，更有安全隐患——把全局对象暴露出去了，而全局对象可能提供本该加以限制的安全性行为。

在严格模式中，`this` 值不再被强制转换成对象了。也就是说，如果 `this` 值没被指定，那它的值就是 `undefined`。

eg1. 全局作用域

```javascript
'use strict';
console.log(this === globalThis); // true
```

eg2. 全局作用域里的函数

```javascript
'use strict';

function fn() {
    console.log(this); // undefined
    this.a = 1;          // TypeError: Cannot set properties of undefined (setting 'a')
    console.log(this.a); // TypeError: Cannot read properties of undefined (reading 'a')
}
fn(); // 等价于 fn.call();
```

eg3. 通过 `call()`, `apply()` 或 `bind()` 给函数指定特定的 `this`

```javascript
'use strict';

function fn() {
    this.a = 1;
    console.log(this);
    console.log(this.a);
}

const o = { a: 10, b: 11 };
fn.call(o);
// {a: 1, b: 11}
// 1

fn.call({});
// {a: 1}
// 1

fn.call(null);
// TypeError: Cannot set properties of null (setting 'a')
// null
// TypeError: Cannot read properties of null (reading 'a')

fn.call();
// TypeError: Cannot set properties of undefined (setting 'a')
// undefined
// TypeError: Cannot read properties of undefined (reading 'a')
```

#### 2.5.2 函数栈不可追踪

当一个函数 `fun` 被调用时，`fun.caller` 表示最近调用 `fun` 的函数，`fun.arguments` 表示调用 `fun` 的参数。考虑到有些 ECMAScript 扩展会通过 `fun.caller` 和 `fun.arguments` 得到 JavaScript 的调用堆栈，所以严格模式中 `fun.caller` 和 `fun.arguments` 这两个属性都是不可删除的，在设置和检索时会报错。如下：

> non-deletable properties, set or retrieved

```javascript
function fn() {
    'use strict';
    
    // TypeError: 
    //   'caller', 'callee', and 'arguments' properties may not be accessed
    //    on strict mode functions or the arguments objects for calls to them
    fn.caller;
    fn.arguments;
}

function privilegedInvoker() {
    return fn();
}
privilegedInvoker();
```

## 3. 清单

### 3.1 语法相关

会报 SyntaxError

1. `function` 的参数名有重复
2. `delete` 变量
3. 以 `0` 为前缀的八进制文字或八进制转义序列
4. `with` 语句
5. 使用 `eval` 和 `arguments` 当标识符（变量名/函数名/类名），或者给它们赋值
6. 未来的保留字：`let`, `yield`, `public`, `protected`, `private`, `static`, `interface`, `package`, `implements`

### 3.2 运行时相关

1. 报错 ReferenceError
   1. 没声明直接赋值的变量，不再自动创建全局变量了
2. 报错 TypeError
   1. 默默失败的赋值操作 `=`，比如给不可写的全局变量或者属性赋值、给仅 getter 的属性赋值、给不可扩展对象的新属性赋值等
   2. `delete` 不可删除的属性
   3. 访问函数的以下属性：`arguments.callee`, `fnName.caller` 和 `fnName.arguments`
   4. 在原始值上设置属性 `=`
3. 语义的更改
   1. 对于函数的 `this` 值，不再进行自动装箱操作
   2. 函数的形参和 `arguments` 对象是完全独立的
   3. `eval` 不会给周围作用域引入新的变量

## 4. 主要参考

1. <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode>
2. <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode/Transitioning_to_strict_mode>
