儲存資料的方式
NOTE
這個章節本來是沒有要寫的,但是寫到物件時覺得如果不先講清楚電腦科學的資料儲存方式,會讓後面寫物件時很難理解,所以就先寫這個章節。 可以作為接續之後的物件章節的前置知識。
在計算機科學中,資料是以二進位的方式存放的。
而在程式運行的過程中,資料會被創建並載入到記憶體中。
那什麼是記憶體?
記憶體可以想像成一批置物櫃格子,每個格子都有編號,而格子裡可以存放資料。
當我們宣告一個變數時,其實中間經過了好幾個步驟:
- 分配記憶體:系統會在記憶體中開啟一個格子,並給它一個編號,假設是
0x1A2B3C。 - 存放資料:將我們的資料(例如數字、字串等)存放到這個格子中。
- 對變數跟記憶體位址建立關聯:把變數名稱跟記憶體位址綁定起來,這樣我們就可以透過變數名稱來存取記憶體中的資料,像是
a => 0x1A2B3C。
當我們存取變數時,實際上是透過變數名稱,找到綁定的記憶體位址,接著才能讀取或修改記憶體中的資料。
以 JavaScript 為例:
let a // 在記憶體中開啟一個格子(編號 0x1A2B3C),並且綁定變數名稱 a
a = 10 // 透過變數 a 找到記憶體格子 0x1A2B3C,並將數字 10 存放到這個格子中
console.log(a); // 透過變數 a 找到記憶體格子 0x1A2B3C,並讀取這個格子中的數字 10這樣就完成一次資料的存取過程。
TIP
因為電腦的是透過控制電壓來進行計算(高電壓(1)和低電壓(0)兩種狀態)。
所以電腦科學中,資料的儲存方式是以二進位(binary)來表示的。
透過組合多個 bit,可以表示更大的數字或更多的資料。
以 8 bit 的 00000011 為例:
00000000 可以表示為八個 0 和 1 的組合,從右邊到左邊分別對應
以 8 bit 的 11111111 為例:
11111111 可以表示為八個 0 和 1 的組合,從右邊到左邊分別對應
儲存資料的限制
通常記憶體的大小上限是 8 bit 一格,也就是 1 byte。
每個 bit 可以表示兩種狀態(0 或 1),因此 8 bit 可以表示 2^8 = 256 種不同的狀態,也就是說一個格子最多能存放 0 ~ 255。
那麼如果我們要存放更大的數字或更多的資料呢?這時候就需要使用多個格子來存放資料。
例如,如果我們要存放一個 16 位元的整數,就需要使用 2 個格子來存放,分別是高位元和低位元。這樣可以讓我們存放的數字範圍擴大到 (2^16) - 1 = 65,535。
在 JavaScript 中,數字的儲存方式是使用 64 位元的浮點數格式,這意味著可以存放非常大的數字和小數。
let b = 12345678901234567890n; // 使用 BigInt 儲存大數字
console.log(b);以 JavaScript 為例,常見的資料格式有:
| 資料類型 | 儲存方式 | 資料長度 | 範圍 |
|---|---|---|---|
| 整數 | 32 位元 | 4 bytes | -2,147,483,648 到 2,147,483,647 |
| 浮點數 | 64 位元 | 8 bytes | 約 ±1.7976931348623157 × 10^308 |
| 字串 | 動態長度 | 依內容而定 | |
| 布林值 | 1 位元 | 1 byte | true 或 false |
| BigInt | 動態長度 | 依內容而定 |
物件的資料儲存
根據上述的內容我們可以得知:
- 變數是對記憶體位址的引用:變數名稱實際上是指向記憶體中某個格子的指標。
- 資料是以二進位的方式存放:這意味著所有資料在底層都是以 0 和 1 的形式存在。
- 資料的存放方式取決於資料類型:不同的資料類型有不同的儲存方式和範圍。
那麼問題來了,物件是如何儲存資料的呢?一個物件在 JavaScript 中長這樣:
let person = {
name: "Alice",
age: 30,
}上面這個物件同時具備了 name(String) 和 age(Number) 兩種資料類型。那它是怎麼儲存的?
在 JavaScript 中,物件實際上是由一組鍵值對組成的資料結構。每個鍵(key)都是一個字串,而每個值(value)可以是任意資料類型,包括基本資料格式、其他物件、陣列、函式等。
以下圖為例:
Stack(暫存變數位置)
┌─────────────┐
│ user │ ──────► 指向 (heap) 0xABCD
└─────────────┘
Heap(實際儲存物件內容)
0xABCD:
┌───────────────────────────┐
│ Object Header │ ← 引擎控制資料(GC, 型別, flags)
├───────────────────────────┤
│ "name" ──► 0xC000 │ ← 指向另一個值("Alice" 的位置)
│ "age" ──► 0xC100 │ ← 指向值 30 的記憶體位址
└───────────────────────────┘
0xC000:
┌────────────┐
│ "Alice" │ ← 儲存字串內容
└────────────┘
0xC100:
┌────────────┐
│ 30 (int) │ ← 儲存整數內容
└────────────┘當我們創建一個物件 user 時,JavaScript 會在記憶體中分配一個格子用來存放這個物件所有的屬性的所在位址。
然後為這個物件的每一個屬性分配一個記憶體位址,並且賦值給這些格子。
並將每個鍵值對(也就是 name -> "Alice", age -> 30)存放的位址,也用一個格子儲存起來,最後將這些格子的位址綁定到物件變數上。
這樣當我們透過 user.name 或 user.age 來存取物件的屬性時,JavaScript 會先找到物件變數在記憶體中的位址,然後再根據鍵名找到對應的位址,最後取得這些位址中的資料。
TIP
那如果物件的屬性也是一個物件或是陣列或是函式呢?就跟上面的步驟一樣,只是該屬性指向的位置也會是一個物件的位址。 也因為物件、陣列、函式對於記憶體的處理方式都一樣,所以事實上在 JavaScript 中,當你使用 typeof 檢查物件、陣列或函式時,都會得到 object 的結果。
所以才會說對 JavaScript 來說,萬物(Object, Array, Function)皆是物件(object)。
加碼:物件怎麼做比較
在前述的章節(運算子(Operator))中我們知道可以用 == 來比較兩個變數是否相等。
對於單純的資料類型(如數字、字串、布林值等),是可以直接比較值來確定是否相等。
但是對物件來說,情況就不一樣了。
物件是由屬性組成的資料結構,兩個物件的比較並不是用值來比較,而是用它們在記憶體中的位址來比較。
如前面的 物件的資料儲存 所述,當我們創建一個物件時,我們可以取得變數的記憶體位址,而記憶體位址內的值也會指向物件的屬性位址。
那麼,當兩個物件的記憶體位址相同時,其屬性的位址也會相同、屬性位址內含的值也必定相同
因此在 JavaScript 中,當我們使用 == 或 === 比較兩個物件時,實際上比較的是它們的記憶體位址,而不是物件的內容。
let obj1 = { name: "Alice" };
let obj2 = { name: "Alice" };
console.log(obj1 == obj2); // false,因為 obj1 和 obj2 的記憶體位址不同
let objA = { name: "Alice" };
let objB = objA; // objB 指向同一個物件
console.log(objA == objB); // true,因為 objA 和 objB 指向同一個物件的記憶體位址