为什么需要在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。

严格模式还不允许在引入E​​S6之前,在对象中复制属性名称,因此以下示例将抛出语法错误:

'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版本中定义的功能。

原文链接https://medium.com/better-programming/why-do-we-need-strict-mode-in-javascript-df34771eb950

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/ZC3JML3zEHLCgCLPVYhk
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券