严格模式

备注: 有时你会看到默认的非严格模式被称为 稀松模式(sloppy mode)。这不是一个官方术语,但以防万一,最好了解一下。

JavaScript 的严格模式是一种选择加入 JavaScript 受限变体的方式,从而隐式地选择了“退出”“稀松模式”。严格模式不仅仅是 JavaScript 的一个子集:它有意地在语义上与普通代码有所不同。严格模式代码和非严格模式代码可以共存,因此脚本可以逐步选择加入严格模式。

严格模式对正常的 JavaScript 语义做出了几处更改:

  1. 通过将一些 JavaScript 的静默错误更改为抛出错误,从而消除了这些错误。
  2. 修复了导致 JavaScript 引擎难以执行优化的错误:有时,严格模式代码的运行速度会比非严格模式下的相同代码更快。
  3. 禁止了未来版本 ECMAScript 中可能定义的某些语法。

调用严格模式

严格模式适用于整个脚本单个函数。它不适用于 {} 花括号括起来的块语句;尝试在这些上下文中应用严格模式不会产生任何效果。eval 代码、Function 代码、事件处理程序属性、传递给 setTimeout() 的字符串以及相关函数,它们要么是函数体,要么是整个脚本,因此在其中调用严格模式会按预期工作。

脚本的严格模式

要为整个脚本调用严格模式,请将确切的语句 "use strict";(或 'use strict';)放在任何其他语句之前。

js
// Whole-script strict mode syntax
"use strict";
const v = "Hi! I'm a strict mode script!";

函数的严格模式

同样,要为函数调用严格模式,请将确切的语句 "use strict";(或 'use strict';)放在函数体的任何其他语句之前。

js
function myStrictFunction() {
  // Function-level strict mode syntax
  "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.";
}

"use strict" 指令只能应用于具有简单参数的函数体。在具有剩余参数默认参数解构参数的函数中使用 "use strict" 会导致语法错误

js
function sum(a = 1, b = 2) {
  // SyntaxError: "use strict" not allowed in function with default parameter
  "use strict";
  return a + b;
}

模块的严格模式

JavaScript 模块的全部内容会自动进入严格模式,无需任何语句来启动它。

js
function myStrictFunction() {
  // because this is a module, I'm strict by default
}
export default myStrictFunction;

类的严格模式

(class)主体的所有部分都是严格模式代码,包括类声明类表达式

js
class C1 {
  // All code here is evaluated in strict mode
  test() {
    delete Object.prototype;
  }
}
new C1().test(); // TypeError, because test() is in strict mode

const C2 = class {
  // All code here is evaluated in strict mode
};

// Code here may not be in strict mode
delete Object.prototype; // Will not throw error

严格模式下的变更

严格模式会改变语法和运行时行为。这些变化通常分为以下几类:

  • 将错误(mistake)转化为真正的错误(语法错误或运行时错误)。
  • 简化变量引用的解析方式。
  • 简化 evalarguments
  • 使编写“安全”的 JavaScript 更加容易。
  • 为未来 ECMAScript 的演进做准备。

将错误转化为真正的错误

严格模式将一些以前被接受的错误(mistake)转变为真正的错误(error)。JavaScript 最初被设计为对新手开发者友好,有时它会给本应是错误的操作赋予非错误的语义。这有时能解决眼前的问题,但有时会在未来造成更糟糕的问题。严格模式将这些错误视为真正的错误,以便它们能被发现并及时修复。

为未声明的变量赋值

严格模式使得意外创建全局变量成为不可能。在稀松模式下,赋值时拼错变量名会在全局对象上创建一个新属性,并继续“工作”。在严格模式下,这种会意外创建全局变量的赋值会抛出错误。

js
"use strict";
let mistypeVariable;

// Assuming no global variable mistypeVarible exists
// this line throws a ReferenceError due to the
// misspelling of "mistypeVariable" (lack of an "a")
mistypeVarible = 17;

为对象属性赋值失败

在严格模式下,那些原本会静默失败的赋值操作会抛出异常。属性赋值失败有三种方式:

  • 为不可写的数据属性赋值。
  • 为只读的访问器属性(getter-only)赋值。
  • 不可扩展的对象上为新属性赋值。

例如,NaN 是一个不可写的全局变量。在稀松模式下,给 NaN 赋值不会有任何效果,开发者也不会收到任何失败反馈。在严格模式下,给 NaN 赋值会抛出异常。

js
"use strict";

