JavaScript 資料型別和資料結構

程式語言都內建了資料結構,但這些資料結構通常因語言而異。本文試圖列出 JavaScript 中可用的內建資料結構及其特性。這些資料結構可以用來構建其他資料結構。

語言概覽提供了類似的常見資料型別摘要,但包含更多與其他語言的比較。

動態和弱型別

JavaScript 是一種動態語言,具有動態型別。JavaScript 中的變數不直接與任何特定的值型別相關聯,任何變數都可以被賦值(和重新賦值)為所有型別的值。

js
let foo = 42; // foo is now a number
foo = "bar"; // foo is now a string
foo = true; // foo is now a boolean

JavaScript 也是一種弱型別語言,這意味著當操作涉及不匹配的型別時,它允許隱式型別轉換,而不是丟擲型別錯誤。

js
const foo = 42; // foo is a number
const result = foo + "1"; // JavaScript coerces foo to a string, so it can be concatenated with the other operand
console.log(result); // 421

隱式強制轉換非常方便,但當轉換髮生在意想不到的地方,或者預期向另一個方向轉換時(例如,字串轉換為數字而不是數字轉換為字串),可能會產生難以察覺的錯誤。對於符號BigInt,JavaScript 有意禁止了某些隱式型別轉換。

原始值

物件之外的所有型別都定義了在語言最低層直接表示的不可變值。我們將這些型別的值稱為_原始值_。

null之外的所有原始型別都可以透過typeof運算子進行測試。typeof null返回"object",因此必須使用=== null來測試null

nullundefined之外的所有原始型別都有其對應的物件包裝器型別,這些型別提供了用於處理原始值的有用方法。例如,Number物件提供了諸如toExponential()之類的方法。當訪問原始值上的屬性時,JavaScript 會自動將該值包裝成相應的包裝器物件,並訪問該物件上的屬性。然而,訪問nullundefined上的屬性會丟擲TypeError異常,這使得引入可選鏈運算子成為必要。

型別 typeof 返回值 物件包裝器
Null "object" N/A
Undefined "undefined" N/A
Boolean "boolean" Boolean
Number "number" Number
BigInt "bigint" BigInt
String "string" String
符號 "symbol" 符號

物件包裝器類的參考頁面包含有關每種型別可用方法和屬性的更多資訊,以及原始型別本身的語義的詳細描述。

Null 型別

Null 型別只包含一個值:null

Undefined 型別

Undefined 型別只包含一個值:undefined

從概念上講,undefined表示_值_的缺失,而null表示_物件_的缺失(這也可以解釋typeof null === "object")。當某個事物沒有值時,語言通常預設使用undefined

  • 不帶值的return語句 (return;) 隱式返回undefined
  • 訪問不存在的物件屬性 (obj.iDontExist) 返回undefined
  • 未初始化的變數宣告 (let x;) 隱式將變數初始化為undefined
  • 許多方法,例如Array.prototype.find()Map.prototype.get(),在沒有找到元素時返回undefined

null在核心語言中使用的頻率要低得多。最重要的用途是原型鏈的末尾——因此,與原型互動的方法,例如Object.getPrototypeOf()Object.create()等,接受或返回null而不是undefined

null是一個關鍵字,但undefined是一個普通的識別符號,它恰好是一個全域性屬性。實際上,這種差異很小,因為undefined不應被重新定義或遮蔽。

布林型別

Boolean型別表示一個邏輯實體,包含兩個值:truefalse

布林值通常用於條件操作,包括三元運算子if...elsewhile等。

數字型別

Number型別是雙精度 64 位二進位制格式 IEEE 754 值。它能夠儲存 2-1074 (Number.MIN_VALUE) 到 21023 × (2 - 2-52) (Number.MAX_VALUE) 之間的正浮點數,以及相同量級的負浮點數,但它只能安全地儲存 -(253 − 1) (Number.MIN_SAFE_INTEGER) 到 253 − 1 (Number.MAX_SAFE_INTEGER) 範圍內的整數。超出此範圍,JavaScript 將無法安全地表示整數;它們將被雙精度浮點近似值表示。您可以使用Number.isSafeInteger()檢查數字是否在安全整數範圍內。

超出可表示範圍的值會自動轉換。

Infinity-Infinity的行為類似於數學上的無窮大,但有一些細微的差別;詳見Number.POSITIVE_INFINITYNumber.NEGATIVE_INFINITY

