聊一聊JavaScript中的严格模式与相关的‘坑’

2,757 阅读9分钟
ECMAScript5中的严格模式(‘use strict’)是该语言的一个受限子集,修正了JavaScript这门语言的一些缺陷,并提供了健壮的查错功能和安全机制。JavaScript中的严格模式分为两个级别,全局和函数级,取决于‘use strict’指令的位置。

      在JavaScript中存在一些‘不好的东西’(在浏览器环境中),比如全局变量自动成为全局对象window的属性,给未声明的变量赋值全局作用域中自动声明一个同名变量,以及函数级作用域中undefined重写的问题等等。而严格模式修正了一些问题,也引发了一些奇怪的问题。

先说一下严格模式与非严格模式的区别:


  • 严格模式下禁止使用with语句

       with语句一般是下面这种形式:

with( obj ) 
statement

一开始设计with的目的可能就是为了简化代码编写。如下:

// 初始化对象
var obj = {
    a = 1,
    b = 2,
    c = 3
}
// 修改对象属性
obj.a = 2;
obj.b = 3; 
obj.c = 4;

// 等同于下面的写法
with( obj ) {
    a = 2;
    b = 3;
    c = 4;
}

with(obj)可以在代码块中的变量解析时将obj对象添加到作用域链的最前端,当代码块中的变量进行解析,根据变量的解析规则会首先查找obj对象中是否存在同名属性。但是这种破坏词法作用域的手段不仅影响代码的执行速度,还会产生意想不到的结果。

var obj = {};

with( obj ) {
    a = 2;
} 

这段代码中的with语句并不会为obj创建一个a属性,在代码快中的a变量进行解析时并未在obj对象中找到同名属性。这时候变量解析会继续沿着作用域链向上查找,若是找到了同名变量a,就修改它的值(但如果这个变量是const声明的,你懂的)。如果未找到同名变量a,在非严格模式下就会创建一个全局变量a并赋值为2。这是多么糟糕的事!!!该禁!


  • 严格模式下限制eval( )的超能力

eval( )一种可以改变JavaScript词法作用域的神奇函数

// 超能力一 访问并拥有更改当前作用域内变量或函数的能力
var a = 2;
eval( 'a = 3' );
console.log( a ); // 3

// 超能力二  拥有在当前作用域创建变量或函数的能力。
eval( 'var b = 6' );
console.log( b ); // 6

// 别名eval;
var otherEval = eval;

这里说一下如果将eval赋值给一个变量在ES3中规定的是会报错,ES5中不会报错但是限制了别名eval的能力,ES5规定别名eval不具有读、写、定义函数作用域内变量和函数的能力,它只能 读、写、定义全局作用域中的变量和函数。

在严格模式下eval()扩号中的内容会创建专属于eval的词法作用域(即使被限制了还是这么牛逼)这个作用域和函数级作用域一样,可以通过作用域链访问并修改外部作用域的变量和函数,但是eval内部创建的变量和函数外部是访问不了的。所以eval内部创建的变量和函数就不会暴露到eval所处的作用域中了。


  • 严格模式下所有的变量都需要先声明再赋值,如果为一个未声明的变量赋值会报错

在非严格模式下会创建一个全局变量并自动成为window的属性。这带来的后果就是屏蔽了window对象上的同名属性。在这里说一下,不管是否是严格模式,全局环境中var声明的变量都会成为window对象的属性(而且是不可配置的,不能通过delete删除)。

如下图:



  • 严格模式下修改对象只读属性和为不可拓展的对象创建属性都会报错。

在非严格模式下会失败并不会报错。由于这个原因我说一下关于JavaScript的另一个‘坑’—— undefined。undefined JavaScript的基本数据类型之一,而且只有undefined这一个值。用来作为已声明但未赋值的变量的值。下面细数一下它的‘坑’。

var a;
console.log( typeof a ) // undefined
console.log( typeof b ) // undefined

这个也不算坑,有人认为这是typeof的一种安全防范机制。在《你不知道的JavaScript中》一书中作者认为使用typeof 操作符检测未声明变量应返回undeclared。之所以提这个是提醒自己下面这个知识点。

console.log( typeof b );  //  报错
let b; or const b = someValue;

这时候typeof的安全防范机制就敌不过temporal dead zone (let,const声明块级绑定带来的暂时性死区)了。这个暂时不说,今天写笔记也不是为了说这个。下面说一下undefined真正的‘坑’

首先明确一件事,作为和null这个基友一样只有唯一值的数据类型,undefined竟然不是关键字,只是一个全局变量(导致了它面临被屏蔽的危险),人家null就是关键字。幸好ES5给了一些补救措施,在ES5中undefined作为window对象的一个只读属性,唉,这就有点变化了,最起码在全局环境中声明同名变量屏蔽不了它了。


// 非严格模式
var undefined = 2;
console.log( undefinded );  // undefinded

// 注意如果是let或const声明,会报重复声明的错误。因为同一作用域下let const 不允许重复声明。
let undefined = 2; or const undefined = 2; 

// 严格模式

var undefined = 2;
console.log( undefined ); // error

这个看起来是不是合理多了。防止了undefined被同名变量覆盖。但是还是有意外的。。。看下面:

