Những lỗi JavaScript ngớ ngẩn dễ mắc khi mới học

Những lỗi JavaScript ngớ ngẩn dễ mắc khi mới học

28 phút đọc Khám phá những lỗi JavaScript đơn giản mà người mới bắt đầu dễ vấp phải.
(0 Đánh giá)
Bài viết giúp học viên nhận diện các lỗi JavaScript cơ bản thường gặp, phân tích nguyên nhân và đưa ra mẹo tránh tái phạm khi lập trình web.
Những lỗi JavaScript ngớ ngẩn dễ mắc khi mới học

Những lỗi JavaScript ngớ ngẩn dễ mắc khi mới học

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!


Quên từ khóa khai báo biến (var, let, const)

javascript, variable, 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
  • Nếu không khai báo (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").
  • Hậu quả: dễ đạp vào chân các biến trùng tên ở nơi khác, gây ra bug ẩn khó lường, mở ra sẵn lỗ hổng bảo mật.
  • Ở chế độ "strict mode" ("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:

  • Luôn sử dụng let hoặc const để khai báo biến.
  • Dùng const cho giá trị không đổi, let khi bạn định cập nhật lại giá trị.
  • Tránh var do phạm vi hoạt động (scope) không rõ ràng dễ sinh bug!

Nhầm lẫn về phạm vi biến (Scope)

javascript scope, variable confusion

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.


Quên hoặc dư dấu chấm phẩy (;)

semicolon, javascript syntax

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:

  • Luôn ghi giá trị trả về cùng dòng với return.
  • Bỏ thói quen viết xuống dòng mà không rõ ý nghĩa về cú pháp của JavaScript.
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.


So sánh giá trị bằng =====

javascript, comparison, equality

Đâ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:

  • Luôn sử dụng ===!== để tránh những tình huống không rõ ràng về kiểu dữ liệu.
  • Chỉ dùng == 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ị!


"Undefined" và "Null" không giống nhau!

javascript, null, undefined

JavaScript có đến hai "giá trị rỗng": undefinednull. 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:

  • So sánh lẫn lộn hai giá trị này, khiến logic kiểm tra trạng thái dữ liệu bị lệch, validation đầu vào gặp lỗi.
  • Test sự tồn tại của biến bằng cách so sánh bằng !x có thể gây hiểu nhầm khi gặp 0, ''.

Giải pháp:

  • Kiểm tra sự undefined bằng typeof x === 'undefined' hoặc x === undefined.
  • Chỉ dùng 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.

Sử dụng sai hoặc thiếu hiểu biết về "callback" và hàm bất đồng bộ

javascript callback, async await

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

  • Khi chồng quá nhiều callback lồng nhau, code trở nên cực kỳ rối rắm:
doAsync1(function(result1) {
  doAsync2(result1, function(result2) {
    doAsync3(result2, function(result3) {
      // ...
    });
  });
});

Giải pháp:

  • Học và sử dụng Promise, 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:

  • Khi làm việc với nhiều tác vụ bất đồng bộ cùng lúc, dùng Promise.all() để xử lý song song, tiết kiệm thời gian.

Lỗi reference (chưa khai báo mà đã dùng biến/hàm)

javascript error, reference error

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"
  • Còn vớ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:

  • Khai báo và khởi tạo mọi biến/hàm trước khi sử dụng.
  • Tránh lạm dụng "hoisting" để code rõ ràng, dễ debug hơn.

Lỗi về kiểu dữ liệu (TypeError), ép kiểu không mong muốn

javascript typeerror, string number

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
  • Với phép cộng chuỗi, số => chuỗi sẽ ưu tiên, dồn sang dạng string.
  • Phép nhân, trừ,… sẽ tự chuyển sang số nếu có thể, cực kỳ "thú vị" (hiểu hoặc cẩn thận là tùy bạn).

Mẹo và khuyến nghị:

  • Đảm bảo kiểu dữ liệu đúng việc trước khi sử dụng chúng với toán tử.
  • Hạn chế dùng toán tử + để nối string và số (hãy convert rõ ràng):
    • Dù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.

Hiểu sai về "this" – cơn ác mộng của số đông!

javascript this, object reference

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:

  • "this" trong JavaScript là thứ cực kỳ "biến ảo", phụ thuộc vào NGỮ CẢNH gọi hàm, chứ không phải ở nơi định nghĩa hàm.

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:

  • Khi truyền phương thức (method) làm callback, mất "liên kết" this gốc.
  • Arrow function (()=>{}) 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:

  • Sử dụng bind() để giữ nguyên tham chiếu "this".
  • Với callback, hoặc sự kiện DOM, có thể dùng arrow function để giữ (hoặc "mất") đúng this bạn muốn, thí dụ:
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);

Biến "rỗng" bị kiểm tra sai (Falsy vs Truthy)

javascript, falsy, truthy

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, NaN

Bất cứ thứ gì KHÁC sẽ được coi là Truthy.

Hiện tượng dễ mắc:

  • Kiểm tra biến "đã tồn tại giá trị" bằng kiểu như 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.
  • Kiểm thử kê khai bằng checkbox, radio ở form nhận dữ liệu bị xét là false dù thực tế hoàn toàn hợp lệ.

Góc nhìn chuyên sâu:

  • Trước mỗi so sánh, bạn nên kiểm tra kiểu dữ liệu nếu logic nhạy cảm với giá trị thực sự của biến (không chỉ là "trống/hay có giá trị").

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:

  • Sử dụng so sánh rõ ràng: 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ệ.

Gán thuộc tính mới lên kiểu dữ liệu nguyên thủy

javascript, primitive value property

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ác giá trị như string, number, boolean là nguyên thủy (primitive values), bạn không thể gán bất kỳ thuộc tính nào lên chúng.
  • Khi gán kiểu trên, JavaScript sẽ tạo bản copy tạm "object wrapper" rồi loại bỏ ngay, nên thuộc tính vừa gán sẽ mặc kệ không lưu giữ.

Cách tránh:

  • Sử dụng object wrapper nếu thật sự cần lưu trữ thêm thuộc tính, nhưng hầu hết các trường hợp – thiết kế đúng cách sẽ giúp tránh nhu cầu này.

Lỗi khi khai báo và sử dụng mảng (Array pitfall)

javascript array bug, undefined index

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
  • Khi bạn gán giá trị ở index "xa", các phần tử xen giữa được sinh ra là "undefined".
  • Dùng .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:

  • Thêm phần tử mảng bằng phép gán:
let arr2 = [];
arr2[0] = 'a';
arr2[4] = 'e';
console.log(arr2); // [ 'a', <empty item>, <empty item>, <empty item>, 'e' ]
  • Các vị trí khuyết sẽ là "empty", làm cho việc lặp, tổng hợp, render gặp bất ngờ.

Bí quyết dùng mảng hiệu quả:

  • Sử dụng .push() để thêm cuối mảng.
  • Khi cần "map" mọi phần tử, kiểm tra kỹ trường hợp phân tử bị "khuyết" bằng cách kết hợp Array.prototype.filter hoặc every.

Deep vs. shallow copy – "sao chép nông sâu"

javascript copy array, deep copy

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] (!)
  • Gán mảng hoặc object chỉ tạo tham chiếu (shallow copy), chỉnh ở bản mới thì bản cũ đổi theo.
  • Muốn tạo BẢN SAO RIÊNG không bị liên quan:
    • Đối với mảng ngắn: let arr2 = [...arr1]
    • Với object: let copy = {...obj}
    • Với mảng đa chiều/object lồng nhau: phải dùng deep copy (dùng JSON.parse(JSON.stringify(obj)), thư viện như lodash.cloneDeep…)

Góc nhìn chuyên sâu:

  • Các phương thức Array như slice(), concat() cũng có thể tạo copy nông.
  • Với các object lồng, chỉ sao chép "nông" thì field bên trong vẫn còn tham chiếu chung.

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ị.


Lỗi logic: lặp không thoát do điều kiện vòng lặp sai

javascript infinite loop, bug code

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
}
  • Mọi vòng lặp nên kiểm tra kỹ điều kiện dừng, đừng chạy vượt khỏi ranh giới (out of bounds), đặc biệt với mảng có chỉnh sửa kích thước liên tục.

Lời khuyên hành động:

  • for (let i = 0; i < arr.length; i++) là pattern an toàn nhất.
  • Khi trong vòng lặp có điều kiện return/continue/break, nên soát lại logic kỹ càng!

Dùng nhầm = thay cho == hoặc ===

javascript, assignment bug, comparison

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!');
}
  • Kết quả: check được GÁN true, không phải đang so sánh!
  • Lỗi kiểu này rất "dai dẳng" khi debug, đặc biệt với cấu trúc điều kiện phức tạp.

Giải pháp:

  • Đổi thứ tự, đặt hằng số hoặc kỳ vọng kiểm tra phía trước giúp phát hiện lỗi sớm: if (true == check) {...} (nếu lỡ tay gõ = sẽ báo lỗi ngay).
  • Trình soạn thảo hiện đại hỗ trợ linter, autocompletion cực tốt – mở cảnh báo về "==" hay phép gán phi lý!

Lười hoặc bỏ qua debug, log thông tin khi cần

javascript, debug, console log

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ả:

  • Đừng ngại thêm nhiều log để "nắm bắt tình hình":
console.log('current value:', x);
  • Sử dụng console.table, console.dir để xem dữ liệu phức tạp.
  • Sử dụng các công cụ debugger trình duyệt (F12 → Sources→ Breakpoint) để "bẻ gãy" dòng thực thi, soi giá trị hiện tại.
  • Test tách rời từng phần code… Bạn sẽ phát hiện ra lỗi ngớ ngẩn mình vô tình tạo ra từ những đoạn tưởng là đúng nhất.

Một số "mẹo sống còn" giúp tránh lỗi JavaScript ngớ ngẩn

javascript tips, pro coding
  • Có thể học lỏm suffix: Dùng các trình soạn thảo thông minh (VSCode, WebStorm…) kết hợp ESLint sẽ sớm báo lỗi cú pháp, gợi ý dùng đúng kiểu khai báo, phạm vi hợp lý hơn.
  • Tích trữ "code snippets" riêng cho mình: cứ dùng lại "pattern" an toàn cho vòng lặp, callback, khai báo biến,...
  • Đọc thật kỹ log báo lỗi, đừng chỉ nhìn tiêu đề!
  • Tự mình thiết kế các bộ test đầu vào ra (unit test): cực kỳ hiệu quả giúp bóc tách từng chỗ code ngớ ngẩn khó tìm ra khi chạy những case bình thường.
  • Kiên nhẫn và kiên trì: Mỗi khi gặp bug, thay vì "đốt cháy" nó — hãy thử in từng bước logic và học từ chính lỗi đó.

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!

Đánh giá bài viết

Thêm bình luận & đánh giá

Đánh giá của người dùng

Dựa trên 0 đánh giá
5 Star
0
4 Star
0
3 Star
0
2 Star
0
1 Star
0
Thêm bình luận & đánh giá
Chúng tôi sẽ không bao giờ chia sẻ email của bạn với bất kỳ ai khác.