严格模式

注意:有时您会看到默认的非严格模式称为松散模式。这不是一个正式的术语,但请注意它,以防万一。

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" 指令只能应用于具有简单参数的函数体。在具有restdefault解构参数的函数中使用"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;

类的严格模式

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

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

严格模式下的更改

严格模式更改了语法和运行时行为。更改通常属于以下类别

  • 将错误转换为错误(作为语法错误或在运行时)的更改
  • 简化变量引用解析方式的更改
  • 简化evalarguments的更改
  • 使编写“安全”JavaScript 更容易的更改
  • 预期未来 ECMAScript 演变的更改。

将错误转换为错误

严格模式将一些以前可接受的错误更改为错误。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 的访问器属性
  • 分配给不可扩展对象上的新属性

例如,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
}

如果函数具有默认参数、rest 参数或解构参数,则在非严格模式下具有重复的参数名称也是语法错误。

旧版八进制字面量

严格模式禁止使用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语句

严格模式禁止使用withwith的问题在于,块内的任何名称都可能映射到传递给它的对象的属性,或者映射到周围(甚至全局)作用域中的变量,这取决于运行时;无法事先知道是哪一个。严格模式使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";
const f = function () {
  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语句
  • 在变量名上使用deletedelete 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对象中的值会修改相应的命名参数。这使得 JavaScript 引擎的优化变得复杂,并使代码难以阅读/理解。在严格模式下,arguments对象被创建并初始化为与命名参数相同的值,但对arguments对象或命名参数的更改不会相互反映。

eval

在严格模式代码中,eval不会在调用它的作用域中创建新的变量。当然,在严格模式下,字符串也是使用严格模式规则进行评估的。需要进行彻底的测试以确保没有任何内容中断。如果不真正需要,不使用 eval 可能是另一种务实的解决方案。

块级函数声明

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

规范

规范
ECMAScript 语言规范

另请参阅