聊一聊JavaScript中的嚴格模式與相關的‘坑’

保持思考發表於2019-03-17
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中的嚴格模式與相關的‘坑’


  • 嚴格模式下修改物件只讀屬性和為不可擴充的物件建立屬性都會報錯。

在非嚴格模式下會失敗並不會報錯。由於這個原因我說一下關於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物件的一個只讀屬性,唉,這就有點變化了,最起碼在全域性環境中宣告同名變數遮蔽不了它了。

聊一聊JavaScript中的嚴格模式與相關的‘坑’

// 非嚴格模式
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的理解。有什麼問題歡迎大家指出,希望在大家們的批評下取得進步。


相關文章