Trong JS có hai phân loại kiểu dữ liệu: Primitive data type và Non-primitive data type.
Primitive Data Type
Các kiểu dữ liệu nguyên thủy bao gồm:
- Number: số nguyên (int) và số thực (float).
- BigInt: số nguyên lớn.
- String: tất cả các chuỗi nằm giữa dấu
``,''và"" - Boolean: true hoặc false.
- Symbol: là một giá trị duy nhất và không thể thay đổi.
- Undefined: biến khai báo nhưng không gán giá trị hoặc hàm không trả về giá trị.
- Null: rỗng hoặc không có giá trị.
Kiểu dữ liệu nguyên thủy chỉ chứa được một giá trị tại một thời điểm.
Immutable Object
JS là một ngôn ngữ OOP, chương trình của nó được xây dựng dựa trên các JS Objects và mọi thứ của JS đều là object.
Các kiểu dữ liệu primitive trong JS cũng là các object, nhưng chúng thuộc một loại object đặc biệt gọi là Immutable Object (đối tượng bất biến).
Immutable object
Đối tượng bất biến là đối tượng mà trạng thái của nó (các thuộc tính bên trong) không thể thay đổi sau khi được tạo ra.
Xét ví dụ sau:
let a = "hello"
a[2] = "e"
console.log(a) // helloCó thể thấy, chúng ta không thể thay đổi nội dung bên trong của biến a.
Reassign
Nếu cố tình gán biến a bằng một giá trị mới, như đoạn code sau:
let a = "hello"
a = "hello world!"
console.log(a) // hello world!Thì có vẻ như giá trị của biến a đã được thay đổi. Tuy nhiên, điều mà đoạn code trên thực sự thực hiện là:
- Trích xuất dữ liệu của biến
a. - Thay đổi dữ liệu đó thành
'hello world!'. - Cho dữ liệu này vào một vùng nhớ mới.
- Biến
atrỏ đến vùng nhớ mới đó. - Vùng nhớ cũ chứa
'hello'sẽ được dọn rác.
Như vậy, bản thân giá trị 'hello' là không thay đổi được, mỗi lần ta dùng phép gán (reassign) là ta đã làm cho biến có một vùng nhớ mới có địa chỉ hoàn toàn khác. Hay nói cách khác, ta đã dùng một phương thức (toán tử gán bằng) tác động vào a và làm thay đổi giá trị của nó.
Comparison
Chúng ta có thể so sánh hai biến primitive thông qua giá trị của nó.
let b = 7
let c = 5
console.log(b == c) // falseNon-reference
Info
Khi ta thay đổi giá trị một biến có kiểu dữ liệu primitive thì sẽ không làm thay đổi biến khác. Lý do là vì chúng không phải là kiểu dữ liệu reference (tham chiếu).
let a = 9
let b = a
b = 10
console.log(a, b) // 9 10Undefined, Null and NaN
undefined
Kiểu dữ liệu
undefinedđược khởi tạo khi biến được khai báo mà không gán giá trị hoặc gán bằng một hàm không trả về giá trị.
Để có thể kiểm tra kiểu dữ liệu của biến, ta có thể sử dụng toán tử typeof:
var unassigned
console.log(typeof unassigned) // undefinednull
Kiểu dữ liệu
nullthuộc kiểu object (không phải kiểu dữ liệu null) nhưng vẫn là một kiểu dữ liệu nguyên thủy.
Kiểu dữ liệu này thể hiện giá trị của một biến/object nào đó là rỗng hoặc không có giá trị.
var empty = null
console.log(typeof empty) // objectNaN
Kiểu dữ liệu của
NaNlà Number.
Non-primitive Data Type
Các kiểu dữ liệu non-primitive bao gồm:
Kiểu dữ liệu không nguyên thủy có thể chứa được nhiều giá trị tại một thời điểm.
Mỗi khi một object, array hay function được tạo ra thì đều sẽ có một vùng nhớ mới được tạo ra.
Reassign
Chúng ta có thể thay đổi giá trị của các kiểu dữ liệu không nguyên thủy sau khi chúng được tạo ra:
let nums = [1, 2, 3]
nums = [3, 4, 5]
console.log(nums) // [3, 4, 5]Comparison
Attention
Không thể so sánh hai đối tượng thuộc kiểu dữ liệu non-primitive, mặc dù thuộc tính và giá trị của chúng là như nhau.
let nums = [1, 2, 3]
let numbers = [1, 2, 3]
console.log(nums == numbers) // falseCác kiểu dữ liệu non-primitive thường được xem là các kiểu dữ liệu reference (tham chiếu). Bởi vì chúng được so sánh dựa trên tham chiếu thay vì giá trị. Hai đối tượng là bằng nhau nếu như chúng cùng tham chiếu đến một vùng nhớ.
let nums = [1, 2, 3]
let numbers = nums
console.log(nums == numbers) // trueReference
Attention
Khi thay đổi dữ liệu của một biến tham chiếu, giá trị tại vùng nhớ mà biến đó tham chiếu đến sẽ thay đổi. Dẫn đến, các biến khác trỏ vào vùng nhớ đó cũng có sự thay đổi giá trị.
Trong ví dụ bên dưới, khi biến person2 được khai báo, trình thông dịch sẽ cấp phát một vùng nhớ cho biến này. Sau đó, địa chỉ ô nhớ mà biến person1 đang nắm giữ sẽ được sao chép qua cho biến person2.
let person1 = { name: "Kwan" }
let person2 = person1
person2.name = "Kedo"
console.log(person1) // {name: "Kedo"}
console.log(person2) // {name: "Kedo"}Về bản chất, person1 và person2 đều có giá trị là vùng nhớ (tương tự như con trỏ trong C-C++). Chỉ khác ở chỗ, khi chúng ta truy cập giá trị của person1 và person2, giá trị trả về sẽ là {name: "Kwan"} thay vì địa chỉ của một vùng nhớ.
Điều này giải thích tại sao hai object khác vùng nhớ lại không bằng nhau trong phép so sánh mặc dù giá trị của chúng là bằng nhau.
Create a Whole New Object
Để tạo ra một object mới hoàn toàn từ object cho trước, ta sử dụng các hàm của kiểu JSON. Cụ thể, ta biến object cho trước thành một chuỗi JSON, rồi lại chuyển ngược lại về object.
const pokemon1 = { name: "Pikachu" }
const pokemon2 = pokemon1 // reference to the old object
const pokemon3 = JSON.parse(JSON.stringify(pokemon1)) // reference to the new object
pokemon3.name = "Arceus"
console.log(pokemon1) // {name: "Pikachu"}
console.log(pokemon2) // {name: "Pikachu"}
console.log(pokemon3) // {name: "Arceus"}Cách này có một khuyết điểm là không thể sử dụng lên các object quá nặng, có thể gây giảm hiệu năng.
Nếu object không quá phức tạp và không có các object con bên trong, có thể sử dụng spread operator.
const pokemon1 = { name: "Pikachu" }
const pokemon2 = pokemon1 // reference to the old object
const pokemon3 = { ...pokemon1 } // reference to the new object