qhxin

JavaScript的严格模式,以及为什么你应该使用它 | Colin J. Ihrig的博文

qhxin · 2016-12-12翻译 · 825阅读 原文链接

ECMAScript 5 开始,开发者能够将他们的代码转换成一个更受约束的执行形式——strict mode。严格模式通过强制更好的编程实践和消除一些语言的不安全和不明智的特点,提高了JavaScript代码质量。通过向您的代码添加以下指令,可以启用严格模式:

"use strict";

“use strict”;指令可以用两种方式使用。唤起严格模式的第一种方法是在文件级别上。通过在文件开始的位置添加这个指令(该指令只能在注释和空格之前),在全局上下文内启用严格模式。这意味着你所有的代码都将在严格模式下进行解析。当严格模式和非严格模式的脚本在一起使用时,一定要注意一下。当严格模式的脚本在前面的话会强制非严格模式的脚本在严格模式下解析。当非严格模式的脚本在前面时则会导致相反的行为。这会导致一些和 Amazon 一样的问题。

第二种方法是在函数级别上使用严格模式。将“use strict”;指令放置在函数体开头,可以启用这个级别的严格模式。与全局严格模式一样,这个指令只能在空格和注释之前。在函数级使用严格模式允许程序员在同一文件中混合和匹配严格模式的和非严格模式的函数。当一些遗留代码依赖于已经过时的被严格模式弃用的特点时这是很有用的。

function foo() {
  "use strict";
  // 此函数是在严格模式下执行的
}

function bar() {
  // 此函数是在非严格模式下执行的
}

关于严格模式的一个好的东西是它的向后兼容性。老版本的JavaScript会视“use strict”;指令为一个无意义的字符串并忽略它。而新版本的JavaScript会对这个声明进行特殊处理并切换到严格模式。对于支持严格模式的浏览器,会有以下约束。

隐式全局变量声明

JavaScript 有一个有趣的处理变量声明的方法。没有使用 var 关键字声明的变量会被隐式声明为全局变量。下面的代码使用了三个变量——“x”,“y”和“z”。

function foo() {
  var x;
  var z;

  x = 1;
  y = 2;
  z = x + y;
}

注意,只有变量“x”和“z”声明使用 var 关键字。有一个足够的可能性是,程序员也打算声明“y”,但错误地没有。该代码将正确执行,但具有副作用,它会创建一个名字叫做“y”、值为2的全局变量。因为 window 是全局对象,这相当于写:

window.y = 2;

这种行为可能是有问题的,如果“y”已经定义在其他地方,且有一个不同的值。这导致代码伸缩性不太好,并且很难调试。启用严格模式将捕获这个问题。会发生一个异常,而不是使“y”成为一个全局变量。在 Chrome 中显示的异常看起来像这样:

ReferenceError: y is not defined

With 语句

With 语句提供了一种用于工作对象属性的速记符号。在 with 语句内部,很难说一个变量是属于 with 使用的对象还是其他范围的对象。下面的代码使用一个 with 语句来输出文档的标题。这里有一个本地变量也叫 “title”,它在 with 语句里面是被忽略的。在一块更复杂的代码里,程序员在使用 with 语句时会很容易犯与变量作用域相关的错误。

function foo() {
  var title = "Not the page title";
  with (document) {
    write(title + "<br />");
    write("contains a with statement");
  }
}

在严格模式下,with 语句被完全的禁用了。任何试图使用 with 语句的尝试都将导致下列语法错误。注:一个更好的主意是将对象赋值给一个变量,并通过变量来访问它的属性。

SyntaxError: Strict mode code may not include a with statement

标识符的命名

严格模式在变量、函数和参数上有几个命名限制。字符串“eval”和“arguments”不能用作标识符,他们也不能有赋值。因为JavaScript内置了一个“eval”函数和“arguments”的对象,这消除了可能的混乱。尝试使用“eval”或“arguments”作为名称的结果就是下面的异常:

SyntaxError: Variable name may not be eval or arguments in strict mode

此外,标识符不能使用任何以下为未来保留的关键字:

  • implements

  • interface

  • let

  • package

  • private

  • protected

  • public

  • static

  • yield

this 的用法

使用“this”变量在面向对象编程中很常见。在一个函数中,“this”变量指向拥有函数的对象。然而,当一个函数不属于任何特定的对象时,“this”变量指向全局对象(window)。严格模式通过赋值给“this”一个未定义的值,移除全局对象的意外或恶意访问。如果一个函数是由一个对象拥有的,那么“this”仍然如预期的一样指向该对象。在下面的例子里,函数“foo”返回undefined。

function foo() {
  "use strict";
  return this;
}

只读属性

JavaScript 允许开发者在对象上创建只读的属性。试图在非严格模式代码中写入一个新值到只读属性会静默地失败。换言之,该属性的值保持不变,但该程序不提供任何警告或错误。在严格的模式下,这样的行为会导致脚本抛出一个异常。下面的示例创建一个名为“prop1”只读属性。在严格模式下,后续对“prop1”的赋值导致一个 TypeError 异常。

function foo() {
  "use strict";
  var obj = Object.defineProperties({}, {
              prop1 : {
                value : 1,
                writable : false
              }
            });

  obj.prop1 = 2;
}

不可扩展的对象和变量

在 JavaScript 中,添加新的属性到对象是微不足道的——只是赋一个值给它就行了。JavaScript 还提供了一种防止新的属性添加的机制。通过将一个对象的内部“extensible”属性设置为 false,对一个对象的新属性的添加将静默失败。再次,严格模式会将静默的失败改为 TypeError 异常。下面的代码试图将一个新属性添加到一个非可扩展的对象中。

function foo() {
  "use strict";
  var obj = {prop1 : 1};

  Object.preventExtensions(obj);
  obj.prop2 = 2;
}

对象“obj”由一个属性“prop1”构造,当“Object.preventExtensions”被调用时,“obj”变为不可扩展。尝试给对象添加“prop2”属性的结果是导致下面的异常:

TypeError: Can't add property prop2, object is not extensible

重复参数和属性

非严格模式的 JavaScript 允许对象包含多个具有相同名称的属性。当相同的名称被使用多次时,只有最后一个声明被使用了。严格模式要求所有的属性名称都是唯一的。在下面的例子中,对象“obj”包含两个名为“prop1”的属性。在非严格模式下,“prop1”的值会是3。然而,当严格模式开启时,这会出现一个语法错误。

// 在严格模式下
obj = {
  prop1 : 1,
  prop2 : 2,
  prop1 : 3
};

类似的,非严格模式下的函数可以有多个参数具有相同的名称。在严格模式下,这是不被允许的。在下面的例子中,重复的“param1”导致一个语法错误。

function foo(param1, param1) {
  "use strict";
  alert(param1);
}

eval 变量

非严格模式的代码可以使用“eval”函数来添加新的变量到当前作用域。在浏览器的原生JSON支持之前,“eval”普遍的(和安全的)用于从字符串构造对象。构造的对象会成为当前作用域的一部分。在严格模式下,“eval”不能引入新的变量。当执行在严格模式下时,下面的代码不会将“bar”变量引入当前作用域。注意:如果一个含有“eval”的函数是在严格模式下执行,那么在“eval”函数里的代码也在严格模式下执行。

// 在严格模式下
eval("var bar = 10;");

删除操作

删除运算符用于从对象中删除属性。然而,一些属性是不能被删除的。不可配置的属性就是属于不能被删除的。非严格模式的代码将在尝试删除不可配置的属性时静默失败。这种尝试会在严格模式下执行时抛出 TypeError 异常。在下面的例子中,对象“obj”有一个叫做“prop1”的不可配置的属性和一个叫做“prop2”的可配置的属性。因为“prop2”是可配置的,所以删除它是完全可以接受的。直到试图删除“prop1”之前是没有异常被抛出的。