// Assignment to a non-writable global
undefined = 5; // TypeError
Infinity = 5; // TypeError

// Assignment to a non-writable property
const obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9; // TypeError

// Assignment to a getter-only property
const obj2 = {
  get x() {
    return 17;
  },
};
obj2.x = 5; // TypeError

// Assignment to a new property on a non-extensible object
const fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // TypeError

删除对象属性失败

在严格模式下,尝试删除一个不可配置或因其他原因不可删除的属性(例如,被代理的 deleteProperty 处理程序拦截并返回 false)会抛出错误(而之前这种尝试不会产生任何效果)。

js
"use strict";
delete Object.prototype; // TypeError
delete [].length; // TypeError

严格模式还禁止删除纯粹的名称。在严格模式下,delete name 是一个语法错误。

js
"use strict";

var x;
delete x; // syntax error

如果该名称是一个可配置的全局属性,请在其前面加上 globalThis 来删除它。

js
"use strict";

delete globalThis.x;

重复的参数名

严格模式要求函数参数名必须唯一。在稀松模式下,最后一个重复的参数会覆盖之前同名的参数。之前的那些参数仍然可以通过 arguments 对象访问,所以并非完全无法访问。尽管如此,这种覆盖行为没什么意义,而且很可能是不希望发生的(例如,它可能掩盖了一个拼写错误),所以在严格模式下,重复的参数名是语法错误。

js
function sum(a, a, c) {
  // syntax error
  "use strict";
  return a + a + c; // wrong if this code ran
}

如果函数有默认参数、剩余参数或解构参数,即使在非严格模式下,拥有重复的参数名也是语法错误。

旧式的八进制字面量

严格模式禁止以 0 为前缀的八进制字面量。在稀松模式下,以 0 开头的数字(如 0644),如果所有数字都小于 8,则被解释为八进制数(0644 === 420)。新手开发者有时会认为前导零没有语义上的意义,可能会用它来对齐——但这会改变数字的含义!使用前导零表示八进制的语法很少有用,并且容易被误用,因此严格模式将其视为语法错误。

js
"use strict";
const sum =
  015 + // syntax error
  197 +
  142;

表示八进制字面量的标准化方法是使用 0o 前缀。例如:

js
const sumWithOctal = 0o10 + 8;
console.log(sumWithOctal); // 16

八进制转义序列,例如 "\45"(等于 "%"),可以用来通过八进制的扩展 ASCII 字符码表示字符。在严格模式下,这是语法错误。更正式地说,不允许 \ 后面跟任何非 0 的十进制数字,或者 \0 后面跟十进制数字;例如 \9\07

在原始值上设置属性

严格模式禁止在原始值上设置属性。在原始值上访问属性会隐式创建一个无法观察到的包装器对象,因此在稀松模式下,设置属性会被忽略(无操作)。在严格模式下,会抛出 TypeError

js
"use strict";

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

重复的属性名

在严格模式下,重复的属性名曾经被认为是 SyntaxError。随着计算属性名的引入,使得在运行时可能出现重复,这一限制在 ES2015 中被移除了。

js
"use strict";
const o = { p: 1, p: 2 }; // syntax error prior to ECMAScript 2015

备注: 让原本会报错的代码变得不再报错,通常被认为是向后兼容的。这是该语言严格抛出错误的一个好处:它为未来的语义变更留下了空间。

简化作用域管理

严格模式简化了变量名如何映射到代码中特定的变量定义。许多编译器优化依赖于能够确定变量 X 存储在那个位置的能力:这对于充分优化 JavaScript 代码至关重要。JavaScript 有时会使这种名称到变量定义的基本映射直到运行时才能确定。严格模式消除了大多数这种情况,因此编译器可以更好地优化严格模式代码。

移除 with 语句

严格模式禁止 with 语句。with 的问题在于,块内的任何名称都可能在运行时映射到传入对象的属性,或周围(甚至全局)作用域中的变量;事先无法知道会是哪一种。严格模式将 with 视为语法错误,因此 with 中的名称不可能在运行时引用到一个未知的位置。

js
"use strict";
const x = 17;
with (obj) {
  // Syntax error
  // If this weren't strict mode, would this be const x, or
  // would it instead be obj.x? It's impossible in general
  // to say without running the code, so the name can't be
  // optimized.
  x;
}

将对象赋给一个短名称的变量,然后访问该变量上的相应属性,这种替代方案可以随时取代 with

不泄漏的 eval

