Những lỗi thường gặp và cách tránh trong JavaScript

Javascript căn bản | by Học Javascript

JavaScript là một trong những ngôn ngữ lập trình phổ biến nhất, được sử dụng rộng rãi trong phát triển web. Tuy nhiên, do tính linh hoạt và cơ chế xử lý đặc thù, JavaScript cũng dễ khiến lập trình viên mắc phải nhiều lỗi phổ biến. Những lỗi này không chỉ làm chương trình hoạt động sai mà còn ảnh hưởng nghiêm trọng đến hiệu suất, khả năng bảo trì và bảo mật của ứng dụng.

Lỗi trong JavaScript có thể gây ra nhiều tác động tiêu cực, từ việc làm chậm tốc độ xử lý do thao tác DOM không tối ưu, đến việc khó bảo trì do mã nguồn lộn xộn, thiếu cấu trúc hợp lý. Quan trọng hơn, một số lỗi có thể dẫn đến lỗ hổng bảo mật, tạo cơ hội cho hacker tấn công thông qua các phương thức như XSS (Cross-Site Scripting) hoặc SQL Injection.

Việc nhận diện và tránh các lỗi thường gặp trong JavaScript không chỉ giúp cải thiện chất lượng mã nguồn mà còn đảm bảo ứng dụng hoạt động ổn định, an toàn và dễ dàng mở rộng trong tương lai. Trong bài viết này, chúng ta sẽ cùng tìm hiểu những lỗi phổ biến nhất trong JavaScript và cách phòng tránh chúng hiệu quả.

Lỗi liên quan đến biến và kiểu dữ liệu trong JavaScript

JavaScript là một ngôn ngữ linh hoạt, nhưng chính sự linh hoạt đó lại dẫn đến nhiều lỗi tiềm ẩn liên quan đến biến và kiểu dữ liệu. Dưới đây là một số lỗi phổ biến mà lập trình viên thường gặp và cách tránh chúng.

Sử dụng var thay vì let hoặc const

Vấn đề:

var có phạm vi (scope) ở cấp độ function (function scope) thay vì block (block scope), dẫn đến lỗi khi biến bị hoisting hoặc ghi đè không mong muốn.

Ví dụ sai:

function example() {
  if (true) {
    var x = 10;
  }
  console.log(x); // Không lỗi, x vẫn tồn tại ngoài block
}
example();

Trong đoạn code trên, biến x được khai báo bằng var, nên nó có phạm vi function scope và không bị giới hạn trong block if. Điều này có thể gây ra lỗi khó phát hiện khi làm việc với các vòng lặp hoặc logic phức tạp.

Cách tránh:

Luôn sử dụng let hoặc const thay vì var.

  • Dùng const cho giá trị không thay đổi.
  • Dùng let cho giá trị có thể thay đổi.

Ví dụ đúng:

function example() {
  if (true) {
    let x = 10;
  }
  console.log(x); // Lỗi: x is not defined (đúng mong đợi)
}
example();

Gán giá trị sai kiểu dữ liệu

JavaScript là ngôn ngữ weakly typed (kiểu dữ liệu yếu), cho phép chuyển đổi kiểu dữ liệu tự động. Điều này có thể gây lỗi nếu không kiểm tra cẩn thận.

Ví dụ sai:

let number = 10;
number = number + "5"; // Kết quả: "105" (chuỗi) thay vì 15 (số)

Trong ví dụ trên, số 10 bị ép kiểu thành chuỗi "105" khi cộng với "5", gây ra lỗi logic không mong muốn.

Cách tránh:

Sử dụng typeof để kiểm tra kiểu dữ liệu trước khi thao tác.

Ví dụ đúng:

let number = 10;
if (typeof number === "number") {
  number += 5; // Đảm bảo phép cộng được thực hiện trên số
}
console.log(number); // Kết quả: 15

Hoặc ép kiểu một cách rõ ràng:

let number = 10;
let stringNumber = "5";
let sum = number + Number(stringNumber); // Chuyển "5" thành số trước khi cộng
console.log(sum); // Kết quả: 15

So sánh lỏng lẻo (== thay vì ===)