// 不管是不是严格模式都会屏蔽
function show() {
    var undefined = 2
    console.log( undefined );
}

show();  // 2

这还是被屏蔽了啊。。。这是因为治标不治本啊!在全局作用域中(非严格模式下)当我们使用var声明或直接给变量undefined赋值,他会自动成为window的属性,因为undefined是只读属性,所以这个赋值就会失败,但是默认不报错。我们使用let或const声明时又由于重复声明,会报重复声明的错误。在严格模式下var声明不仅会失败,还会因为修改只读属性报错。这就是全局环境不能修改undefined值的原因。

因为在函数级作用域中以上两种情况都不存在,undefined又不是关键字,可以用做标识符。所以当在函数内部undefined的值就可以被修改了。这时候如果想要一个可靠地undefined的值,一种方法是通过window对象访问,另一种是使用void操作符。

  • 严格模式下函数作为函数调用this指向undefined

这句话读起来有点绕口,其实就是函数this的默认绑定问题。在非严格模式下,函数直接作为函数调用时this默认绑定报window对象上。这个特性可以用来检测当前环境是否是严格模式。

function isStrict() {
    return this === undefined;
}

var strictMode = isStrict(); // false

  • 严格模式下call和apply的调用的函数的this值就是其第一个参数

在非严格模式下如果传入call或apply的第一个参数是null或undefined,会默认其被window对象替代。如果是基本类型,则包装为对应的包装对象。

var a = 2;
function foo(b ) {
    console.log( this.a + b );
}

foo.call( null, 3 );  // 5

// 这里介绍一种安全的机制,即使不使用严格模式也很安全。
// 如果仅仅使用call或apply调用一个函数并不涉及this绑定时,
    可以给它传入一个完全为‘空’的对象。
var empty = Object.create( {} );
Math.max.apply( empty, array );


  • 严格模式函数的arguments对象仅包含传入实参的副本。以及禁止通过其caller和callee检测函数调用栈的能力

arguments这个对象谁用谁知道。函数重载啊,柯里化,通过caller属性进行函数递归(这个没人用吧哈哈),实现各种绑定的polyfill,简直是神器有木有(ES6使用rest参数代替)。之所以对其进行限制还是因为他无法无天的能力。看看下面的代码:

function foo() {
    console.log( arguments[ 0 ] );
}
foo( 5 ); // 5

function foo( a ) {
    console.log( a + arguments[ 1 ] );
}

foo( 2, 3 ); // 5

这种能力在即使没有形参或者传入实参数量多于形参时我们也可以获取实参。


arguments对象不仅获取了实参还建立了与形参之间的关联(和形参指向同一个值得引用)

function foo( a ) {
    arguments[ 0 ] = 5;
    console.log( a );
}
foo( 2 ); // 5

但是这种关联只在传入对应实参后才会建立。所以当同时访问命名参数和与其对应的arguments类数组单元是就容易产生混乱

function foo( a ) {
    a = 5;
    console.log( arguments[ 0 ] );
    console.log( a );
}
foo();  // undefined  5


在ES6中函数使用默认参数时也会引起混乱

function foo( a = 2 ) {
    console.log( a );
    console.log( arguments[ 0 ] );
    console.log( arguments.length );
}

foo();  // 2 undefined 0

所以严格模式为了避免这种情况出现,使arguments对象仅仅包含传入实参的副本,并不会与形参之间建立起关联

'use strick'

function foo( a ) {
    arguments[ 0 ] = 5;
    console.log( a );
    console.log( arguments[ 0 ] );
}

foo( 2 ); // 2  5

因此如果我们期望使用arguments对象这种神奇能力,就不要同时访问形参何其对应的arguments类数组单元。或者使用ES6的rest参数。

  • 严格模式下delete运算符后面跟非法操作数时这会报错,使用delete删除不可配置的属性时报错

在非严格模式下这两种行为仅仅是简单的返回false并不会报错。这是为了健壮查错机制。


  • 严格模式下函数声明中存在两个或两个以上个同名参数会报错

在非严格模式下最后一个同名形参覆盖前面所有的同名形参

function foo( a, a ) {
    console.log( a );
    console.log( arguments[ 0 ] );
    console.log( arguments[ 1 ] );
}
foo( 2 ); // undefined 2 undefined


  • 在严格模式下不允许使用以0开头的八进制整数直接量

在非严格模式下不报错并将其转化为十进制。ES6中的八进制以0o开头,如:0o32


  • 在严格模式中eval,arguments当做关键字


  • 严格模式下对象直接量中定义两个或两以上的同名属性会报错(ES6中支持属性名重复定义)

由于ES6支持属性名重复定义,所以这个没啥用,但是我们要注意如下:

var obj = { a: 1, a: 2, a: 3 };
    
for ( var key in obj ) {
    console.log( obj[ key ] );
}
// 3

从上面的代码看一看出仅保留最后一个创建的同名属性。


这就是我所理解的严格模式以及JavaScript中可爱的‘坑’。还有很多我还没学到,也没被‘坑’,或者说还未发觉。 本人水平有限,上面也只是自己在书上及实践中对JavaScript的理解。有什么问题欢迎大家指出,希望在大家们的批评下取得进步。