Number 型別只有一個值具有多種表示形式:0既表示為-0也表示為+0(其中0+0的別名)。實際上,不同表示形式之間幾乎沒有區別;例如,+0 === -0true。但是,當您除以零時,您能夠注意到這一點。

js
console.log(42 / +0); // Infinity
console.log(42 / -0); // -Infinity

NaN(“Not a Number”)是一種特殊型別的數值,通常在算術運算結果無法表示為數字時遇到。它也是 JavaScript 中唯一一個不等於自身的值。

儘管數字在概念上是“數學值”並總是隱式地以浮點編碼,但 JavaScript 提供了位運算子。在應用位運算子時,數字首先會轉換為 32 位整數。

注意:雖然位運算子_可以_使用位掩碼在單個數字中表示多個布林值,但這通常被認為是不良實踐。JavaScript 提供了其他方法來表示一組布林值(例如布林值陣列,或具有布林值賦值給命名屬性的物件)。位掩碼也傾向於使程式碼更難閱讀、理解和維護。

在非常受限的環境中可能需要使用此類技術,例如試圖應對本地儲存的限制,或在極端情況下(例如網路上傳輸的每個位都至關重要時)。只有在作為最佳化大小的最後措施時才應考慮此技術。

BigInt 型別

BigInt 型別是 JavaScript 中的一種數字原始型別,可以表示任意精度的整數。使用 BigInt,您可以安全地儲存和操作大整數,甚至超出 Number 的安全整數限制 (Number.MAX_SAFE_INTEGER)。

BigInt 透過在整數末尾新增n或呼叫BigInt()函式來建立。

此示例演示了遞增Number.MAX_SAFE_INTEGER返回預期結果的位置。

js
// BigInt
const x = BigInt(Number.MAX_SAFE_INTEGER); // 9007199254740991n
x + 1n === x + 2n; // false because 9007199254740992n and 9007199254740993n are unequal

// Number
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // true because both are 9007199254740992

您可以使用大多數運算子來處理 BigInt,包括+*-**%——唯一禁止的是>>>。BigInt 不嚴格等於具有相同數學值的 Number,但它寬鬆相等

BigInt 值並不總是比數字更精確,也不總是比數字不精確,因為 BigInt 不能表示小數,但可以更精確地表示大整數。兩種型別都不包含另一種,它們也不能相互替代。如果 BigInt 值與普通數字混合在算術表示式中,或者它們被隱式轉換,則會丟擲TypeError

字串型別

String型別表示文字資料,編碼為一系列 16 位無符號整數值,代表UTF-16 碼元。字串中的每個元素都佔據字串中的一個位置。第一個元素位於索引0,下一個位於索引1,依此類推。字串的長度是其中的 UTF-16 碼元數,這可能與 Unicode 字元的實際數量不對應;有關更多詳細資訊,請參閱String參考頁面。

JavaScript 字串是不可變的。這意味著一旦字串建立,就無法修改它。字串方法會根據當前字串的內容建立新字串——例如:

  • 使用substring()獲取原始字串的子字串。
  • 使用連線運算子 (+) 或concat()連線兩個字串。

警惕程式碼的“字串化型別”!

使用字串來表示複雜資料可能會很誘人。這樣做有短期好處:

  • 使用連線可以輕鬆構建複雜字串。
  • 字串易於除錯(你看到打印出來的內容總是字串中的內容)。
  • 字串是許多 API 的共同點(輸入欄位本地儲存值、使用Response.text()時的fetch()響應等),並且只使用字串可能會很誘人。

透過約定,可以在字串中表示任何資料結構。但這並不是一個好主意。例如,使用分隔符可以模擬列表(而 JavaScript 陣列更合適)。不幸的是,當分隔符用於其中一個“列表”元素時,列表就會被破壞。可以選擇跳脫字元等。所有這些都需要約定,並增加了不必要的維護負擔。

使用字串表示文字資料。在表示複雜資料時,_解析_字串,並使用適當的抽象。

Symbol 型別

Symbol是一個唯一不可變的原始值,可以用作物件屬性的鍵(見下文)。在某些程式語言中,Symbol 被稱為“原子”。符號的目的是建立唯一的屬性鍵,這些鍵保證不會與其他程式碼的鍵衝突。

物件

在計算機科學中,物件是記憶體中的一個值,可能被識別符號引用。在 JavaScript 中,物件是唯一可變的值。函式實際上也是物件,並具有額外的_可呼叫_能力。