Dấu == trong JavaScript thực hiện so sánh giá trị, nhưng có thể tự động ép kiểu dữ liệu, dẫn đến kết quả không mong muốn.

Ví dụ sai:

console.log(0 == ""); // true
console.log(false == "0"); // true
console.log(null == undefined); // true

Cách tránh:

Luôn sử dụng === để so sánh cả giá trị kiểu dữ liệu.

Ví dụ đúng:

console.log(0 === ""); // false
console.log(false === "0"); // false
console.log(null === undefined); // false

Nếu muốn so sánh mà vẫn giữ khả năng ép kiểu an toàn, bạn nên ép kiểu rõ ràng trước khi so sánh.

Ví dụ đúng:

let input = "5";
if (Number(input) === 5) {
  console.log("Input hợp lệ!");
}

Lỗi về điều kiện và vòng lặp trong JavaScript

Điều kiện (if, switch) và vòng lặp (for, while) là những thành phần quan trọng trong JavaScript. Tuy nhiên, nếu không cẩn thận, chúng có thể gây ra lỗi logic nghiêm trọng, ảnh hưởng đến hiệu suất và tính ổn định của ứng dụng. Dưới đây là một số lỗi phổ biến và cách tránh chúng.

Lỗi kiểm tra sai giá trị null, undefined, NaN

Vấn đề:

Trong JavaScript, một số giá trị như null, undefined, NaN, 0, "" (chuỗi rỗng), và false đều được coi là "falsy" (giá trị giả). Do đó, sử dụng if (value) có thể bỏ sót một số trường hợp không mong muốn.

Ví dụ sai:

function checkValue(value) {
  if (value) {
    console.log("Giá trị hợp lệ:", value);
  } else {
    console.log("Giá trị không hợp lệ!");
  }
}

checkValue(0); // "Giá trị không hợp lệ!" (mặc dù 0 có thể là giá trị hợp lệ)
checkValue(""); // "Giá trị không hợp lệ!" (mặc dù chuỗi rỗng có thể là hợp lệ)
checkValue(null); // "Giá trị không hợp lệ!" (đúng)
checkValue(undefined); // "Giá trị không hợp lệ!" (đúng)

Ở trên, 0"" bị coi là không hợp lệ do cách kiểm tra if (value), nhưng đôi khi chúng có thể là giá trị hợp lệ trong một số trường hợp.

Cách tránh:

Kiểm tra cụ thể từng trường hợp với null, undefinedNaN thay vì kiểm tra chung chung với if (value).

Ví dụ đúng:

function checkValue(value) {
  if (value !== null && value !== undefined) {
    console.log("Giá trị hợp lệ:", value);
  } else {
    console.log("Giá trị không hợp lệ!");
  }
}

checkValue(0); // "Giá trị hợp lệ: 0"
checkValue(""); // "Giá trị hợp lệ: "
checkValue(null); // "Giá trị không hợp lệ!"
checkValue(undefined); // "Giá trị không hợp lệ!"

Hoặc nếu cần kiểm tra NaN, bạn có thể dùng Number.isNaN():

function checkNumber(value) {
  if (value !== null && value !== undefined && !Number.isNaN(value)) {
    console.log("Số hợp lệ:", value);
  } else {
    console.log("Không phải số hợp lệ!");
  }
}

checkNumber(NaN); // "Không phải số hợp lệ!"
checkNumber(42); // "Số hợp lệ: 42"

Vòng lặp vô hạn (while hoặc for không có điều kiện dừng chính xác)

Vấn đề:

Nếu vòng lặp không có điều kiện dừng hợp lệ, nó sẽ chạy mãi mãi, gây treo trình duyệt hoặc crash chương trình.

Ví dụ sai:

let i = 0;
while (true) {
  console.log(i);
  i++;
} // Lỗi: Vòng lặp vô hạn, không có điều kiện dừng

Hoặc trường hợp điều kiện sai:

for (let i = 0; i > -1; i++) {
  console.log(i);
} // Lỗi: i luôn > -1 nên vòng lặp không bao giờ kết thúc

Cách tránh:

Luôn đặt điều kiện dừng chính xác trong vòng lặp.

