嚴格模式
備註: 有時你會看到預設的非嚴格模式被稱為 稀鬆模式(sloppy mode)。這不是一個官方術語,但以防萬一,最好了解一下。
JavaScript 的嚴格模式是一種選擇加入 JavaScript 受限變體的方式,從而隱式地選擇了“退出”“稀鬆模式”。嚴格模式不僅僅是 JavaScript 的一個子集:它有意地在語義上與普通程式碼有所不同。嚴格模式程式碼和非嚴格模式程式碼可以共存,因此指令碼可以逐步選擇加入嚴格模式。
嚴格模式對正常的 JavaScript 語義做出了幾處更改:
- 透過將一些 JavaScript 的靜默錯誤更改為丟擲錯誤,從而消除了這些錯誤。
- 修復了導致 JavaScript 引擎難以執行最佳化的錯誤:有時,嚴格模式程式碼的執行速度會比非嚴格模式下的相同程式碼更快。
- 禁止了未來版本 ECMAScript 中可能定義的某些語法。
呼叫嚴格模式
嚴格模式適用於整個指令碼或單個函式。它不適用於 {} 花括號括起來的塊語句;嘗試在這些上下文中應用嚴格模式不會產生任何效果。eval 程式碼、Function 程式碼、事件處理程式屬性、傳遞給 setTimeout() 的字串以及相關函式,它們要麼是函式體,要麼是整個指令碼,因此在其中呼叫嚴格模式會按預期工作。
指令碼的嚴格模式
要為整個指令碼呼叫嚴格模式,請將確切的語句 "use strict";(或 'use strict';)放在任何其他語句之前。
// Whole-script strict mode syntax
"use strict";
const v = "Hi! I'm a strict mode script!";
函式的嚴格模式
同樣,要為函式呼叫嚴格模式,請將確切的語句 "use strict";(或 'use strict';)放在函式體的任何其他語句之前。
function myStrictFunction() {
// Function-level strict mode syntax
"use strict";
function nested() {
return "And so am I!";
}
return `Hi! I'm a strict mode function! ${nested()}`;
}
function myNotStrictFunction() {
return "I'm not strict.";
}
"use strict" 指令只能應用於具有簡單引數的函式體。在具有剩餘引數、預設引數或解構引數的函式中使用 "use strict" 會導致語法錯誤。
function sum(a = 1, b = 2) {
// SyntaxError: "use strict" not allowed in function with default parameter
"use strict";
return a + b;
}
模組的嚴格模式
JavaScript 模組的全部內容會自動進入嚴格模式,無需任何語句來啟動它。
function myStrictFunction() {
// because this is a module, I'm strict by default
}
export default myStrictFunction;
類的嚴格模式
類(class)主體的所有部分都是嚴格模式程式碼,包括類宣告和類表示式。
class C1 {
// All code here is evaluated in strict mode
test() {
delete Object.prototype;
}
}
new C1().test(); // TypeError, because test() is in strict mode
const C2 = class {
// All code here is evaluated in strict mode
};
// Code here may not be in strict mode
delete Object.prototype; // Will not throw error
嚴格模式下的變更
嚴格模式會改變語法和執行時行為。這些變化通常分為以下幾類:
- 將錯誤(mistake)轉化為真正的錯誤(語法錯誤或執行時錯誤)。
- 簡化變數引用的解析方式。
- 簡化
eval和arguments。 - 使編寫“安全”的 JavaScript 更加容易。
- 為未來 ECMAScript 的演進做準備。
將錯誤轉化為真正的錯誤
嚴格模式將一些以前被接受的錯誤(mistake)轉變為真正的錯誤(error)。JavaScript 最初被設計為對新手開發者友好,有時它會給本應是錯誤的操作賦予非錯誤的語義。這有時能解決眼前的問題,但有時會在未來造成更糟糕的問題。嚴格模式將這些錯誤視為真正的錯誤,以便它們能被發現並及時修復。
為未宣告的變數賦值
嚴格模式使得意外建立全域性變數成為不可能。在稀鬆模式下,賦值時拼錯變數名會在全域性物件上建立一個新屬性,並繼續“工作”。在嚴格模式下,這種會意外建立全域性變數的賦值會丟擲錯誤。
"use strict";
let mistypeVariable;
// Assuming no global variable mistypeVarible exists
// this line throws a ReferenceError due to the
// misspelling of "mistypeVariable" (lack of an "a")
mistypeVarible = 17;
為物件屬性賦值失敗
在嚴格模式下,那些原本會靜默失敗的賦值操作會丟擲異常。屬性賦值失敗有三種方式:
- 為不可寫的資料屬性賦值。
- 為只讀的訪問器屬性(getter-only)賦值。
- 在不可擴充套件的物件上為新屬性賦值。
例如,NaN 是一個不可寫的全域性變數。在稀鬆模式下,給 NaN 賦值不會有任何效果,開發者也不會收到任何失敗反饋。在嚴格模式下,給 NaN 賦值會丟擲異常。
"use strict";
// Assignment to a non-writable global
undefined = 5; // TypeError
Infinity = 5; // TypeError
// Assignment to a non-writable property
const obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9; // TypeError
// Assignment to a getter-only property
const obj2 = {
get x() {
return 17;
},
};
obj2.x = 5; // TypeError
// Assignment to a new property on a non-extensible object
const fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // TypeError
刪除物件屬性失敗
在嚴格模式下,嘗試刪除一個不可配置或因其他原因不可刪除的屬性(例如,被代理的 deleteProperty 處理程式攔截並返回 false)會丟擲錯誤(而之前這種嘗試不會產生任何效果)。
"use strict";
delete Object.prototype; // TypeError
delete [].length; // TypeError
嚴格模式還禁止刪除純粹的名稱。在嚴格模式下,delete name 是一個語法錯誤。
"use strict";
var x;
delete x; // syntax error
如果該名稱是一個可配置的全域性屬性,請在其前面加上 globalThis 來刪除它。
"use strict";
delete globalThis.x;
重複的引數名
嚴格模式要求函式引數名必須唯一。在稀鬆模式下,最後一個重複的引數會覆蓋之前同名的引數。之前的那些引數仍然可以透過 arguments 物件訪問,所以並非完全無法訪問。儘管如此,這種覆蓋行為沒什麼意義,而且很可能是不希望發生的(例如,它可能掩蓋了一個拼寫錯誤),所以在嚴格模式下,重複的引數名是語法錯誤。
function sum(a, a, c) {
// syntax error
"use strict";
return a + a + c; // wrong if this code ran
}
如果函式有預設引數、剩餘引數或解構引數,即使在非嚴格模式下,擁有重複的引數名也是語法錯誤。
舊式的八進位制字面量
嚴格模式禁止以 0 為字首的八進位制字面量。在稀鬆模式下,以 0 開頭的數字(如 0644),如果所有數字都小於 8,則被解釋為八進位制數(0644 === 420)。新手開發者有時會認為前導零沒有語義上的意義,可能會用它來對齊——但這會改變數字的含義!使用前導零表示八進位制的語法很少有用,並且容易被誤用,因此嚴格模式將其視為語法錯誤。
"use strict";
const sum =
015 + // syntax error
197 +
142;
表示八進位制字面量的標準化方法是使用 0o 字首。例如:
const sumWithOctal = 0o10 + 8;
console.log(sumWithOctal); // 16
八進位制轉義序列,例如 "\45"(等於 "%"),可以用來透過八進位制的擴充套件 ASCII 字元碼錶示字元。在嚴格模式下,這是語法錯誤。更正式地說,不允許 \ 後面跟任何非 0 的十進位制數字,或者 \0 後面跟十進位制數字;例如 \9 和 \07。
在原始值上設定屬性
嚴格模式禁止在原始值上設定屬性。在原始值上訪問屬性會隱式建立一個無法觀察到的包裝器物件,因此在稀鬆模式下,設定屬性會被忽略(無操作)。在嚴格模式下,會丟擲 TypeError。
"use strict";
false.true = ""; // TypeError
(14).sailing = "home"; // TypeError
"with".you = "far away"; // TypeError
重複的屬性名
在嚴格模式下,重複的屬性名曾經被認為是 SyntaxError。隨著計算屬性名的引入,使得在執行時可能出現重複,這一限制在 ES2015 中被移除了。
"use strict";
const o = { p: 1, p: 2 }; // syntax error prior to ECMAScript 2015
備註: 讓原本會報錯的程式碼變得不再報錯,通常被認為是向後相容的。這是該語言嚴格丟擲錯誤的一個好處:它為未來的語義變更留下了空間。
簡化作用域管理
嚴格模式簡化了變數名如何對映到程式碼中特定的變數定義。許多編譯器最佳化依賴於能夠確定變數 X 儲存在那個位置的能力:這對於充分最佳化 JavaScript 程式碼至關重要。JavaScript 有時會使這種名稱到變數定義的基本對映直到執行時才能確定。嚴格模式消除了大多數這種情況,因此編譯器可以更好地最佳化嚴格模式程式碼。
移除 with 語句
嚴格模式禁止 with 語句。with 的問題在於,塊內的任何名稱都可能在執行時對映到傳入物件的屬性,或周圍(甚至全域性)作用域中的變數;事先無法知道會是哪一種。嚴格模式將 with 視為語法錯誤,因此 with 中的名稱不可能在執行時引用到一個未知的位置。
"use strict";
const x = 17;
with (obj) {
// Syntax error
// If this weren't strict mode, would this be const x, or
// would it instead be obj.x? It's impossible in general
// to say without running the code, so the name can't be
// optimized.
x;
}
將物件賦給一個短名稱的變數,然後訪問該變數上的相應屬性,這種替代方案可以隨時取代 with。
不洩漏的 eval
在嚴格模式下,eval 不會將新變數引入到周圍的作用域中。在稀鬆模式下,eval("var x;") 會在周圍的函式或全域性作用域中引入一個變數 x。這意味著,通常在一個包含 eval 呼叫的函式中,每個不引用引數或區域性變數的名稱都必須在執行時對映到特定的定義(因為那個 eval 可能引入了一個新的變數,會隱藏外部變數)。在嚴格模式下,eval 只為被評估的程式碼建立變數,所以 eval 不能影響一個名稱是指向外部變數還是某個區域性變數。
var x = 17;
var evalX = eval("'use strict'; var x = 42; x;");
console.assert(x === 17);
console.assert(evalX === 42);
傳遞給 eval() 的字串是否在嚴格模式下被評估,取決於 eval() 是如何被呼叫的(直接 eval 或間接 eval)。
塊級作用域的函式宣告
JavaScript 語言規範從一開始就不允許在塊語句中巢狀函式宣告。然而,這種做法非常直觀,以至於大多數瀏覽器都將其作為擴充套件語法來實現。不幸的是,這些實現的語義各不相同,使得語言規範無法統一所有實現。因此,塊級作用域的函式宣告僅在嚴格模式下被明確規定(而它們曾經在嚴格模式下是不允許的),而在稀鬆模式下的行為在不同瀏覽器之間仍然存在差異。
讓 eval 和 arguments 更簡單
嚴格模式使得 arguments 和 eval 的行為不再那麼詭異。在稀鬆模式下,這兩者都涉及大量的魔法行為:eval 可以新增或刪除繫結以及改變繫結的值,而 arguments 會將其索引屬性與命名引數同步。嚴格模式在將 eval 和 arguments 視為關鍵字方面取得了巨大進展。
防止繫結或賦值給 eval 和 arguments
名稱 eval 和 arguments 不能在語言語法中被繫結或賦值。所有這樣做的嘗試都是語法錯誤。
"use strict";
eval = 17;
arguments++;
++eval;
const obj = { set p(arguments) {} };
let eval;
try {
} catch (arguments) {}
function x(eval) {}
function arguments() {}
const y = function eval() {};
const f = new Function("arguments", "'use strict'; return 17;");
引數與 arguments 索引之間不再同步
嚴格模式程式碼不會將 arguments 物件的索引與每個引數繫結同步。在稀鬆模式的函式中,如果第一個引數是 arg,那麼設定 arg 也會設定 arguments[0],反之亦然(除非沒有提供引數或者 arguments[0] 被刪除了)。對於嚴格模式函式,arguments 物件儲存的是函式被呼叫時的原始引數。arguments[i] 不會跟蹤相應命名引數的值,命名引數也不會跟蹤相應 arguments[i] 中的值。
function f(a) {
"use strict";
a = 42;
return [a, arguments[0]];
}
const pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);
“安全化”JavaScript
嚴格模式使得編寫“安全”的 JavaScript 更加容易。現在一些網站為使用者提供了編寫 JavaScript 的方式,這些 JavaScript 將由網站代表其他使用者執行。瀏覽器中的 JavaScript 可以訪問使用者的私人資訊,因此在執行之前,必須對這類 JavaScript 進行部分轉換,以審查對停用功能的訪問。JavaScript 的靈活性使得在沒有大量執行時檢查的情況下,實際上不可能做到這一點。某些語言功能非常普遍,以至於進行執行時檢查會帶來相當大的效能成本。一些嚴格模式的調整,加上要求使用者提交的 JavaScript 是嚴格模式程式碼,並以某種方式呼叫它,可以大大減少對這些執行時檢查的需求。
無 this 替換
在嚴格模式下,傳遞給函式的 this 值不會被強制轉換為物件(即“裝箱”)。對於稀鬆模式的函式,this 始終是一個物件:如果用一個物件作為 this 呼叫,它就是那個提供的物件;如果用一個原始值作為 this 呼叫,它就是 this 的裝箱值;如果用 undefined 或 null 作為 this 呼叫,它就是全域性物件。(使用 call、apply 或 bind 來指定一個特定的 this。)自動裝箱不僅會帶來效能成本,而且在瀏覽器中暴露全域性物件也是一個安全隱患,因為全域性物件提供了對“安全”JavaScript 環境必須限制的功能的訪問。因此,對於嚴格模式函式,指定的 this 不會被裝箱為物件,如果未指定,this 是 undefined 而不是 globalThis。
"use strict";
function fun() {
return this;
}
console.assert(fun() === undefined);
console.assert(fun.call(2) === 2);
console.assert(fun.apply(null) === null);
console.assert(fun.call(undefined) === undefined);
console.assert(fun.bind(true)() === true);
移除堆疊遍歷屬性
在嚴格模式下,不再可能“遍歷”JavaScript 堆疊。許多實現過去都實現了一些擴充套件功能,使得可以檢測函式的上游呼叫者。當一個函式 fun 正在被呼叫時,fun.caller 是最近呼叫 fun 的函式,而 fun.arguments 是那次 fun 呼叫的 arguments。這兩個擴充套件對於“安全”JavaScript 來說都是有問題的,因為它們允許“受限”程式碼訪問“特權”函式及其(可能不安全的)引數。如果 fun 處於嚴格模式,fun.caller 和 fun.arguments 都是不可刪除的屬性,在設定或獲取時會丟擲錯誤。
function restricted() {
"use strict";
restricted.caller; // throws a TypeError
restricted.arguments; // throws a TypeError
}
function privilegedInvoker() {
return restricted();
}
privilegedInvoker();
同樣,arguments.callee 也不再支援。在稀鬆模式下,arguments.callee 指向包含它的函式。這個用例很弱:直接給包含它的函式命名就好了!此外,arguments.callee 嚴重阻礙了像函式內聯這樣的最佳化,因為如果訪問了 arguments.callee,就必須能夠提供對未行內函數的引用。對於嚴格模式函式,arguments.callee 是一個不可刪除的屬性,在設定或獲取時會丟擲錯誤。
"use strict";
function f() {
return arguments.callee;
}
f(); // throws a TypeError
面向未來的 JavaScript
額外的保留字
保留字是不能用作變數名的識別符號。嚴格模式比稀鬆模式保留了更多的名稱,其中一些已經在語言中使用,另一些則為未來保留,以便更容易地實現未來的語法擴充套件。
過渡到嚴格模式
嚴格模式的設計使得向其過渡可以逐步進行。可以單獨更改每個檔案,甚至可以將程式碼過渡到嚴格模式,細化到函式粒度。
你可以透過首先向一段原始碼新增 "use strict",然後修復所有執行錯誤,同時注意語義上的差異,來將一個程式碼庫遷移到嚴格模式。
語法錯誤
新增 'use strict'; 時,以下情況將在指令碼執行前丟擲 SyntaxError:
- 八進位制語法
const n = 023; with語句- 對變數名使用
delete,如delete myVariable; - 使用
eval或arguments作為變數名或函式引數名 - 使用新增的保留關鍵字(為未來語言特性預留):
implements、interface、let、package、private、protected、public、static和yield - 宣告兩個同名的函式引數,如
function f(a, b, b) {} - 在物件字面量中兩次宣告同一個屬性名,如
{a: 1, b: 3, a: 7}。此限制後來被移除(bug 1041128)。
這些錯誤是有益的,因為它們揭示了明顯的錯誤或不良實踐。它們在程式碼執行之前發生,所以只要程式碼被執行時解析,就很容易發現。
新的執行時錯誤
在某些本應是錯誤的情況下,JavaScript 過去會靜默失敗。嚴格模式在這些情況下會丟擲錯誤。如果你的程式碼庫包含這類情況,需要進行測試以確保沒有東西被破壞。你可以以函式為粒度來篩選這類錯誤。
- 給未宣告的變數賦值會丟擲
ReferenceError。這在過去會在全域性物件上設定一個屬性,但這很少是預期的效果。如果你真的想給全域性物件設定一個值,請明確地將其作為globalThis上的一個屬性來賦值。 - 給物件的屬性賦值失敗(例如,屬性是隻讀的)會丟擲
TypeError。在稀鬆模式下,這會靜默失敗。 - 刪除不可刪除的屬性會丟擲
TypeError。在稀鬆模式下,這會靜默失敗。 - 如果函式處於嚴格模式,訪問
arguments.callee、strictFunction.caller或strictFunction.arguments會丟擲TypeError。如果你使用arguments.callee來遞迴呼叫函式,可以改用命名函式表示式。
語義差異
這些差異非常微小。測試套件可能無法捕捉到這種細微的差異。可能需要仔細審查你的程式碼庫,以確保這些差異不會影響程式碼的語義。幸運的是,這種仔細的審查可以逐步進行,細化到函式粒度。
this-
在稀鬆模式下,像
f()這樣的函式呼叫會把全域性物件作為this值傳遞。在嚴格模式下,它現在是undefined。當函式透過call或apply呼叫時,如果值是原始值,它會被裝箱成一個物件(對於undefined和null則是全域性物件)。在嚴格模式下,該值會直接傳遞,不進行轉換或替換。 arguments-
在稀鬆模式下,修改
arguments物件中的值會修改相應的命名引數。這使得 JavaScript 引擎的最佳化變得複雜,也使程式碼更難閱讀/理解。在嚴格模式下,arguments物件被建立並初始化為與命名引數相同的值,但對arguments物件或命名引數的更改不會相互反映。 eval-
在嚴格模式程式碼中,
eval不會在呼叫它的作用域中建立新變數。當然,在嚴格模式下,字串也會按照嚴格模式規則進行評估。需要進行徹底的測試以確保沒有東西被破壞。如果你不是真的需要,不使用 eval 可能是另一個務實的選擇。 - 塊級作用域的函式宣告
-
在稀鬆模式下,塊內的函式宣告可能在塊外可見,甚至可以呼叫。在嚴格模式下,塊內的函式宣告僅在塊內可見。
規範
| 規範 |
|---|
| ECMAScript® 2026 語言規範 |