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 有意地禁止了某些隱式型別轉換。

原始值

除了 Object 之外的所有型別都定義了不可變的值,這些值直接在語言的最低級別表示。我們將這些型別的變數稱為原始值。

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

除了 null 和 undefined 之外的所有原始型別都有其對應的物件包裝器型別,這些型別提供了用於處理原始值的有用方法。例如,Number 物件提供了諸如 toExponential() 之類的方法。當訪問原始值上的屬性時,JavaScript 會自動將該值包裝到相應的包裝器物件中,然後訪問該物件的屬性。但是,訪問 null 或 undefined 上的屬性會丟擲 TypeError 異常,因此需要引入可選鏈運算子。

型別 typeof 返回值 物件包裝器
Null "object" N/A
Undefined "undefined" N/A
Boolean "boolean" Boolean
Number "number" Number
BigInt "bigint" BigInt
String "string" String
Symbol "symbol" 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 型別

Boolean 型別表示邏輯實體,包含兩個值:true 和 false。

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

Number 型別

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() 檢查一個數字是否在安全整數範圍內。

不可表示範圍內的值會自動轉換

  • 大於 Number.MAX_VALUE 的正值會被轉換為 +Infinity。
  • 小於 Number.MIN_VALUE 的正值會被轉換為 +0。
  • 小於 -Number.MAX_VALUE 的負值會被轉換為 -Infinity。
  • 大於 -Number.MIN_VALUE 的負值會被轉換為 -0。

+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 中的一種數字基本型別,它可以表示任意大小的整數。使用 BigInts,你可以安全地儲存和操作大型整數,甚至超過 Numbers 的安全整數限制 (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

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

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

字串型別

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

JavaScript 字串是不可變的。這意味著一旦建立了字串,就無法修改它。字串方法基於當前字串的內容建立新字串 - 例如

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

小心“字串化”你的程式碼!

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

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

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

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

符號型別

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

物件

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

屬性

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

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

資料屬性

資料屬性將鍵與值關聯起來。它可以透過以下屬性來描述

value

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

writable

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

enumerable

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

configurable

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

訪問器屬性

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

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

訪問器屬性具有以下屬性

get

一個函式,在執行對值的獲取訪問時,使用空引數列表呼叫該函式以檢索屬性值。另請參閱 獲取器。可以為 undefined

set

一個函式,使用包含分配值的引數進行呼叫。在嘗試更改指定屬性時執行。另請參閱 設定器。可以為 undefined

enumerable

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

configurable

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

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

物件是臨時鍵值對,因此它們通常用作對映。但是,可能存在可用性、安全性和效能問題。使用 Map 來儲存任意資料。Map 參考 包含關於將普通物件和對映用於儲存鍵值關聯的優缺點的更詳細討論。

日期

在表示日期時,最佳選擇是使用 JavaScript 中的內建 Date 實用程式。

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

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

此外,陣列從 Array.prototype 繼承,這提供了一些方便的方法來運算元組。例如,indexOf() 在陣列中搜索值,而 push() 將元素追加到陣列。這使得陣列成為表示有序列表的理想選擇。

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

鍵控集合:對映、集合、弱對映、弱集合

這些資料結構以物件引用作為鍵。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()方法將物件轉換為原始值。 注意,原始轉換在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不可呼叫或返回一個物件,則會忽略它們。 在過程結束時,如果成功,則結果保證是一個原始值。 然後,根據上下文,生成的原始值將接受進一步的強制。

另請參見