toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
22 stars 1 forks source link

JavaScript 严格模式详解 #232

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

一、概念

除了正常运行模式,ECMAScript 5 添加了第二种运行模式:严格模式(strict mode)。顾名思义,这种模式使得 JavaScript 在更严格的条件下运行。

与之相反的非严格模式,被称为“sloppy mode”,也称之为“正常模式”。因为翻译原因,正常模式也被翻译为 —— 马虎模式/稀松模式/懒散模式。但这并不是一个官方术语,但是你会经常见到如上的一些说法,其意义就是指代非严格模式,即正常模式。

设立严格模式的目的,主要有以下几个:

  • 消除 JavaScript 语法的一些不合理、不严谨之处,减少一些怪异行为;

  • 消除代码运行的一些不安全之处,保证代码运行的安全;

  • 提高编译器效率,增加运行速度;

  • 为未来新版本的 JavaScript 做好铺垫。

严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向,包括 IE10 在内的主流浏览器都已经支持它,许多大项目已经开始全面拥抱它。

另一方面,同样的代码,在严格模式中,可能会有不一样的运行结果;一些在正常模式下可以运行的语句,在严格模式下将不能运行。

二、启用严格模式

启用严格模式很简单,就一行语句。(分号可以显式显示,也可以通过自动分号插入。)

"use strict";

// 或者
'use strict';

// 或者(由于 ASI 机制,编写代码时可省略分号)
'use strict'

不支持该模式的浏览器,会把它当作一行普通字符串,加以忽略。

三、调用严格模式

严格模式可以应用到整个脚本个别函数中。不要在封闭大括弧 {} 内这样做,在这样的上下文中这么做是没有效果的。

严格模式有两种调用方法,适用于不同的场合。

1. 针对整个脚本文件

"use strict"; 放在脚本文件的第一行,则整个脚本都将以严格模式运行。如果这行语句不在第一行,则无效,整个脚本以正常模式运行。

如果不同模式的代码文件合并成一个文件,这一点需要特别注意。

<script>
  "use strict";
  console.log("这是严格模式!");
</script>

<script>
  console.log("这是正常模式!");
</script>

上述代码表示,一个网页中依次有两段 JavaScript 代码。前一个 <script> 标签是严格模式,后一个是非严格模式。

2. 针对单个函数

"use strict"; 放在函数体的第一行,则整个函数以严格模式运行。

function strict() {
  "use strict";
  return "这是严格模式!";
}

function notStrict() {
  return "这是正常模式!";
}
3. 脚本文件的变通写法

因为第一种调用方法不利于文件合并,所以更好的做法是借用第二种方法,将整个脚本文件放在一个立即执行的匿名函数之中。

(function () {
  "use strict";
  // ...
})();
4. 关于 "use strict" 放在 Program 或 FunctionBody 的第一行问题

严格地说,只要前面不是产生实际运行结果的语句,"use strict"; 可以不在第一行,比如前面包括一些注释、或者是一些 JS 引擎无法识别的指令序言等。

例如:

function fn() {
  "use bar";
  "abc";
  "use strict"; // 因为这完全符合指令序言 — 多指令共存的语法. 所被应用的代码仍然会进入严格模式。
}

ES5 会把 "use bar""abc" 也作为指令序言的某个指令处理,由于 JS 引擎不认识该指令,只认识 "use strict" 指令,则同样会进入严格模式.

四、严格模式对于语法和行为的改变

严格模式对 JavaScript 的语法和行为,都做了一些改变。

1. 全局变量显式声明

在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。

"use strict";

name = "Frankie"; // Uncaught ReferenceError: name is not defined

for (i = 0; i < 2; i++) { // Uncaught ReferenceError: i is not defined
  // ...
}

// 上述代码在正常模式下,是可以正常运行的,而在严格模式下就会报错(引用类型错误)

因此,严格模式下变量都必须先声明再使用。抛开 JavaScript 设计的不合理、缺陷、甚至是 Bug,或者是其他看起来很反人类的东西,在了解历史原因和其中原理之后,为了代码可读性都理应如此。