屬性

在 JavaScript 中,物件可以看作是屬性的集合。使用物件字面量語法,可以初始化一組有限的屬性;然後可以新增和刪除屬性。物件屬性等同於鍵值對。屬性鍵可以是字串符號。當其他型別(如數字)用於索引物件時,這些值會隱式轉換為字串。屬性值可以是任何型別的值,包括其他物件,這使得構建複雜資料結構成為可能。

物件屬性有兩種型別:_資料_屬性_訪問器_屬性。每個屬性都有相應的_特性_。每個特性都由 JavaScript 引擎內部訪問,但您可以透過Object.defineProperty()設定它們,或透過Object.getOwnPropertyDescriptor()讀取它們。您可以在Object.defineProperty()頁面上閱讀有關各種細微差別的更多資訊。

資料屬性

資料屬性將鍵與值關聯。它可以由以下屬性描述:

value

透過對屬性進行 get 訪問檢索到的值。可以是任何 JavaScript 值。

可寫

一個布林值,指示屬性是否可以透過賦值更改。

可列舉

一個布林值,指示屬性是否可以透過for...in迴圈列舉。另請參閱屬性的可列舉性和所有權,瞭解可列舉性如何與其他函式和語法互動。

可配置

一個布林值,指示屬性是否可以刪除,是否可以更改為訪問器屬性,以及是否可以更改其屬性。

訪問器屬性

將鍵與兩個訪問器函式(getset)之一關聯,以檢索或儲存值。

注意:重要的是要認識到它是訪問器_屬性_——而不是訪問器_方法_。我們可以透過使用函式作為值來為 JavaScript 物件提供類似類的訪問器——但這並不能使該物件成為一個類。

訪問器屬性具有以下屬性:

get

在每次對值進行 get 訪問時,都會呼叫一個不帶引數的函式來檢索屬性值。另請參閱getter。可能為undefined

set

一個函式,它帶有一個包含賦值值的引數。每當嘗試更改指定屬性時執行。另請參閱setter。可能為undefined

可列舉

一個布林值,指示屬性是否可以透過for...in迴圈列舉。另請參閱屬性的可列舉性和所有權,瞭解可列舉性如何與其他函式和語法互動。

可配置

一個布林值,指示屬性是否可以刪除,是否可以更改為資料屬性,以及是否可以更改其屬性。

物件的原型指向另一個物件或null——它在概念上是物件的隱藏屬性,通常表示為[[Prototype]]。物件[[Prototype]]的屬性也可以在物件本身上訪問。

物件是臨時鍵值對,因此它們經常被用作對映。但是,可能存在人體工程學、安全性和效能問題。請使用Map來儲存任意資料。 Map引用包含了關於普通物件和對映之間儲存鍵值關聯的優缺點更詳細的討論。

日期

JavaScript 提供了兩套用於表示日期的 API:傳統的Date物件和現代的Temporal物件。Date有許多不良設計選擇,應儘可能在新程式碼中避免使用。

索引集合:陣列和型別化陣列

陣列是普通物件,其中整數鍵屬性和length屬性之間存在特定的關係。

此外,陣列繼承自Array.prototype,它提供了許多方便的方法來運算元組。例如,indexOf()在陣列中搜索值,push()將元素附加到陣列中。這使得陣列成為表示有序列表的完美選擇。

型別化陣列提供了一個底層二進位制資料緩衝區的類似陣列的檢視,並提供了許多與陣列對應物具有相似語義的方法。“型別化陣列”是多種資料結構的統稱,包括Int8ArrayFloat32Array等。有關更多資訊,請檢視型別化陣列頁面。型別化陣列通常與ArrayBufferDataView結合使用。

鍵控集合:Map、Set、WeakMap、WeakSet

這些資料結構將物件引用作為鍵。SetWeakSet表示唯一值的集合,而MapWeakMap表示鍵值關聯的集合。

您可以自行實現MapSet。然而,由於物件無法比較(例如,就<“小於”而言),引擎也不會暴露其物件的雜湊函式,因此查詢效能必然是線性的。它們的本機實現(包括WeakMap)可以具有近似對數時間到常數時間的查詢效能。

通常,要將資料繫結到 DOM 節點,可以直接在物件上設定屬性,或使用data-*屬性。缺點是資料可供在相同上下文中執行的任何指令碼訪問。MapWeakMap可以輕鬆地將資料_私下_繫結到物件。

