Trong JS có hai phân loại kiểu dữ liệu: Primitive data typeNon-primitive data type.

Primitive Data Type

Các kiểu dữ liệu nguyên thủy bao gồm:

  1. Number: số nguyên (int) và số thực (float).
  2. BigInt: số nguyên lớn.
  3. String: tất cả các chuỗi nằm giữa dấu ``, ''""
  4. Boolean: true hoặc false.
  5. Symbol: là một giá trị duy nhất và không thể thay đổi.
  6. Undefined: biến khai báo nhưng không gán giá trị hoặc hàm không trả về giá trị.
  7. 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) // hello

Có 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à:

  1. Trích xuất dữ liệu của biến a.
  2. Thay đổi dữ liệu đó thành 'hello world!'.
  3. Cho dữ liệu này vào một vùng nhớ mới.
  4. Biến a trỏ đến vùng nhớ mới đó.
  5. 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) // false

Non-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 10

Undefined, 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) // undefined

null

Kiểu dữ liệu null thuộ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) // object

NaN

Kiểu dữ liệu của NaN là 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) // false

Cá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) // true

Reference

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, person1person2 đề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 person1person2, 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