let
const
const
和不變性const
和迴圈const
還是 let
globalThis
[ES2020]const
和 let
:暫時性死區var
:提升(部分提早啟用)這些是 JavaScript 宣告變數的主要方式
在 ES6 之前,也有 var
。但它有幾個怪癖,因此最好在現代 JavaScript 中避免使用它。您可以在 Speaking JavaScript 中閱讀更多相關資訊。
let
透過 let
宣告的變數是可以變更的
let i;
= 0;
i = i + 1;
i .equal(i, 1); assert
您也可以同時宣告和指定
let i = 0;
const
透過 const
宣告的變數是不可變的。您必須立即初始化
const i = 0; // must initialize
.throws(
assert=> { i = i + 1 },
()
{name: 'TypeError',
message: 'Assignment to constant variable.',
}; )
const
和不可變性在 JavaScript 中,const
僅表示繫結(變數名稱與變數值之間的關聯)是不可變的。值本身可能是可變的,例如以下範例中的 obj
。
const obj = { prop: 0 };
// Allowed: changing properties of `obj`
.prop = obj.prop + 1;
obj.equal(obj.prop, 1);
assert
// Not allowed: assigning to `obj`
.throws(
assert=> { obj = {} },
()
{name: 'TypeError',
message: 'Assignment to constant variable.',
}; )
const
和迴圈您可以在 for-of
迴圈中使用 const
,其中會為每個反覆運算建立新的繫結
const arr = ['hello', 'world'];
for (const elem of arr) {
console.log(elem);
}// Output:
// 'hello'
// 'world'
然而,在一般的 for
迴圈中,您必須使用 let
const arr = ['hello', 'world'];
for (let i=0; i<arr.length; i++) {
const elem = arr[i];
console.log(elem);
}
const
和 let
之間做決定我建議使用以下規則來在 const
和 let
之間做決定
const
表示不可變的繫結,且變數永遠不會變更其值。優先使用它。let
表示變數的值會變更。僅在您無法使用 const
時才使用它。 練習:
const
exercises/variables-assignment/const_exrc.mjs
變數的範圍是程式中可以存取它的區域。考慮以下程式碼。
// // Scope A. Accessible: x
{ const x = 0;
.equal(x, 0);
assert// Scope B. Accessible: x, y
{ const y = 1;
.equal(x, 0);
assert.equal(y, 1);
assert// Scope C. Accessible: x, y, z
{ const z = 2;
.equal(x, 0);
assert.equal(y, 1);
assert.equal(z, 2);
assert
}
}
}// Outside. Not accessible: x, y, z
.throws(
assert=> console.log(x),
()
{name: 'ReferenceError',
message: 'x is not defined',
}; )
x
的(直接)範圍。每個變數都可以在其直接範圍和所有嵌套在該範圍內的範圍中存取。
透過 const
和 let
宣告的變數稱為區塊範圍,因為它們的範圍永遠是最內層的周圍區塊。
您無法在同一個層級兩次宣告同一個變數
.throws(
assert=> {
() eval('let x = 1; let x = 2;');
,
}
{name: 'SyntaxError',
message: "Identifier 'x' has already been declared",
; })
為什麼是
eval()
?
eval()
會延遲剖析(因此也會延遲 SyntaxError
),直到執行 assert.throws()
的回呼函式。如果我們不使用它,我們會在剖析此程式碼時就收到錯誤,而 assert.throws()
甚至不會執行。
但是,您可以巢狀一個區塊,並使用您在區塊外使用的同一個變數名稱 x
const x = 1;
.equal(x, 1);
assert
{const x = 2;
.equal(x, 2);
assert
}.equal(x, 1); assert
在區塊內,內部的 x
是唯一可以存取的同名變數。內部的 x
稱為遮蔽外部的 x
。一旦您離開區塊,您就可以再次存取舊值。
測驗:基礎
請參閱 測驗應用程式。
所有其餘的章節都是進階的。
這兩個形容詞描述程式語言中的現象
我們來看這兩個術語的範例。
變數作用域是靜態現象。考量下列程式碼
function f() {
const x = 3;
// ···
}
x
是靜態(或詞彙)作用域。也就是說,它的作用域是固定的,且在執行階段不會變更。
變數作用域形成靜態樹(透過靜態巢狀結構)。
函式呼叫是動態現象。考量下列程式碼
function g(x) {}
function h(y) {
if (Math.random()) g(y); // (A)
}
A 行中的函式呼叫是否發生,只能在執行階段決定。
函式呼叫形成動態樹(透過動態呼叫)。
JavaScript 的變數作用域是巢狀的。它們形成一棵樹
根節點也稱為全域作用域。在網路瀏覽器中,直接位於該作用域的唯一位置是指令碼的最上層。全域作用域的變數稱為全域變數,且可以在任何地方存取。有兩種全域變數
const
、let
和類別宣告在指令碼的最上層建立。var
和函式宣告建立。globalThis
存取。它可用於建立、讀取和刪除全域物件變數。下列 HTML 片段示範了 globalThis
和兩種全域變數。
<script>
const declarativeVariable = 'd';
var objectVariable = 'o';
</script>
<script>
// All scripts share the same top-level scope:
console.log(declarativeVariable); // 'd'
console.log(objectVariable); // 'o'
// Not all declarations create properties of the global object:
console.log(globalThis.declarativeVariable); // undefined
console.log(globalThis.objectVariable); // 'o'
</script>
每個 ECMAScript 模組都有自己的作用域。因此,存在於模組最上層的變數不是全域性的。圖 5 說明了各種作用域之間的關聯性。
globalThis
[ES2020]全域變數 globalThis
是存取全域物件的新標準方式。它的名稱來自於它在全域作用域中與 this
具有相同的值。
globalThis
不總是直接指向全域物件
例如,在瀏覽器中,有一個間接層。該間接層通常不會被注意到,但它確實存在,而且可以被觀察到。
globalThis
的替代方案較舊的存取全域物件的方式取決於平台
window
:是參考全域物件的傳統方式。但它在 Node.js 和 Web Workers 中不起作用。self
:通常在 Web Workers 和瀏覽器中可用。但它不受 Node.js 支援。global
:僅在 Node.js 中可用。globalThis
的使用案例由於向後相容性的問題,全域物件現在被認為是 JavaScript 無法擺脫的一個錯誤。它會對效能產生負面影響,而且通常會造成混淆。
ECMAScript 6 導入了幾個功能,讓避免使用全域物件變得更容易,例如
const
、let
和類別宣告不會建立全域物件屬性。通常最好透過變數存取全域物件變數,而不是透過 globalThis
的屬性。前者在所有 JavaScript 平台上始終都能以相同的方式運作。
網路上的教學偶爾會透過 window.globVar
存取全域變數 globVar
。但前綴「window.
」並非必要,我建議省略它
window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes
因此,globalThis
的使用案例相對較少,例如
以下是宣告的兩個關鍵面向
表格 1 總結了各種宣告如何處理這些面向。
範圍 | 啟動 | 重複 | 全域屬性 | |
---|---|---|---|---|
const |
區塊 | 宣告 (TDZ) | ✘ |
✘ |
let |
區塊 | 宣告 (TDZ) | ✘ |
✘ |
函式 |
區塊 (*) | 開始 | ✔ |
✔ |
類別 |
區塊 | 宣告 (TDZ) | ✘ |
✘ |
匯入 |
模組 | 與 export 相同 | ✘ |
✘ |
var |
函式 | 開始,部分 | ✔ |
✔ |
import
在 §27.5「ECMAScript 模組」 中有說明。以下各節會更詳細地說明其他建構式。
const
和 let
:暫時性死區對於 JavaScript,TC39 需要決定在宣告常數之前,於其直接範圍內存取常數時會發生什麼事
{console.log(x); // What happens here?
const x;
}
一些可能的方法是
undefined
。方法 1 被拒絕,因為這方法在語言中沒有先例。因此,對於 JavaScript 程式設計師來說,這不會直覺。
方法 2 被拒絕,因為這樣 x
就不是常數了 - 它在宣告前後會有不同的值。
let
使用與 const
相同的方法 3,因此兩者運作方式類似,且可以輕易地在它們之間切換。
進入變數範圍和執行其宣告之間的時間稱為該變數的暫時性死區 (TDZ)
ReferenceError
。undefined
- 如果沒有初始化項。以下程式碼說明了暫時性死區
if (true) { // entering scope of `tmp`, TDZ starts
// `tmp` is uninitialized:
.throws(() => (tmp = 'abc'), ReferenceError);
assert.throws(() => console.log(tmp), ReferenceError);
assert
let tmp; // TDZ ends
.equal(tmp, undefined);
assert }
下一個範例顯示暫時性死區確實是暫時性的(與時間相關)
if (true) { // entering scope of `myVar`, TDZ starts
const func = () => {
console.log(myVar); // executed later
;
}
// We are within the TDZ:
// Accessing `myVar` causes `ReferenceError`
let myVar = 3; // TDZ ends
func(); // OK, called outside TDZ
}
即使 func()
位於 myVar
的宣告之前並使用該變數,我們也可以呼叫 func()
。但是我們必須等到 myVar
的暫時性死區結束。
有關函式的更多資訊
在本節中,我們使用函式 - 在我們有機會適當地學習它們之前。希望一切都仍然有意義。如果沒有,請參閱 §25「可呼叫值」。
函式宣告在進入其範圍時總是執行,無論它位於該範圍內的何處。這使您可以在宣告 foo()
之前呼叫它
.equal(foo(), 123); // OK
assertfunction foo() { return 123; }
foo()
的早期啟動表示前一個程式碼等於
function foo() { return 123; }
.equal(foo(), 123); assert
如果您透過 const
或 let
宣告函數,則不會提早啟動。在以下範例中,您只能在宣告後使用 bar()
。
.throws(
assert=> bar(), // before declaration
() ReferenceError);
const bar = () => { return 123; };
.equal(bar(), 123); // after declaration assert
即使函數 g()
沒有提早啟動,它仍可以被同一個範圍內的先行函數 f()
呼叫,只要我們遵守下列規則:f()
必須在宣告 g()
之後呼叫。
const f = () => g();
const g = () => 123;
// We call f() after g() was declared:
.equal(f(), 123); assert
模組的函數通常會在執行完完整內容後才呼叫。因此,在模組中,您很少需要擔心函數的順序。
最後,請注意提早啟動如何自動遵守上述規則:在進入範圍時,所有函數宣告會先執行,然後才會進行任何呼叫。
如果您依賴提早啟動在函數宣告之前呼叫函數,則需要小心它不會存取未提早啟動的資料。
funcDecl();
const MY_STR = 'abc';
function funcDecl() {
.throws(
assert=> MY_STR,
() ReferenceError);
}
如果您在宣告 MY_STR
之後呼叫 funcDecl()
,問題就會消失。
我們已經看到提早啟動有一個陷阱,而且您可以在不使用它的情況下獲得大部分的好處。因此,最好避免提早啟動。但我對此沒有強烈感覺,而且如前所述,我經常使用函數宣告,因為我喜歡它們的語法。
即使類別宣告在某些方面類似於函數宣告,類別宣告不會提早啟動
.throws(
assert=> new MyClass(),
() ReferenceError);
class MyClass {}
.equal(new MyClass() instanceof MyClass, true); assert
這是為什麼?請考慮下列類別宣告
class MyClass extends Object {}
extends
的運算元是一個表達式。因此,您可以執行以下操作
const identity = x => x;
class MyClass extends identity(Object) {}
此類表達式的評估必須在提及它的位置進行。其他任何事情都會令人困惑。這說明了為什麼類別宣告不會提早啟動。
var
:提升(部分提早啟動)var
是一種較舊的變數宣告方式,早於 const
和 let
(現在較為優先)。請考慮以下 var
宣告。
var x = 123;
此宣告有兩個部分
var x
:var
宣告變數的範圍是內層包圍函數,而不是內層包圍區塊,就像大多數其他宣告一樣。此類變數在範圍開始時已啟動,並初始化為 undefined
。x = 123
:指定會在原地執行。以下程式碼示範 var
的效果
function f() {
// Partial early activation:
.equal(x, undefined);
assertif (true) {
var x = 123;
// The assignment is executed in place:
.equal(x, 123);
assert
}// Scope is function, not block:
.equal(x, 123);
assert }
在我們探索閉包之前,我們需要了解繫結變數和自由變數。
在每個範圍中,都有一組被提及的變數。在這些變數中,我們區分
考慮以下程式碼
function func(x) {
const y = 123;
console.log(z);
}
在 func()
的主體中,x
和 y
是約束變數。z
是自由變數。
那麼,什麼是閉包?
閉包是一個函式,加上與其「誕生地」中存在的變數的連線。
保留此連線的目的是什麼?它提供函式的自由變數的值,例如
function funcFactory(value) {
return () => {
return value;
;
}
}
const func = funcFactory('abc');
.equal(func(), 'abc'); // (A) assert
funcFactory
傳回一個閉包,並指定給 func
。由於 func
與其誕生地的變數有連線,因此它在 A 行中被呼叫時仍可存取自由變數 value
(即使它已經「跳脫」其範圍)。
JavaScript 中的所有函式都是閉包
靜態範圍透過 JavaScript 中的閉包獲得支援。因此,每個函式都是閉包。
以下函式傳回遞增器(我剛編造的名稱)。遞增器是一個在內部儲存數字的函式。當它被呼叫時,它會透過將參數加到該數字上來更新該數字,並傳回新的值。
function createInc(startValue) {
return (step) => { // (A)
+= step;
startValue return startValue;
;
}
}const inc = createInc(5);
.equal(inc(2), 7); assert
我們可以看到,在 A 行中建立的函式會在自由變數 startValue
中保留其內部數字。這次,我們不只是從誕生範圍中讀取,我們使用它來儲存我們變更且會在函式呼叫中持續存在的資料。
我們可以透過區域變數在誕生範圍中建立更多儲存槽
function createInc(startValue) {
let index = -1;
return (step) => {
+= step;
startValue ++;
indexreturn [index, startValue];
;
}
}const inc = createInc(5);
.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]); assert
閉包有什麼好處?
首先,它們只是靜態範圍的一種實作。因此,它們為回呼提供內容資料。
函式也可以使用它們來儲存會在函式呼叫中持續存在的狀態。createInc()
就是一個範例。
而且它們可以提供物件的私有資料(透過文字或類別產生)。有關其運作方式的詳細資訊說明於 Exploring ES6。
測驗:進階
請參閱 測驗應用程式。