Equality comparisons in Javascript
Bạn đã bao giờ băn khoăn giữa việc sử dụng == và === khi muốn thực hiện một phép so sánh bằng hay chưa? Có ý kiến cho rằng: ***" == chỉ so sánh về mặt giá trị, còn === thì so sánh về cả giá trị và kiểu dữ liệu"***. Nghe có vẻ hợp lý và dễ nhớ nhưng lại chưa chính xác. Phát biểu đúng phải là: ...
Bạn đã bao giờ băn khoăn giữa việc sử dụng == và === khi muốn thực hiện một phép so sánh bằng hay chưa? Có ý kiến cho rằng: ***" == chỉ so sánh về mặt giá trị, còn === thì so sánh về cả giá trị và kiểu dữ liệu"***. Nghe có vẻ hợp lý và dễ nhớ nhưng lại chưa chính xác. Phát biểu đúng phải là: ***“ == cho phép ép kiểu (coercion hay chuyển kiểu – type conversion) còn === thì không.”***. Vậy lời giải thích thứ 2 khác gì so với lời giải thích ban đầu về sự khác nhau giữa == và === ? Nội dung bài viết này sẽ trả lời cho câu hỏi trên. Nhưng trước tiên, chúng ta hay cùng nhau tìm hiểu về các kiểu so sánh bằng trong Javascript.
Trong ES2015, có 4 thuật toán so sánh bằng:
- So sánh bằng trừu tượng (==)
- So sánh bằng chính xác (===)
- SameValueZero
- SameValue
Javascript cung cấp 3 toán tử/hàm so sánh:
- == sử dụng trong so sánh bằng trừu tượng (so sánh 2 bằng)
- === sử dụng trong so sánh bằng chính xác (so sánh 3 bằng)
- Object.is (có trong ES2015)
So sánh bằng chính xác (===)
So sánh bằng về mặt giá trị. Không có toán hạng nào được ép kiểu ngầm trước khi được so sánh. Do đó, nếu 2 toán hạng khác nhau về kiểu dữ liệu, thì chúng được coi là không bằng nhau. Trường hợp ngoại lệ:
- NaN !== NaN : Đối với giá trị NaN (Not a Number) , ví dụ như căn bậc 2 của một số âm, thì phép so sánh === trả về false ngay cả khi so sánh với chính nó.
- +0 === 0 : Giá trị 0 được coi là bằng nhau bất kể có dấu hay không.
So sánh bằng trừu tượng (==)
So sánh bằng cho phép ép/chuyển kiểu. Nếu 2 toán hạng khác nhau về kiểu dữ liệu thì phép so sánh được thực hiện sau khi đã chuyển đổi 2 giá trị về cùng một kiểu dữ liệu. Trường hợp ngoại lệ ở phép so sánh == cũng tương tự phép so sánh ===
- NaN != NaN
- +0 == -0
So sánh: string với number
Xem xét ví dụ sau:
var a = 42; var b = "42"; a === b; // false a == b; // true
Ở phép so sánh đầu tiên, kết quả là đương nhiên vì 2 toán hạng này có kiểu dữ liệu khác nhau. Ở phép so sánh thứ 2, một trong 2 hoặc cả 2 toán hạng đã được chuyển kiểu ngầm trước khi so sánh. Vậy giá trị 42 của a được chuyển kiểu về string hay “42” của b được chuyển kiểu về number ? Trong đặc tả của ES5, mệnh đề 11.9.3.4-5 phát biểu rằng:
- If Type(x) is Number and Type(y) is String, return the result of the comparison x == ToNumber(y).
- If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.
Tức nếu so sánh string với number thì giá trị kiểu string sẽ được chuyển về kiểu number. Do đó, câu trả lời đúng là giá trị “42” sẽ được chuyển kiểu về dạng number.
So sánh: kiểu boolean
Một trong những vấn đề lớn nhất gặp phải khi ép kiểu ngầm trong so sánh == . Xem xét ví dụ sau:
var a = "42"; // bad (will fail!): if (a == true) { // .. } // good enough (works implicitly): if (a) { // .. }
Rõ ràng 42 là giá trị đúng. Tuy nhiên câu điều kiện đầu trả về false. Lí giải cho điều này là mệnh đề 11.9.3.6-7 trong đặc tả ES5:
If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
Theo đó, khi so sánh giá trị kiểu boolean thì giá trị này sẽ được chuyển kiểu về dạng number. Quay trở lại phép so sánh “42” == true ở ví dụ trên. true được ép kiểu thành 1. Phép so sánh trở thành “42” == 1. Kiểu dữ liệu của 2 toán hạng vẫn khác nhau. “42” lại được ép kiểu về dạng number là 42. Phép so sánh cuối cùng là 42 == 1. Rõ ràng kết quả của phép so sánh này là false. Tương tự, phép so sánh “42” == false cũng trả về false khi false được ép kiểu về 0. Vậy “42” không == true và cũng không == false. Một giá trị không đúng cũng không không sai. Có vẻ lạ nhỉ? Thực tế thì “42” là giá trị đúng. ToBoolean(“42”) = true. Tính đúng đắn của nó không liên quan tới phép so sánh == true hay == false. Bởi trong các phép so sánh này, không hề có ép kiểu về dạng boolean nào được thực hiện cả. Do đó, ta nên tránh sử dụng phép so sánh bằng (== và ===) với giá trị kiểu boolean. Khi so sánh giá trị kiểu boolean trong phép so sánh ==, giá trị boolean luôn được ép kiểu về dạng number trước tiên.
So sánh: null và undefined
Hai giá trị này là bằng nhau trong phép so sánh == và không bằng bất cứ giá trị nào khác. Chính vì vậy ta có thể dùng câu điều kiện a == null để kiểm tra a có bằng null hay undefined hay không thay vì a == null || a == undefined. Với trường hợp ta chỉ muốn kiểm tra a có phải là undefined hay không (a có thể bằng null) thì hàm typeof sẽ giải quyết vấn đề này. typeof a == “undefined”.
So sánh: object và non-object
Giá trị kiểu object được ép kiểu về dạng nguyên thủy thông qua hàm trừu tượng ToPrimitive (valueOf(), toString()) khi so sánh với giá trị không phải dạng object. Xét ví dụ sau:
var a = "abc"; var b = Object( a ); // same as `new String( a )` a === b; // false a == b; // true
b là giá trị kiểu object tương đương với new String( a ). Thông qua hàm toString(), giá trị trả về sau khi ép kiểu là “abc” bằng với giá trị củaa.
Bảng so sánh == với các kiểu dữ liệu khác nhau:
Lưu ý khi sử dụng so sánh == :
- Tránh sử dụng trong các phép so sánh có các giá trị true hoặc false
- Xem xét cẩn thận trong các phép so sánh có các giá trị [], “” hoặc 0
- Sử dụng hàm typeof để kiểm tra kiểu dữ liệu trước khi so sánh.
Đến đây ta có thể thấy so sánh === an toàn hơn với so sánh == khi không cho phép ép kiểu. Đồng thời cũng lý giải được sự nhầm lẫn về 2 kiểu so sánh này như phần đầu của bài viết đã đề cập. “So sánh == chỉ so sánh về mặt giá trị” là chưa đúng. Cả 2 kiểu so sánh == và === đều so sánh kiểu dữ liệu trước tiên. Sau đó mới so sánh về mặt giá trị.
- Nếu kiểu dữ liệu trùng nhau: 2 kiểu so sánh này là như nhau.
- Nếu kiểu dữ liệu không trùng nhau: + So sánh === trả về false và kết thúc việc so sánh. + So sánh == thực hiện chuyển đổi 2 toán hạng về cùng một kiểu dữ liệu rồi tiếp tục so sánh về mặt giá trị.
Dễ thấy, so sánh == thực hiện nhiều công việc hơn khi 2 toán hạng không cùng một kiểu dữ liệu. Do đó, hiệu năng so sánh thấp hơn với so sánh ===. Tuy nhiên sự chênh lệch này là không đáng kể. Việc quyết định sử dụng kiểu so sánh nào chỉ nên dựa vào việc bạn có muốn chuyển kiểu được thực hiện hay không mà thôi. Không nên lạm dụng so sánh === trong mọi trường hợp. Ví dụ dưới đây cho thấy việc sử dụng == làm code trở nên dễ đọc hơn, thậm chí có phần nhanh hơn:
var a = doSomething(); if (a == null) { // .. }
var a = doSomething(); if (a === undefined || a === null) { //unnecessary and ugly // .. }
Ngoài 2 kiểu so sánh trên, Javascript còn 2 thuật toán so sánh được sử dụng chủ yếu bên trong phần core của ngôn ngữ:
So sánh SameValue
- Thuật toán so sánh này tương tự với so sánh === nhưng coi +0 và -0 là không bằng nhau NaN bằng với chính nó
- Hàm tương ứng: Object.is (có trong ES2015) Object.is(+0, -0) = false Object.is(NaN, NaN) = false
So sánh SameValueZero
- Thuật toán này tương tự thuật toán SameValue ngoại trừ việc coi +0 và -0 là bằng nhau
- Thuật toán này chưa có toán tử hay hàm tương ứng.
Bảng dưới đây tổng kết 4 kiểu so sánh bằng trong Javascript.
Nguồn tham khảo:
- Kyle Simpson, You Don’t Know JS, Chapter 4: Coercion, https://github.com/getify/You-Dont-Know-JS/blob/master/types %26 grammar/ch4.md#loose-equals-vs-strict-equals
- MDN Web Docs, Equality comparisons and sameness, https://developer.mozilla.org/en/docs/Web/JavaScript/Equality_comparisons_and_sameness