JavaScript – thứ "phép màu" biến web từ những trang tĩnh thành các trải nghiệm sống động, đáp ứng cực kỳ nhanh ngay trên trình duyệt. Khi lấn chân vào ngôn ngữ này, chắc hẳn bạn đã từng trải nghiệm cảm giác vui thích khi viết những dòng đầu tiên làm cho web đổi màu, popup hiện ra… Nhưng sớm thôi, những bug kỳ lạ như "kiểu dữ liệu biến mất", "kết quả ở đâu ra thế này?" bắt đầu xuất hiện. Điều thú vị (lẫn khéo gây nản lòng) của JavaScript là ở chỗ nó vô cùng dễ tiếp cận nhưng cũng cực kỳ… dễ mắc những lỗi ngớ ngẩn – đặc biệt nếu bạn là người mới.
Cùng khám phá hàng loạt lỗi JavaScript vui nhộn (và đôi khi bất ngờ) mà ai cũng từng hoặc sẽ từng gặp phải. Và quan trọng hơn, bạn sẽ biết cách tránh để mã của mình chạy trơn tru, chuyên nghiệp hơn mỗi ngày!
var, let, const)
Nếu ở các ngôn ngữ khác, mọi biến cần được khai báo rõ ràng thì JavaScript lại tỏ ra hơi "hiền hòa" – thiếu từ khóa khai báo vẫn không báo lỗi ngay. Tuy nhiên, đây lại là cái bẫy cực kỳ tai hại.
Ví dụ sai lầm thường gặp:
x = 5;
function foo() {
y = 10;
}
foo();
console.log(x); // 5
console.log(y); // 10
let, const, var), biến sẽ trở thành global mặc định (tức "rông khắp toàn bộ chương trình")."use strict"), lỗi này sẽ bị chặn lại bằng exception – do đó, hãy tập thói quen sử dụng strict mode sớm để tránh quên khai báo biến.Lời khuyên:
let hoặc const để khai báo biến.const cho giá trị không đổi, let khi bạn định cập nhật lại giá trị.var do phạm vi hoạt động (scope) không rõ ràng dễ sinh bug!
Không chỉ mỗi chuyện khai báo, mà phạm vi (scope) của biến trong JavaScript cũng rất hay… đánh lừa người mới.
So sánh: var vs. let/const:
function testScope() {
if (true) {
var a = 10;
let b = 20;
}
console.log(a); // In ra 10
console.log(b); // Lỗi ReferenceError!
}
testScope();
var: có phạm vi hàm (function scope) → dùng trong khối if vẫn nhìn thấy ở ngoài khối đó nếu cùng hàm.let, const: có phạm vi block ({ }) → chỉ sống trong từng block, cực kỳ khó gây bug tràn giá trị.Lỗi điển hình:
Cố dùng biến bên ngoài phạm vi cho phép:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
} // Kết quả: 3, 3, 3
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log(j);
}, 100);
} // Kết quả: 0, 1, 2
Điểm mấu chốt:
Sử dụng let/const cho phạm vi hẹp, tránh sử dụng var, đặc biệt trong các vòng lặp hoặc block để không gặp những bug lạ khó tìm.
;)
JavaScript hỗ trợ tự động chèn dấu chấm phẩy (ASi – Automatic Semicolon Insertion), khiến không ít bạn mới bị "thả thính" rơi vào lỗi ngớ ngẩn:
Trường hợp thường gặp:
function foo() {
return
{
message: 'Xin chào!'
};
}
console.log(foo()); // ???
Kết quả: In ra undefined! Vì dấu chấm phẩy được tự thêm sau return, đoạn sau bị coi như khối lệnh rỗng riêng, không trả về gì cả.
Cách tránh:
return.return {
message: 'Xin chào!'
};
Mẹo: Dù JavaScript "tha lỗi" việc thiếu ;, nhưng coding style nhất quán, thêm chấm phẩy ở cuối mỗi lệnh vẫn luôn là lựa chọn an toàn.
== và ===
Đây chính là "đặc sản" gây bối rối nhất với người mới học JavaScript.
==: So sánh không nghiêm ngặt (loose equality), tự động ép kiểu dữ liệu trước khi so sánh (type coercion).===: So sánh nghiêm ngặt (strict equality), vừa so sánh giá trị, vừa so cả kiểu.So sánh thực tế:
console.log(1 == '1'); // true
console.log(1 === '1'); // false
console.log(false == 0); // true
console.log(false === 0); // false
Kịch bản dễ mắc lỗi:
let a = 0;
if (a == false) {
alert('a được coi là false');
}
Bạn có thể không ngờ rằng rất nhiều trường hợp so sánh bằng == sẽ cho ra kết quả "khó đỡ", đặc biệt trong biểu mẫu, validation đầu vào, hoặc xét token người dùng…
Lời khuyên:
=== và !== để tránh những tình huống không rõ ràng về kiểu dữ liệu.== khi THỰC SỰ hiểu rõ việc ép kiểu sẽ diễn ra thế nào.Bạn có thể dùng thêm Equality Table của JavaScript để thử nghiệm các kết quả thú vị!
JavaScript có đến hai "giá trị rỗng": undefined và null. Nhiều người mới (và cả đã code một thời gian) té ngửa rằng chúng không hoàn toàn giống nhau.
undefined: Biến tồn tại, nhưng CHƯA gán giá trị gì.null: Biến được gán một giá trị "rỗng có chủ đích".Ví dụ so sánh:
let a;
let b = null;
console.log(a); // undefined
console.log(b); // null
console.log(a == b); // true (==)
console.log(a === b); // false (===)
Dễ mắc phải:
!x có thể gây hiểu nhầm khi gặp 0, ''.Giải pháp:
typeof x === 'undefined' hoặc x === undefined.null khi muốn thể hiện biến "không có giá trị" một cách rõ ràng, riêng biệt.
Làm việc bất đồng bộ là "xương sống" của JavaScript (AJAX, file, database…). Người học mới thường hoặc lồng callback bừa bãi, hoặc xử lý bất đồng bộ như code tuần tự.
Minh họa:
console.log("Trước");
setTimeout(function() {
console.log("Trong timeout");
}, 1000);
console.log("Sau");
Kết quả trên console:
Trước
Sau
Trong timeout
Bạn dễ tưởng rằng dòng in "Trong timeout" sẽ chạy ngay sau "Trước", nhưng thật ra lại chạy cuối cùng! (vì setTimeout là bất đồng bộ)
Callback hell
doAsync1(function(result1) {
doAsync2(result1, function(result2) {
doAsync3(result2, function(result3) {
// ...
});
});
});
Giải pháp:
async/await cho code dễ đọc hơn:async function run() {
const r1 = await doAsync1();
const r2 = await doAsync2(r1);
const r3 = await doAsync3(r2);
}
run();
Mẹo thêm:
Promise.all() để xử lý song song, tiết kiệm thời gian.
JavaScript sẽ báo ReferenceError nếu cố gắng truy cập biến/hàm chưa khởi tạo.
Nhưng cạm bẫy nằm ở chỗ "hoisting" (cơ chế "kéo lên đầu"):
console.log(a); // undefined
var a = 10;
foo(); // OK
function foo() { console.log('Hello!'); }
bar(); // ReferenceError
let bar = function() { console.log('Hi!'); };
var, và cả định nghĩa hàm kiểu function foo() {} sẽ được compiler đưa lên "đầu khối"let, const, hoặc biểu thức hàm (let x = function(){}) thì KHÔNG – phải khai báo xong mới dùng được.Cách tránh:
JavaScript "ưu ái" bạn bằng việc tự ép kiểu "giúp đỡ". Tuy vậy quyền năng này dễ khiến code phát sinh những lỗi cực kỳ buồn cười.
Tình cảnh hài hước:
let a = '5';
let b = 2;
console.log(a + b); // 52 (!)
console.log(a * b); // 10
console.log(a - b); // 3
console.log(a + true); // 5true
console.log(a - true); // 4
Mẹo và khuyến nghị:
+ để nối string và số (hãy convert rõ ràng):
String(), parseInt(), Number()… để chuyển đổi.Number(a) để chuyển string sang số, tránh lẫn lộn kiểu lúc render hoặc làm phép toán.
Không quá lời khi nói: "this" là cạm bẫy kinh điển của lập trình viên mới (và cả cũ!).
Điểm mấu chốt:
Minh hoạ mắc lỗi:
const obj = {
val: 42,
print: function() {
console.log(this.val);
}
};
obj.print(); // 42
const f = obj.print;
f(); // undefined (vì this không còn tham chiếu obj)
Các trường hợp thường thấy:
method) làm callback, mất "liên kết" this gốc.()=>{}) không nhận "this" mới (lấy this của ngoài ra):const obj2 = {
val: 100,
print: () => { console.log(this.val); } // this ở đây là window hoặc undefined!
};
obj2.print(); // undefined
Giải pháp:
bind() để giữ nguyên tham chiếu "this".btn.onclick = obj.print.bind(obj);
Hoặc khi dùng class:
class Demo {
constructor() {
this.value = 888;
}
show() {
console.log(this.value);
}
}
const d = new Demo();
setTimeout(() => d.show(), 1000);
JavaScript "rộng lượng" tới mức sau sẽ ngầm xem những giá trị sau là Falsy (bị coi như false):
false, 0, '', null, undefined, NaNBất cứ thứ gì KHÁC sẽ được coi là Truthy.
Hiện tượng dễ mắc:
if (userInput) { ... }, nhưng đầu vào lại có thể là 0 hoặc chuỗi rỗng → bị loại bỏ oan uổng.Góc nhìn chuyên sâu:
Minh họa:
let age = 0;
if (age) {
console.log("Đã nhập tuổi");
} else {
console.log("Chưa nhập tuổi hoặc bỏ trống");
}
// Kết quả: "Chưa nhập tuổi hoặc bỏ trống" (dù age = 0)
Mẹo:
if (typeof age !== 'undefined' && age !== null) hoặc if (age !== '')… để logic hợp lý và tránh "bỏ sót" giá trị hợp lệ.
Người mới thường nhầm tưởng string, number là object, và tìm cách… gán thuộc tính lên chúng.
Ví dụ sai lầm:
let s = 'Hello';
s.customProp = 42;
console.log(s.customProp); // undefined
Cách tránh:
Khởi tạo và đường dẫn chỉ mục mảng thường bị lẫn lộn:
Ví dụ:
let arr = [1, 2, 3];
console.log(arr[5]); // undefined
arr[10] = 99;
console.log(arr.length); // 11
.length có thể đưa đến kết quả bất ngờ – số phần tử sẽ bằng số chỉ mục cao nhất cộng 1.Một lỗi phổ biến khác:
let arr2 = [];
arr2[0] = 'a';
arr2[4] = 'e';
console.log(arr2); // [ 'a', <empty item>, <empty item>, <empty item>, 'e' ]
Bí quyết dùng mảng hiệu quả:
.push() để thêm cuối mảng.Array.prototype.filter hoặc every.
Ai cũng từng copy biến trong JavaScript, nhưng "copy" không đơn giản như bạn nghĩ nếu là OBJECT hoặc ARRAY!
Ví dụ minh họa:
let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2[0] = 100;
console.log(arr1); // [100, 2, 3] (!)
let arr2 = [...arr1]let copy = {...obj}JSON.parse(JSON.stringify(obj)), thư viện như lodash.cloneDeep…)Góc nhìn chuyên sâu:
slice(), concat() cũng có thể tạo copy nông.Mẹo: Chú ý loại dữ liệu trước khi copy/phân phối biến đi khắp hàm, class, tránh bug dây chuyền sau đổi giá trị.
Chỉ cần thiếu một dấu hoặc chỉnh điều kiện một chút, vòng lặp sẽ chạy "miệt mài không biết điểm dừng":
Ví dụ dễ mắc:
let i = 0;
while (i !== 10) {
i += 2;
}
// Vòng lặp VÔ TẬN, vì i nhảy 0-2-4-6-8-10, nhưng khi i = 10 thì dừng, thực tế UNIXONLY loop nếu quên break hoặc có lỗi khác
Hoặc:
let arr = [1, 2, 3, 4, 5];
for (let i = 0; i <= arr.length; i++) {
console.log(arr[i]); // Khi i = arr.length, truy cập phần tử không tồn tại => undefined
}
Lời khuyên hành động:
for (let i = 0; i < arr.length; i++) là pattern an toàn nhất.= thay cho == hoặc ===
Nhầm lẫn giữa phép gán (=) và phép so sánh (==,===) là một sai lầm cơ bản nhưng lại rất phổ biến!
Ví dụ classic:
let check = false;
if (check = true) {
// Sẽ luôn chạy vào đây, vì check được GÁN true!
console.log('Check là true!');
}
check được GÁN true, không phải đang so sánh!Giải pháp:
if (true == check) {...} (nếu lỡ tay gõ = sẽ báo lỗi ngay).
Cứ tưởng coder giỏi sẽ không cần debug? Nhầm to! Lỗi nhỏ nhất đôi khi "ẩn nấp" chỉ lòi mặt ra khi dùng đến console.log, debugger,...
Chiến lược debug hiệu quả:
console.log('current value:', x);
console.table, console.dir để xem dữ liệu phức tạp.
Lập trình JavaScript rất thú vị nhờ sự linh hoạt và đa chiều, nhưng cũng đặt ra thử thách khi bạn mới bắt đầu bước vào. Hiểu và đề phòng những "lỗi ngớ ngẩn" kể trên sẽ giúp bạn tiến bộ rất nhanh – vừa tránh được bụi rậm của bug, vừa xây dựng thói quen lập trình rõ ràng, chuyên nghiệp hơn. Hãy cứ tự tin thử nghiệm, nhưng đừng quên tích lũy "vốn quý" từ những lần vấp ngã ấy. JavaScript không khó – chỉ cần ta khéo để tâm!