在严格模式下,eval 不会将新变量引入到周围的作用域中。在稀松模式下,eval("var x;") 会在周围的函数或全局作用域中引入一个变量 x。这意味着,通常在一个包含 eval 调用的函数中,每个不引用参数或局部变量的名称都必须在运行时映射到特定的定义(因为那个 eval 可能引入了一个新的变量,会隐藏外部变量)。在严格模式下,eval 只为被评估的代码创建变量,所以 eval 不能影响一个名称是指向外部变量还是某个局部变量。

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

传递给 eval() 的字符串是否在严格模式下被评估,取决于 eval() 是如何被调用的(直接 eval 或间接 eval)。

块级作用域的函数声明

JavaScript 语言规范从一开始就不允许在块语句中嵌套函数声明。然而,这种做法非常直观,以至于大多数浏览器都将其作为扩展语法来实现。不幸的是,这些实现的语义各不相同,使得语言规范无法统一所有实现。因此,块级作用域的函数声明仅在严格模式下被明确规定(而它们曾经在严格模式下是不允许的),而在稀松模式下的行为在不同浏览器之间仍然存在差异。

让 eval 和 arguments 更简单

严格模式使得 argumentseval 的行为不再那么诡异。在稀松模式下,这两者都涉及大量的魔法行为:eval 可以添加或删除绑定以及改变绑定的值,而 arguments 会将其索引属性与命名参数同步。严格模式在将 evalarguments 视为关键字方面取得了巨大进展。

防止绑定或赋值给 eval 和 arguments

名称 evalarguments 不能在语言语法中被绑定或赋值。所有这样做的尝试都是语法错误。

js
"use strict";
eval = 17;
arguments++;
++eval;
const obj = { set p(arguments) {} };
let eval;
try {
} catch (arguments) {}
function x(eval) {}
function arguments() {}
const y = function eval() {};
const f = new Function("arguments", "'use strict'; return 17;");

参数与 arguments 索引之间不再同步

严格模式代码不会将 arguments 对象的索引与每个参数绑定同步。在稀松模式的函数中,如果第一个参数是 arg,那么设置 arg 也会设置 arguments[0],反之亦然(除非没有提供参数或者 arguments[0] 被删除了)。对于严格模式函数,arguments 对象存储的是函数被调用时的原始参数。arguments[i] 不会跟踪相应命名参数的值,命名参数也不会跟踪相应 arguments[i] 中的值。

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

“安全化”JavaScript

严格模式使得编写“安全”的 JavaScript 更加容易。现在一些网站为用户提供了编写 JavaScript 的方式,这些 JavaScript 将由网站代表其他用户运行。浏览器中的 JavaScript 可以访问用户的私人信息,因此在运行之前,必须对这类 JavaScript 进行部分转换,以审查对禁用功能的访问。JavaScript 的灵活性使得在没有大量运行时检查的情况下,实际上不可能做到这一点。某些语言功能非常普遍,以至于进行运行时检查会带来相当大的性能成本。一些严格模式的调整,加上要求用户提交的 JavaScript 是严格模式代码,并以某种方式调用它,可以大大减少对这些运行时检查的需求。

无 this 替换

在严格模式下,传递给函数的 this 值不会被强制转换为对象(即“装箱”)。对于稀松模式的函数,this 始终是一个对象:如果用一个对象作为 this 调用,它就是那个提供的对象;如果用一个原始值作为 this 调用,它就是 this 的装箱值;如果用 undefinednull 作为 this 调用,它就是全局对象。(使用 callapplybind 来指定一个特定的 this。)自动装箱不仅会带来性能成本,而且在浏览器中暴露全局对象也是一个安全隐患,因为全局对象提供了对“安全”JavaScript 环境必须限制的功能的访问。因此,对于严格模式函数,指定的 this 不会被装箱为对象,如果未指定,thisundefined 而不是 globalThis

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

移除堆栈遍历属性

在严格模式下,不再可能“遍历”JavaScript 堆栈。许多实现过去都实现了一些扩展功能,使得可以检测函数的上游调用者。当一个函数 fun 正在被调用时,fun.caller 是最近调用 fun 的函数,而 fun.arguments 是那次 fun 调用的 arguments。这两个扩展对于“安全”JavaScript 来说都是有问题的,因为它们允许“受限”代码访问“特权”函数及其(可能不安全的)参数。如果 fun 处于严格模式,fun.callerfun.arguments 都是不可删除的属性,在设置或获取时会抛出错误。