Ví dụ đúng:

let i = 0;
while (i < 10) {
  console.log(i);
  i++; // Điều kiện dừng: khi i >= 10, vòng lặp kết thúc
}

Với for loop:

for (let i = 0; i < 10; i++) {
  console.log(i); // Điều kiện dừng: i < 10
}

Lưu ý:
Nếu bạn không chắc chắn về số lần lặp, hãy thêm một điều kiện dừng dự phòng:

let i = 0;
while (i < 10) {
  console.log(i);
  i++;
  if (i > 1000) break; // Thêm điều kiện tránh vòng lặp vô hạn
}

Quên sử dụng break trong switch-case

Vấn đề:

Nếu không có break, tất cả các case phía sau đều sẽ thực thi, gây lỗi logic.

Ví dụ sai:

let fruit = "apple";

switch (fruit) {
  case "apple":
    console.log("Đây là táo");
  case "banana":
    console.log("Đây là chuối");
  case "orange":
    console.log("Đây là cam");
}

Kết quả:

Đây là táo
Đây là chuối
Đây là cam

Tất cả các case sau "apple" đều được thực thi vì thiếu break.

Cách tránh:

Luôn thêm break vào mỗi case để dừng khi đã tìm thấy giá trị phù hợp.

Ví dụ đúng:

let fruit = "apple";

switch (fruit) {
  case "apple":
    console.log("Đây là táo");
    break;
  case "banana":
    console.log("Đây là chuối");
    break;
  case "orange":
    console.log("Đây là cam");
    break;
  default:
    console.log("Không biết loại quả này");
}

Kết quả đúng:

Đây là táo

Nếu bạn muốn cố ý bỏ qua break, hãy thêm ghi chú để tránh nhầm lẫn:

vlet fruit = "apple";

switch (fruit) {
  case "apple":
  case "banana": // Không có break => xử lý chung cho cả táo và chuối
    console.log("Đây là táo hoặc chuối");
    break;
  case "orange":
    console.log("Đây là cam");
    break;
  default:
    console.log("Không biết loại quả này");
}

Lỗi liên quan đến hàm và xử lý bất đồng bộ trong JavaScript

Hàm và cơ chế bất đồng bộ (async/await, Promise, callback) là một phần quan trọng của JavaScript. Tuy nhiên, nếu không cẩn thận, lập trình viên có thể gặp phải nhiều lỗi gây ảnh hưởng đến logic chương trình, làm ứng dụng hoạt động không như mong đợi.

Quên return trong hàm

Vấn đề:

Khi quên return, hàm sẽ không trả về giá trị mong muốn, dẫn đến lỗi logic hoặc undefined.

Ví dụ sai:

function add(a, b) {
  a + b; // Quên return
}

console.log(add(5, 10)); // undefined

Cách tránh:

Luôn kiểm tra giá trị trả về khi viết hàm, đảm bảo có return nếu cần.

Ví dụ đúng:

function add(a, b) {
  return a + b; // Trả về kết quả
}

console.log(add(5, 10)); // 15
v
Nếu dùng arrow function, chú ý khi viết một dòng mà không có {}:
const multiply = (a, b) => a * b; // Không cần return vì mặc định trả về

console.log(multiply(5, 10)); // 50

Sử dụng callback hell thay vì async/await

Vấn đề:

Khi lồng nhiều callback vào nhau, code trở nên khó đọc và khó bảo trì.

Ví dụ sai (callback hell):

getUser(1, function (user) {
  getOrders(user.id, function (orders) {
    getOrderDetails(orders[0], function (details) {
      console.log(details);
    });
  });
});

Cách tránh:

Sử dụng async/await hoặc Promise để xử lý bất đồng bộ rõ ràng hơn.

Ví dụ đúng (dùng async/await):

async function getUserData(userId) {
  const user = await getUser(userId);
  const orders = await getOrders(user.id);
  const details = await getOrderDetails(orders[0]);
  console.log(details);
}

Xử lý lỗi bất đồng bộ không đúng cách

Vấn đề:

Nếu không dùng try...catch hoặc .catch(), lỗi trong async/await có thể gây crash ứng dụng.

Ví dụ sai:

async function fetchData() {
  let response = await fetch("https://api.example.com/data");
  let data = await response.json();
  return data;
}

fetchData(); // Nếu có lỗi, chương trình sẽ bị crash

Cách tránh:

Luôn bọc async/await trong try...catch hoặc dùng .catch().

Ví dụ đúng:

async function fetchData() {
  try {
    let response = await fetch("https://api.example.com/data");
    let data = await response.json();
    return data;
  } catch (error) {
    console.error("Lỗi khi lấy dữ liệu:", error);
    return null;
  }
}

fetchData();
Hoặc với .catch():
fetch("https://api.example.com/data")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Lỗi:", error));

Lỗi liên quan đến DOM và sự kiện trong JavaScript

Các thao tác với DOM (document.getElementById, querySelector, addEventListener) rất phổ biến trong JavaScript. Nếu không kiểm tra cẩn thận, chúng có thể gây ra lỗi, ảnh hưởng đến hiệu suất hoặc làm ứng dụng hoạt động không đúng.

Truy cập phần tử chưa tồn tại

Vấn đề:

Nếu gọi document.getElementById() hoặc querySelector() trên một phần tử không tồn tại, nó sẽ trả về null. Nếu tiếp tục thao tác trên null, sẽ gặp lỗi TypeError.

Ví dụ sai:

let element = document.getElementById("nonexistent");
console.log(element.textContent); // Lỗi: Cannot read properties of null

Cách tránh:

Kiểm tra phần tử trước khi thao tác:

let element = document.getElementById("nonexistent");
if (element) {
  console.log(element.textContent);
} else {
  console.warn("Phần tử không tồn tại!");
}
Hoặc đảm bảo DOM đã load xong trước khi truy vấn phần tử:
document.addEventListener("DOMContentLoaded", () => {
  let element = document.getElementById("myElement");
  if (element) {
    console.log(element.textContent);
  }
});

Lạm dụng thao tác DOM trực tiếp

Vấn đề:

Việc thay đổi DOM quá nhiều lần có thể làm giảm hiệu suất.

Ví dụ sai:

let list = document.getElementById("list");
for (let i = 0; i < 1000; i++) {
  let item = document.createElement("li");
  item.textContent = `Item ${i}`;
  list.appendChild(item); // Thêm từng phần tử một → hiệu suất kém
}

Cách tránh:

Sử dụng DocumentFragment để giảm số lần thao tác DOM:

let list = document.getElementById("list");
let fragment = document.createDocumentFragment();

for (let i = 0; i < 1000; i++) {
  let item = document.createElement("li");
  item.textContent = `Item ${i}`;
  fragment.appendChild(item);
}

list.appendChild(fragment); // Chỉ cập nhật DOM một lần
Hoặc dùng innerHTML nếu có thể:
let list = document.getElementById("list");
let html = "";

for (let i = 0; i < 1000; i++) {
  html += `<li>Item ${i}</li>`;
}

list.innerHTML = html; // Chỉ cập nhật DOM một lần

Gán sự kiện trong vòng lặp mà không sử dụng closure

Vấn đề:

Khi dùng var trong vòng lặp để gán sự kiện, tất cả sự kiện sẽ trỏ đến giá trị cuối cùng của var.

Ví dụ sai:

for (var i = 0; i < 3; i++) {
  document.getElementById(`btn${i}`).addEventListener("click", function () {
    console.log(`Button ${i} clicked`);
  });
}

Lỗi:var có phạm vi function scope, giá trị i sau vòng lặp là 3, nên khi bấm vào bất kỳ nút nào, sẽ luôn in "Button 3 clicked".

Cách tránh:

Dùng let để đảm bảo mỗi lần lặp có một i riêng biệt:

for (let i = 0; i < 3; i++) {
  document.getElementById(`btn${i}`).addEventListener("click", function () {
    console.log(`Button ${i} clicked`);
  });
}

Hoặc dùng closure với var:

for (var i = 0; i < 3; i++) {
  (function (index) {
    document.getElementById(`btn${index}`).addEventListener("click", function () {
      console.log(`Button ${index} clicked`);
    });
  })(i);
}