WeakMapWeakSet只允許可垃圾回收的值作為鍵,這些值是物件或未註冊的符號,並且即使它們仍存在於集合中,這些鍵也可能被回收。它們專門用於記憶體使用最佳化

結構化資料:JSON

JSON (JavaScript Object Notation) 是一種輕量級資料交換格式,源自 JavaScript,但被許多程式語言使用。JSON 構建了可以在不同環境甚至跨語言傳輸的通用資料結構。有關更多詳細資訊,請參閱JSON

標準庫中的更多物件

JavaScript 有一個內建物件的標準庫。閱讀參考文件以瞭解有關內建物件的更多資訊。

型別轉換

如上所述,JavaScript 是一種弱型別語言。這意味著您通常可以在需要另一種型別的地方使用一種型別的值,語言會自動將其轉換為正確的型別。為此,JavaScript 定義了一些強制轉換規則。

原始值強制轉換

當需要原始值但對實際型別沒有強烈偏好時,使用原始值強制轉換過程。這通常發生在字串數字BigInt都同樣可接受時。例如:

  • Date()建構函式,當它接收到一個不是Date例項的引數時——字串表示日期字串,而數字表示時間戳。
  • +運算子——如果一個運算元是字串,則執行字串連線;否則,執行數字加法。
  • ==運算子——如果一個運算元是原始值,而另一個是物件,則物件被轉換為沒有首選型別的原始值。

如果值已經是原始值,此操作不進行任何轉換。物件透過呼叫其[Symbol.toPrimitive]()(以"default"為提示)、valueOf()toString()方法,按此順序轉換為原始值。請注意,原始值轉換優先呼叫valueOf(),這類似於數字轉換的行為,但與字串轉換不同。

如果存在,[Symbol.toPrimitive]()方法必須返回一個原始值——返回一個物件會導致TypeError。對於valueOf()toString(),如果其中一個返回一個物件,則返回值將被忽略,並使用另一個的返回值;如果兩者都不存在,或者兩者都不返回原始值,則會丟擲TypeError。例如,在以下程式碼中:

js
console.log({} + []); // "[object Object]"

{}[]都沒有[Symbol.toPrimitive]()方法。{}[]都從Object.prototype.valueOf繼承valueOf(),它返回物件本身。由於返回值是一個物件,它被忽略。因此,會呼叫toString(){}.toString()返回"[object Object]",而[].toString()返回"",因此結果是它們的連線:"[object Object]"

在轉換為任何原始型別時,[Symbol.toPrimitive]()方法始終優先。原始轉換通常行為類似於數字轉換,因為valueOf()優先呼叫;但是,具有自定義[Symbol.toPrimitive]()方法的物件可以選擇返回任何原始值。DateSymbol物件是唯一覆蓋[Symbol.toPrimitive]()方法的內建物件。Date.prototype[Symbol.toPrimitive]()"default"提示視為"string",而Symbol.prototype[Symbol.toPrimitive]()忽略提示並始終返回一個符號。

數字強制轉換

有兩種數字型別:NumberBigInt。有時語言會特別期望一個數字或一個 BigInt(例如Array.prototype.slice(),其中索引必須是一個數字);其他時候,它可能容忍兩者並根據運算元的型別執行不同的操作。對於不允許從其他型別進行隱式轉換的嚴格強制轉換過程,請參閱數字強制轉換BigInt強制轉換

數值強制轉換與數字強制轉換幾乎相同,不同之處在於 BigInt 會按原樣返回,而不會導致TypeError。所有算術運算子都使用數值強制轉換,因為它們對數字和 BigInt 都進行了過載。唯一的例外是一元加號,它總是執行數字強制轉換。

其他強制轉換

除 Null、Undefined 和 Symbol 之外的所有資料型別都有其各自的強制轉換過程。有關更多詳細資訊,請參閱字串強制轉換布林強制轉換物件強制轉換

正如您可能已經注意到的,物件可以透過三種不同的路徑轉換為原始值:

在所有情況下,如果[Symbol.toPrimitive]()存在,則必須可呼叫並返回一個原始值,而valueOftoString如果不可呼叫或返回一個物件,則將被忽略。在過程結束時,如果成功,結果保證是一個原始值。然後,根據上下文,生成的原始值將接受進一步的強制轉換。

另見