function foo() {
  "use strict";
  var obj = Object.defineProperties({}, {
              prop1 : {
                value : 1,
                configurable : false
              },
              prop2 : {
                value : 2,
                configurable : true
              }
            });

  delete obj.prop2;
  delete obj.prop1;
}

删除运算符是刻意只对对象的属性起作用的。当试图去删除普通变量和函数时会出现语法错误。下面例子中所有的删除操作都会失败。

function foo(param1) {
  "use strict";
  var bar = 1;
  function baz() {};

  delete param1;
  delete bar;
  delete baz;
}

使用 arguments,caller,和 callee

当一个函数被调用,一个名为“arguments”的特殊对象会在函数的作用域被创建。“arguments”是一个包含了传给函数的所有参数的类数组对象。在下面的例子里“arguments[0]”和“arg1”持有同样的值,“arguments[1]”和“arg2”亦然。在非严格模式下,更新一个值会自动更新它所映射到的值。在“bar”结束时,“arg1”和“arg2”的值已经交换,即使值只赋值给了“arguments”数组。在严格模式下,别名值不会自动更新。因此,严格模式下运行,在“bar”结束时,“arg1”和“arg2”的值不会改变。

function foo() {
  bar(1, 2);
}

function bar(arg1, arg2) {
  var temp = arguments[0];
  arguments[0] = arg2;
  arguments[1] = temp;
  alert(arg1 + " " + arg2);
}

“arguments”对象还包含一个指向当前正在执行的函数的指针。当前函数称为被调用方,可被“arguments.callee”引用。这个语法只能在匿名函数内部使用,因为匿名函数没有名字。在任何其他情况下,用名字来引用被调用函数更有意义。在严格模式下,访问“arguments.callee”是不被允许的。

有一个类似的方法可以用来确定函数调用是从哪里来的。在旧的浏览器里有一个“arguments.caller”对象是可用的,但是它已经被弃用了。然而,调用方法仍然可以使用“arguments.callee.caller”来被确定。严格模式也禁止访问这个调用方对象。在下面的例子中,“bar”没有在严格模式中。然而,试图访问“arguments.callee.caller”依然会失败,因为“foo”是一个在严格模式下的函数。

function foo() {
  "use strict";
  bar(1, 2);
}

function bar(arg1, arg2) {
  var caller = arguments.callee.caller
}

更新 (11/21/14)

启用严格模式可以允许一些 JavaScript 引擎诸如 v8,去优化代码,否则不会。在非严格模式下,在同一个函数里引用“arguments”和一个已命名参数会导致 v8 不去优化这个函数。稍微更详细的阅读,可以看这个问题以及我个人的 will-it-optimize 项目。

更新 (11/21/14)

我也要感谢 Chris Dickinson 提供了一个很好的(我觉得是)严格模式阻止的坏的编码实践的案例。下面的例子在非严格模式下返回 102,但是在严格模式下,当一个函数试图重写另一个函数的参数时,会抛出一个错误。

// "use strict"; // 取消注释这行会看到一个错误

function modify() {
  poison.arguments[0] = 100;
}

function poison(a, b) {
  modify();
  return a + b;
}

console.log(poison(1, 2));

八进制

八进制变量用八进制来表示数字。他们很少被使用,并且有一个非常容易使人混淆的语法。正的八进制数由“0”作前缀,负的八进制数由“-0”作前缀。举个例子,定义一个八进制的 100 看起来就像这样:

var foo = 0100;

由于人们往往忽略前导零,这可以很容易地与十进制 100 混淆。然而,一个简短的调用“parseInt()”表明这个八进制值实际上等于转换为十进制时的64。为了解决这个问题,严格模式不允许使用八进制。在严格模式下,八进制的变量和八进制转义序列都被视为语法错误。

分享这篇文章:

相关文章