为什么需要在 JavaScript 中使用严格模式?
严格模式是什么意思?有什么用途?为什么我们应该使用它?本文将主要从这几个问题入手,讲述在 JavaScript 中使用严格模式的必要性。
严格模式是现代 JavaScript 的重要组成部分。通过这种模式,我们可以选择使用更为严格的 JavaScript 语法。
严格模式的语义不同于以前的 JavaScript“稀松模式”(sloppy mode),后者的语法更宽松,并且会静默代码中的错误。这意味着错误会被忽略,并且运行代码可能会产生意外的结果。
严格模式对 JavaScript 语义进行了一些更改。它不会静默错误,而是会抛出错误,阻止出错的代码继续运行。
它还能指出会阻碍 JavaScript 引擎优化工作的过错。另外,它禁止使用可能在未来的 JavaScript 版本中定义的功能。
严格模式可以应用于单个函数或整个脚本。它不能仅应用于大括号内的语句等块。要为某个脚本启用严格模式,我们要在脚本顶部所有语句之前添加"use strict"或’use strict’语句。
如果我们让一些脚本使用严格模式,而其他脚本不使用严格模式,那么使用严格模式的脚本可能会与其他不使用严格模式的脚本串联在一起。
串联起来后,不使用严格模式的代码可能会被设置为严格模式,反之亦然。因此,最好不要将它们混合在一起。
我们也可以将其应用于函数。为此,我们在函数主体顶部开头添加"use strict"或’use strict’语句,后面接其他语句。它会应用到函数内部的所有内容上,包括嵌套在使用严格模式的函数中的函数。
例如:
const strictFunction = ()=>{
'use strict';
const nestedFunction = ()=>{
// 这个函数也使用严格模式
}
}
ES2015 中引入的 JavaScript 模块自动启用了严格模式,因此无需声明即可启用它。
严格模式下的变化
严格模式同时改变了语法及运行时行为。变化分为这几类:将过错(mistake)转化为运行时抛出的语法错误;简化了特定变量的计算方式;简化了 eval 函数以及 arguments 对象;还改变了可能在未来 ECMAScript 规范中实现的功能的应对方式。
将过失转化为错误
过失会转化为错误。以前它们在稀松模式中会被接受。严格模式限制了错误语法的使用,并且不会让代码在有错误的位置继续运行。
由于这种模式不允许我们使用 var、let 或 const 声明变量,因此很难创建全局变量;所以创建变量时,不使用这些关键字声明这些变量是不行的。例如,以下代码将抛出一个 ReferenceError:
'use strict';
badVariable = 1;
我们无法在严格模式下运行上述代码,因为如果关闭了严格模式,此代码将创建一个全局变量 badVariable。严格模式可以防止这种情况,以防止意外创建全局变量。
现在,任何以静默方式失败的代码都将抛出异常。这包括以前被静默忽略的所有无效语法。
例如,我们启用了严格模式后,不能为只读变量(如 arguments、NaN 或 eval)赋值。
对只读属性(如不可写的全局属性)的赋值,对 getter-only 属性的赋值以及对不可扩展对象上的属性的赋值都将在严格模式下抛出异常。
以下是一些语法示例,这些语法在启用严格模式后将失败:
'use strict';
let undefined = 5;
let Infinity = 5;
let obj = {};
Object.defineProperty(obj, 'foo', { value: 1, writable: false });
obj.foo = 1
let obj2 = { get foo() { return 17; } };
obj2.foo = 2
let fixedObj = {};
Object.preventExtensions(fixedObj);
fixed.bar= 1;
上面所有示例都将抛出一个 TypeError 。undefined 和 Infinity 是不可写的全局对象。obj 是不可写的属性。obj2 的 foo 属性是 getter-only 的属性,因此无法设置。使用 Object.preventExtensions 方法阻止了 fixedObj 向其添加更多属性。
此外,如果有代码尝试删除不可删除的属性,则会抛出一个 TypeError 。例如:
'use strict';
delete Array.prototype
这将抛出一个 TypeError。
严格模式还不允许在引入 ES6 之前,在对象中复制属性名称,因此以下示例将抛出语法错误:
'use strict';
var o = { a: 1, a: 2 };
严格模式要求函数参数名称唯一。不使用严格模式时,如果两个形参(parameter)的名称均为 1,则传入实参(argument)时,后定义的那个 1 将被接受为形参的值。
在严格模式下,不再允许具有相同名称的多个函数参数,因此以下示例将因语法错误而无法运行:
const multiply = (x, x, y) => x*x*y;
在严格模式下也不允许八进制语法。它不是规范的一部分,但是在浏览器中可以通过为八进制数字加上 0 前缀来支持这种语法。
这使开发人员感到困惑,因为有些人可能认为数字前面的 0 是没有意义的。因此,严格模式不允许使用此语法,并且会抛出语法错误。
严格模式还阻止使用阻碍优化的语法。在优化执行之前,程序需要知道一个变量实际上存储在了预期的位置,因此我们必须避免那种阻碍优化的语法。
一个示例是 with 语句。如果我们使用它,它会阻止 JavaScript 解释器了解你要引用的变量或属性,因为可能在 with 语句的内部或外部具有相同名称的变量。
如果我们有类似以下代码的内容:
let x = 1;
with (obj) {
x;
}
JavaScript 就不会知道 with 语句中的 x 是指 x 变量还是 obj、obj.x 的属性。这样,x 的存储位置不明确。因此,严格模式将阻止使用 with 语句。如果我们有如下严格模式:
'use strict';
let x = 1;
with (obj) {
x;
}
上面的代码将出现语法错误。
严格模式阻止的另一件事是在 eval 语句中声明变量。
例如,在没有严格模式的情况下,eval(‘let x’) 会将变量 x 声明进代码。这样一来,人们可以在字符串中隐藏变量声明,而这些字符串可能会覆盖 eval 语句之外的同一变量声明。为避免这种情况,严格模式不允许在传递给 eval 语句的字符串参数中进行变量声明。严格模式还禁止删除普通变量名称,因此以下内容将抛出语法错误:
'use strict';
let x;
delete x;
禁止无效语法
在严格模式下,不允许使用 eval 和 argument 的无效语法。这意味着不允许对它们执行任何操作,例如为它们分配新值或将它们用作变量、函数或函数中参数的名称。
以下是 eval 的无效用法和不允许的 argument 对象的示例:
'use strict';
eval = 1;
arguments++;
arguments--;
++eval;
eval--;
let obj = { set p(arguments) { } };
let eval;
try { } catch (arguments) { }
try { } catch (eval) { }
function x(eval) { }
function arguments() { }
let y = function eval() { };
let eval = ()=>{ };
let f = new Function('arguments', "'use strict'; return 1;");
严格模式不允许为 arguments 对象创建别名,并不允许通过别名设置新值。非严格模式下,如果函数的第一个参数是 a,则设置 a 还将设置 arguments[0]。在严格模式下,arguments 对象将始终具有调用该函数所使用的参数列表。
例如,如果我们有:
const fn = function(a) {
'use strict';
a = 2;
return [a, arguments[0]];
}
console.log(fn(1))
那么我们应该看到 [2,1] 已记录。这是因为将 a 设置为 2 也会同时将 arguments[0] 设置为 2。
性能优化
此外,严格模式不再支持 arguments.callee。非严格模式下,它所做的就是返回 arguments.callee 所在的,被调用函数的名称。
它阻止了诸如内联函数之类的优化,因为 arguments.callee 要求,如果访问 arguments.callee,则对未内联函数的引用可用。因此在严格模式下,arguments.callee 现在将抛出 TypeError。
使用严格模式时,this 不会强制始终成为对象。如果函数的 this 是用 call、apply 或 bind 绑定到任何非对象类型(例如 undefined、null、number、boolean 等原始类型)的,则必须强制它们成为对象。
如果 this 的上下文切换为非对象类型,则全局 window 对象将取代其位置。这意味着全局对象公开了正在被调用的函数,并且 this 绑定到非对象类型。
例如,如果我们运行以下代码:
'use strict';
function fn() {
return this;
}
console.log(fn() === undefined);
console.log(fn.call(2) === 2);
console.log(fn.apply(null) === null);
console.log(fn.call(undefined) === undefined);
console.log(fn.bind(true)() === true);
所有控制台日志都会是 true,因为当 this 更改为具有非对象类型的对象时,该函数内部的 this 不会自动转换为 window 全局对象。
安全修复
在严格模式下,我们也不允许公开函数的 caller 和 arguments,因为函数的 caller 属性访问的函数被一个函数调用时,caller 可能会暴露后者。
arguments 具有在调用函数时传递的参数。例如,如果我们有一个名为 fn 的函数,则可以通过 fn.caller 查看调用 fn 的函数,并通过 fn.arguments 可以看到在调用 fn 时传递给 fn 的参数。
这是一个潜在的安全漏洞,通过禁止访问该函数的这两个属性就能堵上它。
function secretFunction() {
'use strict';
secretFunction.caller;
secretFunction.arguments;
}
function restrictedRunner() {
return secretFunction();
}
restrictedRunner();
在上面的示例中,我们无法在严格模式下访问 secretFunction.caller 和 secretFunction.arguments,因为人们可能会使用它来获取函数的调用堆栈。如果我们运行该代码,将抛出 TypeError。
在将来的 JavaScript 版本中将成为受限关键字的标识符,将不被允许用作变量或属性名称之类的标识符。
以下关键字不得用来在代码中定义标识符:implements、interface、let、package、private、protected、public、static 和 yield。
在 ES2015 或更高版本中,这些已成为保留字;因此在非严格模式下,绝对不能将它们用于变量命名和对象属性。
严格模式成为一种标准已经很多年了。浏览器对它的支持很普遍,只有像 Internet Explorer 这样的旧浏览器才可能出问题。
其他浏览器使用严格模式应该不会有问题。因此,应使用它来防止错误并避免安全隐患,例如暴露调用栈或在 eval 中声明新变量。
此外,它还消除了静默错误,现在会抛出错误,从而使代码不会在出现错误的情况下运行。它还会指出阻止 JavaScript 引擎进行优化的过错。
另外,它禁用了可能在将来的 JavaScript 版本中定义的功能。
作者:John Au-Yeung
译者:王强
来源:InfoQ