Lỗi về bảo mật trong JavaScript

Bảo mật trong JavaScript rất quan trọng vì nó giúp bảo vệ ứng dụng khỏi các cuộc tấn công như XSS (Cross-Site Scripting), SQL Injection, và Code Injection. Nếu không xử lý đúng cách, dữ liệu người dùng có thể bị đánh cắp hoặc ứng dụng có thể bị tấn công.

Không xử lý dữ liệu đầu vào (Input Validation)

Vấn đề:

Nếu không kiểm tra dữ liệu đầu vào, hacker có thể chèn mã độc vào trang web, dẫn đến XSS (Cross-Site Scripting) hoặc SQL Injection.

Ví dụ sai (XSS):

document.getElementById("output").innerHTML = "<p>" + userInput + "</p>";

Nếu userInput chứa đoạn mã như sau:

<script>alert("Hacked!")</script>

Thì trình duyệt sẽ thực thi mã <script>, gây rủi ro bảo mật.

Cách tránh:

Luôn kiểm tra và mã hóa dữ liệu đầu vào trước khi hiển thị lên trang web.

Ví dụ đúng:

function sanitizeHTML(str) {
  return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

document.getElementById("output").innerHTML = "<p>" + sanitizeHTML(userInput) + "</p>";

Đối với API hoặc dữ liệu gửi lên server, luôn kiểm tra dữ liệu đầu vào:

if (!userEmail.match(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)) {
  throw new Error("Email không hợp lệ!");
}

Lưu trữ dữ liệu nhạy cảm trên client-side (localStorage, sessionStorage)

Vấn đề:

Dữ liệu lưu trên localStorage hoặc sessionStorage có thể bị hacker truy cập thông qua XSS.

Ví dụ sai:

localStorage.setItem("userToken", "abcdef123456");

Nếu hacker chèn mã XSS vào trang web, họ có thể lấy token từ localStorage:

console.log(localStorage.getItem("userToken"));

Cách tránh:

  • Không lưu trữ dữ liệu nhạy cảm (mật khẩu, token) trên client-side.
  • Sử dụng HTTP-only Cookies thay vì localStorage.

Ví dụ đúng (sử dụng HTTP-only Cookie):

document.cookie = "userToken=abcdef123456; Secure; HttpOnly";

Sử dụng eval() hoặc setTimeout() với chuỗi lệnh

Vấn đề:

eval() cho phép thực thi chuỗi mã JavaScript, tạo lỗ hổng Code Injection.

Ví dụ sai:

let userInput = "console.log('Hacked!')";
eval(userInput); // Hacker có thể chèn mã độc vào đây

Tương tự, sử dụng setTimeout() với chuỗi lệnh cũng nguy hiểm:

setTimeout("console.log('Hacked!')", 1000);

Cách tránh:

  • Không sử dụng eval(), thay vào đó dùng hàm an toàn hơn.
  • Sử dụng JSON.parse() thay vì eval() khi xử lý dữ liệu JSON.

Ví dụ đúng:

try {
  let jsonData = JSON.parse(userInput);
} catch (error) {
  console.error("Lỗi parse JSON:", error);
}

Kết bài

JavaScript là một ngôn ngữ mạnh mẽ, nhưng nếu không cẩn thận, các lỗi phổ biến có thể ảnh hưởng nghiêm trọng đến hiệu suất, bảo mật và khả năng bảo trì của ứng dụng. Việc hiểu rõ và tránh những lỗi như sai phạm trong khai báo biến, điều kiện, vòng lặp, xử lý bất đồng bộ, thao tác DOM, và đặc biệt là các lỗi bảo mật sẽ giúp code trở nên tối ưu và an toàn hơn.

Bên cạnh đó, sử dụng các công cụ như ESLint, Prettier, và Chrome DevTools giúp phát hiện lỗi sớm, cải thiện chất lượng code và tối ưu quy trình phát triển.

Bằng cách áp dụng các kỹ thuật và công cụ này, bạn có thể viết JavaScript sạch hơn, hiệu quả hơn và bảo mật hơn, giúp ứng dụng hoạt động mượt mà và bền vững theo thời gian.

Bài viết liên quan