9 lỗi JavaScript phổ biến các lập trình viên thường gặp
25/02/2022 04:37
JavaScript là một ngôn ngữ kịch bản được sử dụng để thêm chức năng và tính tương tác vào các trang web. Đối với các lập trình viên đã thông thạo một ngôn ngữ lập trình khác trước đó thì JavaScript khá dễ hiểu. Với một vài hướng dẫn, bạn sẽ có thể bắt đầu với nó ngay lập tức. Tuy nhiên, có một vài lỗi phổ biến mà nhiều lập trình viên mới bắt đầu với JavaScript thường mắc phải. Trong bài viết này, chúng ta sẽ cùng tìm hiểu và giải quyết 09 lỗi phổ biến khi lập trình JavaScript, để giúp bạn trở thành nhà phát triển JS thông thạo hơn.
- 1. Nhầm lẫn giữa toán tử gán (=) và toán tử bình đẳng ( ==, ===)
- 2. Mong chờ các callback đều đồng bộ
- 3. Tham chiếu sai đến this
- 4. Bỏ qua khả năng thay đổi của đối tượng
- 5. Lưu mảng và đối tượng vào bộ nhớ của trình duyệt
- 6. Không sử dụng các giá trị mặc định
- 7. Đặt tên biến không đúng
- 8. Kiểm tra các giá trị boolean
- 8. Lẫn lộn giữa Addition và Concatenation
- Kết luận
1. Nhầm lẫn giữa toán tử gán (=) và toán tử bình đẳng ( ==, ===)
Như tên gọi của nó, toán tử gán (assignment operator, ký hiệu: =) được sử dụng để gán giá trị cho các biến. Các nhà phát triển thường nhầm lẫn nó với toán tử bình đẳng (equality operator).
Đây là một ví dụ:
const name = "javascript";
if ((name = "nodejs")) {
console.log(name);
}
// output - nodejs
Biến tên và chuỗi ‘nodejs’ không được so sánh trong trường hợp này. Thay vào đó, 'nodejs' được gán cho tên và được in ra bảng điều khiển.
Trong JavaScript, dấu bằng kép (==) và dấu bằng ba (===) được gọi là toán tử so sánh (comparison operator).
Đối với đoạn code trên, dưới đây mới là cách thích hợp để so sánh các giá trị:
const name = "javascript";
if (name == "nodejs") {
console.log(name);
}
// no output
// OR
if (name === "nodejs") {
console.log(name);
}
// no output
Sự khác biệt giữa các toán tử so sánh này là dấu bằng đôi thực hiện so sánh lỏng lẻo (loose comparison) và dấu bằng ba thực hiện so sánh chặt chẽ (strict comparison).
Trong loose comparison, chỉ các giá trị được so sánh. Nhưng trong một strict comparison, các giá trị và cả kiểu dữ liệu đều được so sánh.
Như đoạn code sau đây:
const number = "1";
console.log(number == 1);
// true
console.log(number === 1);
// false
Biến số được gán giá trị chuỗi là 1. Khi so sánh với 1 (thuộc kiểu số) bằng cách sử dụng ==, nó trả về true vì cả hai giá trị đều là 1.
Nhưng khi so sánh bằng cách sử dụng ===, nó trả về false vì mỗi giá trị có một kiểu dữ liệu khác nhau.
2. Mong chờ các callback đều đồng bộ
Callback (gọi lại) là một cách mà JavaScript xử lý các hoạt động không đồng bộ. Tuy nhiên, Promise và async/await mới là các phương thức thích hợp hơn để xử lý các hoạt động không đồng bộ vì nhiều callback sẽ dẫn đến callback hell.
Các callback không đồng bộ. Chúng được sử dụng như một hàm được gọi sau một hoạt động khi hoàn thành một quá trình thực thi bị trì hoãn.
Một ví dụ là hàm setTimeout nhận một hàm callback làm đối số đầu tiên và thời lượng (tính bằng ms) là đối số thứ hai như sau:
function callback() {
console.log("I am the first");
}
setTimeout(callback, 300);
console.log("I am the last");
// output
// I am the last
// I am the first
Sau 300 mili giây, hàm callback được gọi. Nhưng trước khi nó hoàn thành, phần còn lại của code sẽ chạy. Đó là lý do tại sao console.log cuối cùng được chạy đầu tiên.
Một sai lầm phổ biến mà các nhà phát triển mắc phải là hiểu sai các lệnh callback là đồng bộ. Ví dụ, một lệnh callback trả về giá trị mà đáng lẽ sẽ được sử dụng cho các operation khác.
Sai lầm đó như sau:
function addTwoNumbers() {
let firstNumber = 5;
let secondNumber;
setTimeout(function () {
secondNumber = 10;
}, 200);
console.log(firstNumber + secondNumber);
}
addTwoNumbers();
// NaN
NaN là output bởi vì secondNumber là không xác định được. Tại thời điểm chạy firstNumber + secondNumber, secondNumber vẫn chưa được xác định vì hàm setTimeout sẽ thực hiện lệnh callback sau 200ms.
Cách tốt nhất để tiếp cận bài toán này là thực thi phần còn lại của code trong hàm callback:
function addTwoNumbers() {
let firstNumber = 5;
let secondNumber;
setTimeout(function () {
secondNumber = 10;
console.log(firstNumber + secondNumber);
}, 200);
}
addTwoNumbers();
// 15
3. Tham chiếu sai đến this
this là một khái niệm thường bị hiểu nhầm trong JavaScript. Để sử dụng this trong JavaScript, bạn thực sự cần hiểu cách hoạt động của nó vì nó hoạt động hơi khác so với các ngôn ngữ còn lại.
Dưới đây là một ví dụ về một lỗi phổ biến khi sử dụng this:
const obj = {
name: "JavaScript",
printName: function () {
console.log(this.name);
},
printNameIn2Secs: function () {
setTimeout(function () {
console.log(this.name);
}, 2000);
},
};
obj.printName();
// JavaScript
obj.printNameIn2Secs();
// undefined
Kết quả đầu tiên là JavaScript vì this.name trỏ chính xác đến thuộc tính tên của đối tượng (object). Kết quả thứ hai là undefined vì this đã làm mất tham chiếu đến các thuộc tính của đối tượng (bao gồm cả tên).
Điều này là do this phụ thuộc vào đối tượng gọi hàm mà nó sống trong đó. Có một biến this trong mọi hàm nhưng đối tượng mà nó trỏ đến lại được xác định bởi đối tượng gọi nó.
this trong obj.printName() trỏ trực tiếp đến obj. this trong obj.printNameIn2Secs trỏ trực tiếp đến obj. Nhưng this trong hàm callback của setTimeout không trỏ đến bất kỳ đối tượng nào vì không có đối tượng nào gọi nó.
Theo đó một đối tượng được gọi là setTimeout, hay đại loại như obj.setTimeout... sẽ được thực thi. Vì không có đối tượng nào gọi hàm đó nên đối tượng mặc định (window) được sử dụng.
name không tồn tại trên window, dẫn đến undefined.
Các cách tốt nhất để giữ lại tham chiếu this trong setTimeout là sử dụng các hàm bind, call, apply hoặc arrow (được giới thiệu trong ES6). Không giống như các hàm thông thường, các hàm arrow không tạo ra this của riêng chúng.
Vì vậy, code như sau sẽ có thể giữ nguyên tham chiếu đến this:
const obj = {
name: "JavaScript",
printName: function () {
console.log(this.name);
},
printNameIn2Secs: function () {
setTimeout(() => {
console.log(this.name);
}, 2000);
},
};
obj.printName();
// JavaScript
obj.printNameIn2Secs();
// JavaScript
4. Bỏ qua khả năng thay đổi của đối tượng
Không giống như các kiểu dữ liệu nguyên thủy như chuỗi, số, v.v., trong các đối tượng JavaScript là kiểu dữ liệu tham chiếu. Ví dụ: trong các đối tượng khóa-giá trị (key-value object):
const obj1 = {
name: "JavaScript",
};
const obj2 = obj1;
obj2.name = "programming";
console.log(obj1.name);
// programming
obj1 và obj2 có cùng tham chiếu đến vị trí trong bộ nhớ nơi lưu trữ đối tượng.
Trong mảng:
const arr1 = [2, 3, 4];
const arr2 = arr1;
arr2[0] = "javascript";
console.log(arr1);
// ['javascript', 3, 4]
Một sai lầm phổ biến mà các nhà phát triển mắc phải là họ coi thường bản chất này của JavaScript và điều này dẫn đến các lỗi không mong muốn. Ví dụ: nếu 5 đối tượng có cùng tham chiếu đến cùng một đối tượng, một trong các đối tượng có thể can thiệp vào các thuộc tính (properties) trong một codebase quy mô lớn.
Khi điều này xảy ra, bất kỳ nỗ lực nào để truy cập vào các thuộc tính ban đầu sẽ trả về không xác định hoặc có thể gây ra lỗi.
Cách tốt nhất để phòng tránh và giải quyết lỗi này là luôn tạo các tham chiếu mới cho các đối tượng mới khi bạn muốn nhân bản một đối tượng. Để làm điều này, toán tử còn lại (rest operator, ký hiệu ... được giới thiệu trong ES6) là một giải pháp hoàn hảo.
Ví dụ: trong các đối tượng key-value:
const obj1 = {
name: "JavaScript",
};
const obj2 = { ...obj1 };
console.log(obj2);
// {name: 'JavaScript' }
obj2.name = "programming";
console.log(obj.name);
// 'JavaScript'
Trong mảng:
const arr1 = [2, 3, 4];
const arr2 = [...arr1];
console.log(arr2);
// [2,3,4]
arr2[0] = "javascript";
console.log(arr1);
// [2, 3, 4]
5. Lưu mảng và đối tượng vào bộ nhớ của trình duyệt
Thỉnh thoảng khi làm việc với JavaScript, các nhà phát triển có thể muốn tận dụng localStorage để lưu các giá trị. Nhưng một sai lầm phổ biến ở đây là cố gắng lưu các mảng và đối tượng nguyên trạng trong localStorage. localStorage chỉ chấp nhận các chuỗi.
Trong nỗ lực lưu các đối tượng, JavaScript chuyển đổi đối tượng thành một chuỗi. Vì thế đừng khóc thét khi kết quả bạn nhận được là [object object] cho các đối tượng và một chuỗi được phân tách bằng dấu phẩy cho các phần tử mảng.
Ví dụ:
const obj = { name: "JavaScript" };
window.localStorage.setItem("test-object", obj);
console.log(window.localStorage.getItem("test-object"));
// [Object Object]
const arr = ["JavaScript", "programming", 45];
window.localStorage.setItem("test-array", arr);
console.log(window.localStorage.getItem("test-array"));
// JavaScript, programming, 45
Khi các đối tượng được lưu như vậy, rất khó để truy cập chúng. Ở ví dụ, việc truy cập đối tượng như .name sẽ dẫn đến lỗi. Do [object object] bây giờ là một chuỗi, không có thuộc tính name.
Một cách tốt hơn để lưu các đối tượng và mảng trong bộ nhớ cục bộ là sử dụng JSON.stringify (để chuyển đổi đối tượng thành chuỗi) và JSON.parse (để chuyển đổi chuỗi thành đối tượng). Bằng cách này, việc truy cập các đối tượng trở nên dễ dàng.
Phiên bản chính xác của đoạn code trên sẽ là:
const obj = { name: "JavaScript" };
window.localStorage.setItem("test-object", JSON.stringify(obj));
const objInStorage = window.localStorage.getItem("test-object");
console.log(JSON.parse(objInStorage));
// {name: 'JavaScript'}
const arr = ["JavaScript", "programming", 45];
window.localStorage.setItem("test-array", JSON.stringify(arr));
const arrInStorage = window.localStorage.getItem("test-array");
console.log(JSON.parse(arrInStorage));
// JavaScript, programming, 45
6. Không sử dụng các giá trị mặc định
Đặt giá trị mặc định (default value) trong các biến động (dynamic variable) là một cách rất tốt để ngăn ngừa các lỗi không mong muốn. Đây là một ví dụ về một lỗi phổ biến:
function addTwoNumbers(a, b) {
console.log(a + b);
}
addTwoNumbers();
// NaN
Kết quả trả về NaN bởi vì a là undefined và b cũng undefined. Khi sử dụng giá trị mặc định, lỗi như này sẽ được ngăn chặn. Ví dụ:
function addTwoNumbers(a, b) {
if (!a) a = 0;
if (!b) b = 0;
console.log(a + b);
}
addTwoNumbers();
// 0
Ngoài ra, tính năng giá trị mặc định được giới thiệu trong ES6 có thể được sử dụng như sau:
function addTwoNumbers(a = 0, b = 0) {
console.log(a + b);
}
addTwoNumbers();
// 0
Ví dụ này nhấn mạnh tầm quan trọng của các giá trị mặc định. Ngoài ra, các nhà phát triển có thể cung cấp lỗi hoặc thông báo cảnh báo khi có các giá trị ngoài mong đợi được cung cấp.
7. Đặt tên biến không đúng
Vâng, các nhà phát triển vẫn mắc lỗi này. Việc đặt tên rất khó, nhưng đó là điều cơ bản và bạn không nên mắc những sai lầm ngớ ngẩn.
Ví dụ:
function total(discount, p) {
return p * discount
}
Biến discount thì ok thôi, nhưng còn p và total? Total của cái gì? Code chỉn chu thì phải như bên dưới:
function totalPrice(discount, price) {
return discount * price
}
Việc đặt tên biến đúng cách là rất quan trọng vì ở hiện tại và tương lai luôn có nhiều lập trình viên khác cùng làm việc với bạn trên một codebase. Đặt tên các biến đúng cách sẽ giúp mọi người dễ dàng hiểu được cách hoạt động của một dự án.
8. Kiểm tra các giá trị boolean
const isRaining = false
if(isRaining) {
console.log('It is raining')
} else {
console.log('It is not raining')
}
// It is not raining
Việc kiểm tra các giá trị boolean rất phổ biến như có thể thấy trong đoạn code trên. Ổn thôi, nhưng lỗi lại xuất hiện khi kiểm tra một số giá trị.
Trong JavaScript, loose comparison của 0 và false trả về true, và 1 và true trả về true. Điều này có nghĩa là nếu isRain là 1, isRain sẽ là true.
Đây cũng là một lỗi thường mắc phải ở các object. Ví dụ:
const obj = {
name: 'JavaScript',
number: 0
}
if(obj.number) {
console.log('number property exists')
} else {
console.log('number property does not exist')
}
// number property does not exist
Mặc dù property number tồn tại, obj.number trả về 0, là một giá trị sai, do đó khối else được thực thi.
Vì vậy, trừ khi bạn chắc chắn về phạm vi giá trị sẽ được sử dụng, các giá trị và thuộc tính boolean trong các đối tượng nên được kiểm tra như sau:
if(a === false)...
if(object.hasOwnProperty(property))...
8. Lẫn lộn giữa Addition và Concatenation
Dấu cộng (+) có hai chức năng trong JavaScript: cộng và nối. Phép cộng dành cho các số và nối là cho các chuỗi. Một số nhà phát triển thường sử dụng sai toán tử này.
Ví dụ:
const num1 = 30;
const num2 = "20";
const num3 = 30;
const word1 = "Java"
const word2 = "Script"
console.log(num1 + num2);
// 3020
console.log(num1 + num3);
// 60
console.log(word1 + word2);
// JavaScript
Khi thêm chuỗi và số, JavaScript sẽ chuyển đổi các số thành chuỗi và nối tất cả các giá trị. Để cộng các số, một phép toán phải được thực hiện.
Kết luận
Hãy đảm bảo rằng bạn luôn cập nhật những phát triển và đổi mới trong ngôn ngữ JavaScript. Nghiên cứu và tránh những sai lầm này sẽ giúp bạn xây dựng các ứng dụng và công cụ web tốt hơn và trở thành một lập trình viên JavaScript đáng tin cậy hơn.
Trên đây là các thông tin về JavaScipt, nếu muốn học thêm các khóa học lập trình khác, đừng quên liên hệ với chúng tôi tại Viện Công nghệ thông tin T3H nhé!