js
function restricted() {
  "use strict";
  restricted.caller; // throws a TypeError
  restricted.arguments; // throws a TypeError
}
function privilegedInvoker() {
  return restricted();
}
privilegedInvoker();

同样,arguments.callee 也不再支持。在稀松模式下,arguments.callee 指向包含它的函数。这个用例很弱:直接给包含它的函数命名就好了!此外,arguments.callee 严重阻碍了像函数内联这样的优化,因为如果访问了 arguments.callee,就必须能够提供对未内联函数的引用。对于严格模式函数,arguments.callee 是一个不可删除的属性,在设置或获取时会抛出错误。

js
"use strict";
function f() {
  return arguments.callee;
}
f(); // throws a TypeError

面向未来的 JavaScript

额外的保留字

保留字是不能用作变量名的标识符。严格模式比稀松模式保留了更多的名称,其中一些已经在语言中使用,另一些则为未来保留,以便更容易地实现未来的语法扩展。

  • implements
  • interface
  • let
  • package
  • private
  • protected
  • public
  • static
  • yield

过渡到严格模式

严格模式的设计使得向其过渡可以逐步进行。可以单独更改每个文件,甚至可以将代码过渡到严格模式,细化到函数粒度。

你可以通过首先向一段源代码添加 "use strict",然后修复所有执行错误,同时注意语义上的差异,来将一个代码库迁移到严格模式。

语法错误

添加 'use strict'; 时,以下情况将在脚本执行前抛出 SyntaxError

  • 八进制语法 const n = 023;
  • with 语句
  • 对变量名使用 delete,如 delete myVariable;
  • 使用 evalarguments 作为变量名或函数参数名
  • 使用新增的保留关键字(为未来语言特性预留):implementsinterfaceletpackageprivateprotectedpublicstaticyield
  • 声明两个同名的函数参数,如 function f(a, b, b) {}
  • 在对象字面量中两次声明同一个属性名,如 {a: 1, b: 3, a: 7}。此限制后来被移除(bug 1041128)。

这些错误是有益的,因为它们揭示了明显的错误或不良实践。它们在代码运行之前发生,所以只要代码被运行时解析,就很容易发现。

新的运行时错误

在某些本应是错误的情况下,JavaScript 过去会静默失败。严格模式在这些情况下会抛出错误。如果你的代码库包含这类情况,需要进行测试以确保没有东西被破坏。你可以以函数为粒度来筛选这类错误。

  • 给未声明的变量赋值会抛出 ReferenceError。这在过去会在全局对象上设置一个属性,但这很少是预期的效果。如果你真的想给全局对象设置一个值,请明确地将其作为 globalThis 上的一个属性来赋值。
  • 给对象的属性赋值失败(例如,属性是只读的)会抛出 TypeError。在稀松模式下,这会静默失败。
  • 删除不可删除的属性会抛出 TypeError。在稀松模式下,这会静默失败。
  • 如果函数处于严格模式,访问 arguments.calleestrictFunction.callerstrictFunction.arguments 会抛出 TypeError。如果你使用 arguments.callee 来递归调用函数,可以改用命名函数表达式。

语义差异

这些差异非常微小。测试套件可能无法捕捉到这种细微的差异。可能需要仔细审查你的代码库,以确保这些差异不会影响代码的语义。幸运的是,这种仔细的审查可以逐步进行,细化到函数粒度。

this

在稀松模式下,像 f() 这样的函数调用会把全局对象作为 this 值传递。在严格模式下,它现在是 undefined。当函数通过 callapply 调用时,如果值是原始值,它会被装箱成一个对象(对于 undefinednull 则是全局对象)。在严格模式下,该值会直接传递,不进行转换或替换。

arguments

在稀松模式下,修改 arguments 对象中的值会修改相应的命名参数。这使得 JavaScript 引擎的优化变得复杂,也使代码更难阅读/理解。在严格模式下,arguments 对象被创建并初始化为与命名参数相同的值,但对 arguments 对象或命名参数的更改不会相互反映。

eval

在严格模式代码中,eval 不会在调用它的作用域中创建新变量。当然,在严格模式下,字符串也会按照严格模式规则进行评估。需要进行彻底的测试以确保没有东西被破坏。如果你不是真的需要,不使用 eval 可能是另一个务实的选择。

块级作用域的函数声明

在稀松模式下,块内的函数声明可能在块外可见,甚至可以调用。在严格模式下,块内的函数声明仅在块内可见。

规范

规范
ECMAScript® 2026 语言规范

另见