Explore all courses in this category.
Explore all courses in this category.
Nguyễn Đình Cường
0 comment
09 Nov, 2026
Một lớp lỗi rất khó tìm là khi dữ liệu bị thay đổi âm thầm ở một nơi nào đó trong chương trình, và nơi khác dùng nó nhận kết quả bất ngờ. Hai khái niệm giúp phòng tránh: tính bất biến (immutability) và hạn chế side effect (tác dụng phụ). Bài này giải thích và đưa ra ví dụ dễ áp dụng.
Một hàm có side effect khi nó làm gì đó ngoài việc trả về giá trị: thay đổi biến bên ngoài, sửa đối số truyền vào, ghi file, gọi mạng, in ra màn hình. Không phải side effect nào cũng xấu, chương trình hữu ích phải có (ghi database, hiển thị). Vấn đề là những side effect ẩn, bất ngờ.
// Có side effect ẩn: sửa trực tiếp mảng truyền vàofunction addTax(prices) {for (let i = 0; i < prices.length; i++) {prices[i] = prices[i] * 1.1;}return prices;}const original = [100, 200];const withTax = addTax(original);console.log(original); // [110, 220] - mảng gốc bị đổi!Ai đó dùng original ở chỗ khác sẽ nhận giá trị đã bị đổi mà không hiểu vì sao.
Hàm thuần có hai đặc điểm: với cùng đầu vào luôn cho cùng đầu ra, và không gây side effect. Phiên bản thuần của ví dụ trên:
function addTax(prices) {return prices.map(price => price * 1.1);}const original = [100, 200];const withTax = addTax(original);console.log(original); // [100, 200] - nguyên vẹnconsole.log(withTax); // khoảng [110, 220]Hàm thuần dễ test (đưa vào, kiểm tra kết quả), dễ hiểu (chỉ cần xem tham số), dễ tái sử dụng và an toàn khi chạy song song.
Thay vì sửa đối tượng hay mảng tại chỗ, bạn tạo bản sao với thay đổi. Trong JavaScript:
const user = { name: 'An', age: 20, address: { city: 'Hà Nội' } };// Cập nhật thuộc tính bằng spread (bản sao nông)const olderUser = { ...user, age: 21 };// Thêm, xóa phần tử mảng không đổi mảng gốcconst list = [1, 2, 3];const added = [...list, 4];const removed = list.filter(n => n !== 2);const doubled = list.map(n => n * 2);Lưu ý spread chỉ sao chép nông: user.address vẫn là cùng một đối tượng được chia sẻ giữa hai bản. Với cấu trúc lồng nhau, bạn phải sao chép cả tầng bên trong cần đổi (hoặc dùng structuredClone cho bản sao sâu, hay thư viện chuyên dụng).
const moved = {...user,address: { ...user.address, city: 'Đà Nẵng' },};const chỉ ngăn gán lại biến, không ngăn sửa nội dung. Object.freeze() khóa nông một đối tượng. Phương thức map, filter, slice, concat trả bản mới, còn push, splice, sort, reverse sửa tại chỗ (chú ý!).readonly và ReadonlyArray giúp trình biên dịch bắt lỗi sửa nhầm.tuple, frozenset, @dataclass(frozen=True) là bất biến. Cẩn thận với đối số mặc định là list hoặc dict (dùng chung giữa các lần gọi).final, record (từ phiên bản mới), danh sách không thể sửa như List.of.val, let, mặc định bất biến.from dataclasses import dataclass@dataclass(frozen=True)class Point:x: inty: intp = Point(1, 2)# p.x = 5 # gây lỗi FrozenInstanceError# Sai: danh sách mặc định được tạo một lần và dùng chungdef add_item(item, items=[]):items.append(item)return items# Đúngdef add_item(item, items=None):if items is None:items = []return items + [item]Không cần cực đoan. Sao chép dữ liệu lớn liên tục có thể tốn bộ nhớ và thời gian, trong vòng lặp hiệu năng cao, sửa tại chỗ cục bộ (biến chỉ tồn tại trong hàm) là chấp nhận được. Nguyên tắc thực dụng: đừng sửa dữ liệu mà bạn không sở hữu (đối số, trạng thái chia sẻ); cô lập các side effect ở rìa chương trình (đọc ghi file, mạng, database) và giữ phần logic ở giữa là hàm thuần.
Ưu tiên hàm thuần, tạo bản mới thay vì sửa tại chỗ và để side effect nằm ở ranh giới hệ thống. Chỉ vài thói quen này đã loại bỏ được cả một lớp bug khó chịu. Hãy rà lại code của bạn xem có hàm nào đang âm thầm sửa đối số hay không.
Nếu bạn muốn học JavaScript, Python hay tư duy lập trình nền tảng với bài tập thực hành, hãy xem qua codebem.com.
Nguyễn Đình Cường
0 comment