2. 静态绑定

JavaScript 语言的一个特点,就是允许“动态绑定”,即某些属性和方法到底属于哪一个对象,不是在编译时确定的,而是在运行时(runtime)确定的。

严格模式对动态绑定做了一些限制。某些情况下,只允许静态绑定。也就是说,属性和方法到底归属哪个对象,在编译阶段就确定。这样做有利于编译效率的提高,也使得代码更容易阅读,更少出现意外。

具体来说,涉及以下几个方面。

(1)禁止使用 with 语句

因为 with 语句无法在编译时就确定,属性到底归属哪个对象。

"use strict";

var obj = {
  name: "Frankie"
}

// 语法错误,Uncaught SyntaxError: Strict mode code may not include a with statement
with (obj) {
  name = "Mandy";
}

(2)创设 eval 作用域

正常模式下,JavaScript 语言有两种变量作用域(scope):全局作用域函数作用域。严格模式创设了第三种作用域:eval 作用域

正常模式下,eval 语句的作用域,取决于它处于全局作用域,还是处于函数作用域。严格模式下,eval 语句本身就是一个作用域,不再能够生成全局变量了,它所生成的变量只能用于 eval 内部。

"use strict";

var name = "Frankie";
console.log(eval("var name = 'Mandy'; name")); // "Mandy"
console.log(name); // "Frankie"
3. 增强的安全措施

(1)禁止 this 关键字指向全局对象

function fn1() {
  // 返回 false,因为 "this" 指向全局对象 
  return !this;
}

function fn2() {
  "use strict";
  // 返回 true,因为严格模式下,this 的值为 undefined。
  return !this;
}

因此,使用构造函数时,如果忘加 new 关键字时,this 不再指向全局对象,而是报错。

// 构造函数
function Fn() {
  "use strict";
  this.name = "Frankie"; // Uncaught TypeError: Cannot set property 'name' of undefined
};

// 直接当作普通函数调用就会报错,因为此时 this 为 undefined。
Fn();

(2)禁止在函数内部遍历调用栈

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

fn();
4. 禁止删除变量

严格模式下无法删除变量。

"use strict";

var name;
delete name; // 语法错误,Uncaught SyntaxError: Delete of an unqualified identifier in strict mode.

只有 configurable 设置为 true 的对象属性,才能被删除。

"use strict";

var obj = Object.create(null, {
  "name": {
    value: "Frankie",
    configurable: true
  }
});

delete obj.name; // 删除成功
5. 显式报错

正常模式下,对一个对象的只读属性进行赋值,不会报错,只会默默地失败。严格模式下,将报错。

"use strict";

var obj = {};

Object.defineProperty(obj, "name", { value: "Frankie", writable: false });

obj.name = "Mandy"; // 报错,Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'

严格模式下,对一个使用 getter 方法读取的属性进行赋值,会报错。

"use strict";

var obj = {
  get name() {
    return "Frankie";
  }
};

obj.name = "Mandy"; // 报错,Uncaught TypeError: Cannot set property name of #<Object> which has only a getter

严格模式下,对禁止扩展的对象添加新属性,会报错。

"use strict";

var obj = {};

Object.preventExtensions(obj);

obj.name = "Frankie"; // 报错,Uncaught TypeError: Cannot add property name, object is not extensible

严格模式下,删除一个不可删除的属性,会报错。

"use strict";

// 报错,Uncaught TypeError: Cannot delete property 'prototype' of function Object() { [native code] }
delete Object.prototype;
6. 重名错误

严格模式新增了一些语法错误。

(1)对象不能有重名的属性

在 Gecko 版本 34 之前,严格模式要求一个对象内的所有属性名在对象内必须唯一。正常模式下重名属性是允许的,最后一个重名的属性决定其属性值。因为只有最后一个属性起作用,当代码要去改变属性值而不是修改最后一个重名属性的时候,复制这个对象就产生一连串的 bug。在严格模式下,重名属性被认为是语法错误:

这个问题在 ECMAScript 6 中已经不复存在(bug 1041128)。

"use strict";

// 语法错误:SyntaxError: property name age appears more than once in object literal
var obj = {
  age: 18,
  age: 20
}

(2)函数不能有重名的参数

正常模式下,如果函数有多个重名的参数,最后一个重名参数名会掩盖之前的重名参数,之前的参数仍然可以通过 arguments[i] 来访问。然而,这种隐藏毫无意义而且可能是意料之外的 (比如它可能本来是打错了),所以在严格模式下重名参数被认为是语法错误。

"use strict";

// 语法错误:Uncaught SyntaxError: Duplicate parameter name not allowed in this context
function fn(x, x, y) {
  return;
}
7. 禁止八进制表示法

ECMAScript 并不包含八进制语法,但所有的浏览器都支持这种以零(0)开头的八进制语法:0100 === 64,还有 "\045" === "%"。在 ECMAScript 6 中支持为一个数字加 "0o" 的前缀来表示八进制数.

"use strict";

var n = 0100; // 语法错误:Uncaught SyntaxError: Octal literals are not allowed in strict mode.
var n = 0o100; // ES6 八进制数表示法
8. arguments 对象的限制

arguments 是函数的参数对象,严格模式对它的使用做了限制。

(1)不允许对arguments赋值

"use strict";

arguments++; // 语法错误:Uncaught SyntaxError: Unexpected eval or arguments in strict mode

var obj = { set p(arguments) { } }; // 语法错误,同上

try { } catch (arguments) { } // 语法错误,同上

function arguments() { } // 语法错误,同上

var fn = new Function("arguments", "'use strict'; return 17;"); // 语法错误,同上

(2)arguments不再追踪参数的变化

function fn1(x) {
  x = 2;
  return [x, arguments[0]];
}

fn1(1); // 正常模式为 [2, 2]

function fn2(x) {
  "use strict";
  x = 2;
  return [x, arguments[0]];
}

fn2(1); // 严格模式为 [2, 1]

(3)禁止使用 arguments.callee

这意味着,你无法在匿名函数内部调用自身了。

"use strict";

var fn = function () { return arguments.callee; };

fn(); // 报错:Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
9. 函数必须声明在顶层

将来 JavaScript 的新版本会引入"块级作用域"。为了与新版本接轨,严格模式只允许在全局作用域或函数作用域的顶层声明函数。也就是说,不允许在非函数的代码块内声明函数。

严格模式禁止了不在脚本或者函数层面上的函数声明。在浏览器的普通代码中,在“所有地方”的函数声明都是合法的。这并不在 ES5 规范中(甚至是 ES3)!这是一种针对不同浏览器中不同语义的一种延伸。未来的 ECMAScript 版本很有希望制定一个新的,针对不在脚本或者函数层面进行函数声明的语法。在严格模式下禁止这样的函数声明对于将来 ECMAScript 版本的推出扫清了障碍:

"use strict";
if (true) {
  function f() { } // !!! 语法错误,SyntaxError: in strict mode code, functions may be declared only at top level or immediately within another function
  f();
}

for (var i = 0; i < 5; i++) {
  function f2() { } // !!! 语法错误,SyntaxError: in strict mode code, functions may be declared only at top level or immediately within another function
  f2();
}

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

关于这块内容,可以看下这两篇文章或讨论:

10. 保留字

为了向将来 JavaScript 的新版本过渡,严格模式新增了一些保留字:implementsinterfaceletpackageprivateprotectedpublicstaticyield。使用这些词作为变量名将会报错。

function package(protected) { // 语法错误,Uncaught SyntaxError: Unexpected strict mode reserved word
  "use strict";
  var implements; // 语法错误
}

此外,ECMAScript 5 本身还规定了另一些保留字(classenumexportextendsimportsuper),以及各大浏览器自行增加的 const 保留字,也是不能作为变量名的。

未完待续...

参考