Khởi đầu câu chuyện: Tờ giấy ghi công thức và cuốn sổ bí kíp
Chào mừng bạn quay trở lại với chuyên mục “Kiến thức lập trình” của The Blogs News! Hãy tưởng tượng một kịch bản đơn giản trong cuộc sống: bạn có một công thức nấu ăn siêu ngon và muốn chia sẻ nó cho người bạn thân. Bạn có hai cách:
- Cách 1: Bạn cẩn thận chép lại công thức ra một tờ giấy mới và đưa cho người bạn. Người bạn có thể thoải mái ghi chú, thêm thắt, hay thậm chí làm rách tờ giấy đó mà không hề ảnh hưởng đến công thức gốc của bạn.
- Cách 2: Bạn đưa cho người bạn mượn nguyên cả cuốn sổ tay bí kíp nấu ăn của mình. Bất cứ thay đổi nào người bạn thực hiện – ví dụ như làm đổ cà phê lên trang giấy – cũng sẽ ảnh hưởng trực tiếp đến cuốn sổ gốc của bạn.
Trong thế giới lập trình, hai kịch bản này tương ứng với hai khái niệm cực kỳ quan trọng và là nền tảng cốt lõi mà bất kỳ lập trình viên nào cũng phải nắm vững: Truyền tham trị (Pass by Value) và Truyền tham chiếu (Pass by Reference). Hiểu sai hoặc nhầm lẫn giữa chúng có thể dẫn đến những lỗi (bug) khó lường và hàng giờ gỡ rối mệt mỏi. Vậy chúng thực sự là gì và tại sao lại quan trọng đến vậy? Hãy cùng khám phá ngay sau đây!
Truyền tham trị (Pass by Value): An toàn là trên hết
Quay lại ví dụ tờ giấy chép tay, đó chính là hình ảnh minh họa hoàn hảo cho cơ chế truyền tham trị. Khi bạn truyền một biến vào hàm theo kiểu tham trị, thực chất bạn đang tạo ra một bản sao (copy) giá trị của biến đó và gửi bản sao này vào hàm. Mọi thao tác, tính toán, thay đổi bên trong hàm đều được thực hiện trên bản sao này, hoàn toàn không đụng chạm gì đến biến gốc bên ngoài.

Cơ chế hoạt động
Hãy hình dung trong bộ nhớ máy tính. Khi bạn khai báo một biến, ví dụ let tuoi = 25;, hệ thống sẽ cấp phát một ô nhớ để lưu giá trị 25. Khi bạn gọi một hàm và truyền biến tuoi vào, ví dụ tinhTuoiNghiHuu(tuoi), điều gì sẽ xảy ra?
- Một ô nhớ hoàn toàn mới được tạo ra bên trong phạm vi (scope) của hàm
tinhTuoiNghiHuu. - Giá trị của biến
tuoi(là25) được sao chép và gán vào ô nhớ mới này. - Hàm sẽ làm việc với ô nhớ mới chứa giá trị
25. Dù nó có cộng, trừ, nhân, chia giá trị này, biếntuoigốc ở bên ngoài vẫn không hề hay biết và giữ nguyên giá trị25.
Ví dụ thực tế với JavaScript
Hãy xem một đoạn code ngắn để thấy rõ hơn:
function tangTuoi(tuoiHienTai) {
// Thao tác trên biến "tuoiHienTai" chỉ ảnh hưởng trong hàm này
tuoiHienTai = tuoiHienTai + 1;
console.log("Tuổi trong hàm:", tuoiHienTai); // Kết quả: 26
}
let tuoiCuaToi = 25;
console.log("Tuổi ban đầu:", tuoiCuaToi); // Kết quả: 25
tangTuoi(tuoiCuaToi);
console.log("Tuổi sau khi gọi hàm:", tuoiCuaToi); // Kết quả: Vẫn là 25!
Như bạn thấy, dù hàm tangTuoi đã thay đổi giá trị, biến tuoiCuaToi ở ngoài vẫn “bình an vô sự”. Đây chính là sức mạnh của truyền tham trị: sự an toàn và tính đoán trước được. Dữ liệu gốc của bạn được bảo vệ khỏi những thay đổi không mong muốn.
Ưu và nhược điểm
- Ưu điểm: An toàn dữ liệu tuyệt đối. Dữ liệu gốc không bị thay đổi bởi các hàm con, giúp việc kiểm soát luồng chương trình và gỡ lỗi dễ dàng hơn.
- Nhược điểm: Tốn bộ nhớ và hiệu năng. Với mỗi lần gọi hàm, hệ thống phải tạo một bản sao dữ liệu. Nếu dữ liệu lớn (ví dụ một chuỗi văn bản dài), việc sao chép này có thể làm chậm chương trình.
Truyền tham trị thường được áp dụng cho các kiểu dữ liệu nguyên thủy (primitive types) như số (number), chuỗi ký tự (string), boolean (true/false), null, undefined…
Truyền tham chiếu (Pass by Reference): Sức mạnh và sự sẻ chia
Nếu truyền tham trị là đưa một bản sao, thì truyền tham chiếu chính là đưa “chìa khóa” vào kho báu. Thay vì sao chép toàn bộ dữ liệu, chương trình chỉ truyền đi địa chỉ của vùng nhớ nơi dữ liệu đó đang được lưu trữ. Hàm nhận được địa chỉ này sẽ có toàn quyền truy cập và thay đổi trực tiếp dữ liệu gốc.
Cơ chế hoạt động
Giờ hãy tưởng tượng bạn có một đối tượng (object) chứa thông tin người dùng: let user = { name: 'An', age: 30 };. Biến user không thực sự chứa đối tượng đó, mà nó chứa một tham chiếu (giống như địa chỉ nhà) trỏ đến vị trí của đối tượng trong bộ nhớ.
Khi bạn truyền biến user vào một hàm, ví dụ capNhatEmail(user), chuyện gì xảy ra?
- Một biến mới được tạo ra trong hàm, nhưng thay vì sao chép toàn bộ đối tượng, nó chỉ sao chép cái địa chỉ (tham chiếu) kia.
- Kết quả là cả biến gốc bên ngoài và biến trong hàm đều cùng trỏ về một đối tượng duy nhất trong bộ nhớ.
- Bất kỳ thay đổi nào được thực hiện thông qua tham chiếu bên trong hàm (ví dụ thêm thuộc tính email) sẽ làm thay đổi đối tượng gốc.
Ví dụ thực tế với JavaScript
Hãy xem cách nó hoạt động với một đối tượng:
function capNhatThongTin(thongTinCaNhan) {
// Thay đổi trực tiếp trên đối tượng gốc thông qua tham chiếu
thongTinCaNhan.thanhPho = "Hà Nội";
console.log("Đối tượng trong hàm:", thongTinCaNhan);
// Kết quả: { ten: 'Bình', tuoi: 28, thanhPho: 'Hà Nội' }
}
let nguoiDung = { ten: "Bình", tuoi: 28 };
console.log("Đối tượng ban đầu:", nguoiDung);
// Kết quả: { ten: 'Bình', tuoi: 28 }
capNhatThongTin(nguoiDung);
console.log("Đối tượng sau khi gọi hàm:", nguoiDung);
// Kết quả: { ten: 'Bình', tuoi: 28, thanhPho: 'Hà Nội' } - Đã bị thay đổi!
Bạn thấy không? Chỉ một lời gọi hàm đã làm thay đổi vĩnh viễn dữ liệu gốc. Đây là một cơ chế cực kỳ mạnh mẽ và hiệu quả, nhưng cũng tiềm ẩn rủi ro nếu không được sử dụng cẩn thận.
Ưu và nhược điểm
- Ưu điểm: Hiệu năng cao. Không cần sao chép dữ liệu lớn, chỉ cần truyền một địa chỉ bộ nhớ nhỏ gọn. Rất hữu ích khi làm việc với các cấu trúc dữ liệu phức tạp.
- Nhược điểm: Rủi ro cao về tác dụng phụ (side effects). Một hàm có thể vô tình thay đổi dữ liệu mà các phần khác của chương trình đang sử dụng, gây ra các lỗi khó tìm và logic khó hiểu.
Truyền tham chiếu thường được áp dụng cho các kiểu dữ liệu phức hợp (complex types) như đối tượng (object), mảng (array), hàm (function).
So sánh trực quan: Đặt lên bàn cân
Để giúp bạn dễ hình dung hơn, hãy cùng xem bảng so sánh nhanh giữa hai cơ chế này:

- Tiêu chí: Dữ liệu được truyền đi
- Tham trị: Một bản sao của giá trị.
- Tham chiếu: Địa chỉ trỏ đến vùng nhớ của dữ liệu.
- Tiêu chí: Vùng nhớ
- Tham trị: Tạo ra vùng nhớ mới cho bản sao.
- Tham chiếu: Cùng sử dụng chung một vùng nhớ.
- Tiêu chí: Tác động đến dữ liệu gốc
- Tham trị: An toàn, không thay đổi.
- Tham chiếu: Bị thay đổi trực tiếp.
- Tiêu chí: Hiệu năng
- Tham trị: Chậm hơn khi dữ liệu lớn do phải sao chép.
- Tham chiếu: Nhanh và hiệu quả, không cần sao chép.
“Cạm bẫy” và mẹo vặt cuộc sống cho lập trình viên
Hiểu lý thuyết là một chuyện, áp dụng vào thực tế lại là một câu chuyện khác. Dưới đây là một vài “mẹo vặt” giúp bạn làm chủ hai khái niệm này.
Cái bẫy “lai căng” trong JavaScript
Nhiều người tranh cãi rằng JavaScript thực chất không có truyền tham chiếu thuần túy, mà sử dụng một cơ chế gọi là “Pass by Sharing”. Về cơ bản, khi bạn truyền một đối tượng, bạn đang truyền một bản sao của tham chiếu (pass by value, where the value is a reference). Điều này nghe có vẻ phức tạp, nhưng mẹo để nhớ là: Bạn có thể thay đổi thuộc tính của đối tượng, nhưng bạn không thể gán lại toàn bộ đối tượng đó cho một đối tượng mới bên trong hàm và mong nó ảnh hưởng ra bên ngoài.

Mẹo 1: Chủ động tạo bản sao khi cần an toàn
Nếu bạn muốn truyền một đối tượng vào hàm để xử lý nhưng lại không muốn đối tượng gốc bị thay đổi thì phải làm sao? Mẹo ở đây là hãy chủ động tạo một bản sao trước khi truyền vào.
// Sử dụng toán tử spread (...) để tạo một bản sao nông (shallow copy)
capNhatThongTin({...nguoiDung});
// Hoặc với mảng, có thể dùng slice() hoặc spread
let mangMoi = [...mangCu];
Bằng cách này, bạn có được sự an toàn của truyền tham trị và hiệu năng của truyền tham chiếu (vì chỉ sao chép một lần khi cần).
Mẹo 2: Đặt tên hàm rõ ràng
Một mẹo đơn giản nhưng cực kỳ hiệu quả. Nếu một hàm được thiết kế để thay đổi đối tượng đầu vào, hãy đặt tên nó một cách gợi mở. Ví dụ:
- Tên tốt:
updateUserAddress(user, newAddress),sortArrayInPlace(arr) - Tên dễ gây hiểu lầm:
processData(data)(process là gì? có thay đổi không?)
Nắm vững tham trị và tham chiếu – Nâng tầm tư duy lập trình
Việc phân biệt và vận dụng thành thạo truyền tham trị và truyền tham chiếu không chỉ là một kiến thức lý thuyết. Nó là chìa khóa để viết ra những đoạn code sạch sẽ, hiệu quả, dễ bảo trì và quan trọng nhất là… chạy đúng như ý muốn. Nó ảnh hưởng trực tiếp đến cách bạn quản lý bộ nhớ, tối ưu hóa hiệu năng và tránh được những lỗi logic “trời ơi đất hỡi”.
Hy vọng qua bài viết này của The Blogs News, bạn đã có một cái nhìn thật rõ ràng và trực quan về hai khái niệm nền tảng này. Đừng chỉ đọc, hãy mở trình soạn thảo code của bạn lên và thử nghiệm với những ví dụ trên. Thực hành chính là cách tốt nhất để biến kiến thức thành kỹ năng. Chúc bạn thành công trên con đường chinh phục lập trình!





Leave a Comment