附錄B、嚴格模式
-
嚴格模式
- ECMAScript 5 首次引入嚴格模式的概念。嚴格模式用于選擇以更嚴格的條件檢查 JavaScript 代碼錯誤,可以應用到全局,也可以應用到函數內部。嚴格模式的好處是可以提早發現錯誤,因此可以捕獲某些 ECMAScript 問題導致的編程錯誤。
- 理解嚴格模式的規則非常重要,因為未來的 ECMAScript 會逐步強制全局使用嚴格模式。嚴格模式已得到所有主流瀏覽器支持。
-
選擇使用
- 要選擇使用嚴格模式,需要使用嚴格模式編譯指示(pragma),即一個不賦值給任何變量的字符串:
"use strict";
- 這樣一個即使在 ECMAScript 3 中也有效的字符串,可以兼容不支持嚴格模式的 JavaScript 引擎。支持嚴格模式的引擎會啟用嚴格模式,而不支持的引擎則會將這個編譯指示當成一個未賦值的字符串字面量。
- 如果把這個編譯指示應用到全局作用域,即函數外部,則整個腳本都會按照嚴格模式來解析。這意味著在最終會與其他腳本拼接為一個文件的腳本中添加了編譯指示,會將該文件中的所有 JavaScript 置于嚴格模式之下。
- 也可以像下面這樣只在一個函數內部開啟嚴格模式:
function doSomething() { "use strict"; // 其他代碼 }
- 如果你不能控制頁面中的所有腳本,那么建議只在經過測試的特定函數中啟用嚴格模式。
- 要選擇使用嚴格模式,需要使用嚴格模式編譯指示(pragma),即一個不賦值給任何變量的字符串:
-
變量
- 嚴格模式下如何創建變量及何時會創建變量都會發生變化。第一個變化是不允許意外創建全局變量。在非嚴格模式下,以下代碼可以創建全局變量:
// 變量未聲明 // 非嚴格模式:創建全局變量 // 嚴格模式:拋出 ReferenceError message = "Hello world!";
- 雖然這里的 message 沒有前置 let 關鍵字,也沒有明確定義為全局對象的屬性,但仍然會自動創建為全局變量。在嚴格模式下,給未聲明的變量賦值會在執行代碼時拋出 ReferenceError。
- 相關的另一個變化是無法在變量上調用 delete。在非嚴格模式下允許這樣,但可能會靜默失敗(返回 false)。在嚴格模式下,嘗試刪除變量會導致錯誤:
// 刪除變量 // 非嚴格模式:靜默失敗 // 嚴格模式:拋出 ReferenceError let color = "red"; delete color;
- 嚴格模式也對變量名增加了限制。具體來說,不允許變量名為 implements、interface、let、package、private、protected、public、static 和 yield。這些是目前的保留字,可能在將來的 ECMAScript 版本中用到。如果在嚴格模式下使用這些名稱作為變量名,則會導致語法錯誤。
- 嚴格模式下如何創建變量及何時會創建變量都會發生變化。第一個變化是不允許意外創建全局變量。在非嚴格模式下,以下代碼可以創建全局變量:
-
對象
- 在嚴格模式下操作對象比在非嚴格模式下更容易拋出錯誤。嚴格模式傾向于在非嚴格模式下會靜默失敗的情況下拋出錯誤,增加了開發中提前發現錯誤的可能性。
- 首先,以下幾種情況下試圖操縱對象屬性會引發錯誤。
- 給只讀屬性賦值會拋出 TypeError。
- 在不可配置屬性上使用 delete 會拋出 TypeError。
- 給不存在的對象添加屬性會拋出 TypeError。
- 另外,與對象相關的限制也涉及通過對象字面量聲明它們。在使用對象字面量時,屬性名必須唯一。例如:
// 兩個屬性重名 // 非嚴格模式:沒有錯誤,第二個屬性生效 // 嚴格模式:拋出 SyntaxError let person = { name: "Nicholas", name: "Greg" };
- 這里的對象字面量 person 有兩個叫作 name 的屬性。第二個屬性在非嚴格模式下是最終的屬性。但在嚴格模式下,這樣寫是語法錯誤。
- 注意,ECMAScript 6 刪除了對重名屬性的這個限制,即在嚴格模式下重復的對象字面量屬性鍵不會拋出錯誤。
-
函數
- 首先,嚴格模式要求命名函數參數必須唯一。看下面的例子:
// 命名參數重名 // 非嚴格模式:沒有錯誤,只有第二個參數有效 // 嚴格模式:拋出 SyntaxError function sum (num, num){ // 函數代碼 }
- 在非嚴格模式下,這個函數聲明不會拋出錯誤。這樣可以通過名稱訪問第二個 num,但只能通過arguments 訪問第一個參數。
- arguments 對象在嚴格模式下也有一些變化。在非嚴格模式下,修改命名參數也會修改 arguments對象中的值。而在嚴格模式下,命名參數和 arguments 是相互獨立的。例如:
// 修改命名參數的值 // 非嚴格模式:arguments 會反映變化 // 嚴格模式:arguments 不會反映變化 function showValue(value){ value = "Foo"; alert(value); // "Foo" alert(arguments[0]); // 非嚴格模式:"Foo" // 嚴格模式:"Hi" } showValue("Hi");
- 在這個例子中,函數 showValue()有一個命名參數 value。調用這個函數時給它傳入參數"Hi",該值會賦給value。在函數內部,value被修改為"Foo"。在非嚴格模式下,這樣也會修改arguments[0]的值,但在嚴格模式下則不會。
- 另一個變化是去掉了 arguments.callee 和 arguments.caller。在非嚴格模式下,它們分別引用函數本身和調用函數。在嚴格模式下,訪問這兩個屬性中的任何一個都會拋出 TypeError。例如:
// 訪問 arguments.callee // 非嚴格模式:沒問題 // 嚴格模式:拋出 TypeError function factorial(num){ if (num <= 1) { return 1; } else { return num * arguments.callee(num-1) } } let result = factorial(5);
- 類似地,讀或寫函數的 caller 或 callee 屬性也會拋出 TypeError。因此對這個例子而言,訪問factorial.caller 和 factorial.callee 也會拋出錯誤。
- 另外,與變量一樣,嚴格模式也限制了函數的命名,不允許函數名為 implements、interface、let、package、private、protected、public、static 和 yield。
- 關于函數的最后一個變化是不允許函數聲明,除非它們位于腳本或函數的頂級。這意味著在 if 語句中聲明的函數現在是個語法錯誤:
// 在 if 語句中聲明函數 // 非嚴格模式:函數提升至 if 語句外部 // 嚴格模式:拋出 SyntaxError if (true){ function doSomething(){ // ... } }
- 首先,嚴格模式要求命名函數參數必須唯一。看下面的例子:
-
函數參數
- ES6 增加了剩余操作符、解構操作符和默認參數,為函數組織、結構和定義參數提供了強大的支持。ECMAScript 7 增加了一條限制,要求使用任何上述先進參數特性的函數內部都不能使用嚴格模式,否則會拋出錯誤。不過,全局嚴格模式還是允許的。
// 可以 function foo(a, b, c) { "use strict"; } // 不可以 function bar(a, b, c='d') { "use strict"; } // 不可以 function baz({a, b, c}) { "use strict"; } // 不可以 function qux(a, b, ...c) { "use strict"; }
- ES6 增加的這些新特性期待參數與函數體在相同模式下進行解析。如果允許編譯指示"use strict"出現在函數體內,JavaScript 解析器就需要在解析函數參數之前先檢查函數體內是否存在這個編譯指示,而這會帶來很多問題。為此,ES7 規范增加了這個約定,目的是讓解析器在解析函數之前就確切知道該使用什么模式。
- ES6 增加了剩余操作符、解構操作符和默認參數,為函數組織、結構和定義參數提供了強大的支持。ECMAScript 7 增加了一條限制,要求使用任何上述先進參數特性的函數內部都不能使用嚴格模式,否則會拋出錯誤。不過,全局嚴格模式還是允許的。
-
eval()
- eval()函數在嚴格模式下也有變化。最大的變化是 eval()不會再在包含上下文中創建變量或函數。例如:
// 使用 eval()創建變量 // 非嚴格模式:警告框顯示 10 // 嚴格模式:調用 alert(x)時拋出 ReferenceError function doSomething(){ eval("let x = 10"); alert(x); }
- 以上代碼在非嚴格模式下運行時,會在 doSomething()函數內部創建局部變量 x,然后 alert()會顯示這個變量的值。在嚴格模式下,調用 eval()不會在 doSomething()中創建變量 x,由于 x 沒有聲明,alert()會拋出 ReferenceError。
- 變量和函數可以在 eval()中聲明,但它們會位于代碼執行期間的一個特殊的作用域里,代碼執行完畢就會銷毀。因此,以下代碼就不會出錯:
"use strict"; let result = eval("let x = 10, y = 11; x + y"); alert(result); // 21
- 這里在 eval()中聲明了變量 x 和 y,將它們相加后返回得到的結果。變量 result 會包含 x 和 y相加的結果 21,雖然 x 和 y 在調用 alert()時已經不存在了,但不影響結果的顯示。
- eval()函數在嚴格模式下也有變化。最大的變化是 eval()不會再在包含上下文中創建變量或函數。例如:
-
eval 與 arguments
- 嚴格模式明確不允許使用 eval 和 arguments 作為標識符和操作它們的值。例如:
// 將 eval 和 arguments 重新定義為變量 // 非嚴格模式:可以,沒有錯誤 // 嚴格模式:拋出 SyntaxError let eval = 10; let arguments = "Hello world!";
- 在非嚴格模式下,可以重寫 eval 和 arguments。在嚴格模式下,這樣會導致語法錯誤。不能用它們作為標識符,這意味著下面這些情況都會拋出語法錯誤:
- 使用 let 聲明;
- 賦予其他值;
- 修改其包含的值,如使用++;
- 用作函數名;
- 用作函數參數名;
- 在 try/catch 語句中用作異常名稱。
- 嚴格模式明確不允許使用 eval 和 arguments 作為標識符和操作它們的值。例如:
-
this 強制轉型
- JavaScript 中最大的一個安全問題,也是最令人困惑的一個問題,就是在某些情況下 this 的值是如何確定的。使用函數的 apply()或 call()方法時,在非嚴格模式下 null 或 undefined 值會被強制轉型為全局對象。在嚴格模式下,則始終以指定值作為函數 this 的值,無論指定的是什么值。例如:
// 訪問屬性 // 非嚴格模式:訪問全局屬性 // 嚴格模式:拋出錯誤,因為 this 值為 null let color = "red"; function displayColor() { alert(this.color); } displayColor.call(null);
- 這里在調用 displayColor.call()時傳入 null 作為 this 的值,在非嚴格模式下該函數的 this值是全局對象。結果會顯示"red"。在嚴格模式下,該函數的 this 值是 null,因此在訪問 null 的屬性時會拋出錯誤。
- 通常,函數會將其 this 的值轉型為一種對象類型,這種行為經常被稱為“裝箱”(boxing)。這意味著原始值會轉型為它們的包裝對象類型。
function foo() { console.log(this); } foo.call(); // Window {} foo.call(2); // Number {2}
- 在嚴格模式下執行以上代碼時,this 的值不會再“裝箱”:
function foo() { "use strict"; console.log(this); } foo.call(); // undefined foo.call(2); // 2
- JavaScript 中最大的一個安全問題,也是最令人困惑的一個問題,就是在某些情況下 this 的值是如何確定的。使用函數的 apply()或 call()方法時,在非嚴格模式下 null 或 undefined 值會被強制轉型為全局對象。在嚴格模式下,則始終以指定值作為函數 this 的值,無論指定的是什么值。例如:
-
類與模塊
- 類和模塊都是 ECMAScript 6 新增的代碼容器特性。在之前的 ECMAScript 版本中沒有類和模塊這兩個概念,因此不用考慮從語法上兼容之前的 ECMAScript 版本。為此,TC39 委員會決定在 ES6 類和模塊中定義的所有代碼默認都處于嚴格模式。
- 對于類,這包括類聲明和類表達式,構造函數、實例方法、靜態方法、獲取方法和設置方法都在嚴格模式下。對于模塊,所有在其內部定義的代碼都處于嚴格模式。
-
其他變化
- 嚴格模式下還有其他一些需要注意的變化。首先是消除 with 語句。with 語句改變了標識符解析時的方式,嚴格模式下為簡單起見已去掉了這個語法。在嚴格模式下使用 with 會導致語法錯誤:
// 使用 with 語句 // 非嚴格模式:允許 // 嚴格模式:拋出 SyntaxError with(location) { alert(href); }
- 嚴格模式也從 JavaScript 中去掉了八進制字面量。八進制字面量以前導 0 開始,一直以來是很多錯誤的源頭。在嚴格模式下使用八進制字面量被認為是無效語法:
// 使用八進制字面量 // 非嚴格模式:值為 8 // 嚴格模式:拋出 SyntaxError let value = 010;
- ECMAScript 5修改了非嚴格模式下的parseInt(),將八進制字面量當作帶前導0的十進制字面量。例如:
// 在 parseInt()中使用八進制字面量 // 非嚴格模式:值為 8 // 嚴格模式:值為 10 let value = parseInt("010");
- 嚴格模式下還有其他一些需要注意的變化。首先是消除 with 語句。with 語句改變了標識符解析時的方式,嚴格模式下為簡單起見已去掉了這個語法。在嚴格模式下使用 with 會導致語法錯誤: