Effective JavaScript - Chapter 1 - Accustoming Yourself to JavaScript (Part III)
JavaScript được thiết kế để mang lại cảm giác quen thuộc. Với cú pháp (syntax) gợi nhớ về Java và hàm dựng vốn dĩ đã phổ biến ở rất nhiều ngôn ngữ scripting (function, array, dictionary và regular expression), JavaScript dường như là một cái gì đó dễ học với bất cứ ai đã có một chút kinh nghiệm về ...
JavaScript được thiết kế để mang lại cảm giác quen thuộc. Với cú pháp (syntax) gợi nhớ về Java và hàm dựng vốn dĩ đã phổ biến ở rất nhiều ngôn ngữ scripting (function, array, dictionary và regular expression), JavaScript dường như là một cái gì đó dễ học với bất cứ ai đã có một chút kinh nghiệm về programming. Và với các programmer ít kinh nghiệm, họ có thể bắt đầu viết các chương trình mà không cần training quá nhiều tại vì lượng khái niệm core trong JavaScript là không quá nhiều.
Việc tiếp cận tuy dễ dàng, nếu muốn thuần thục (master) nó thì sẽ mất khá nhiều thời gian và đòi hỏi sự hiểu biết sâu hơn về ngữ nghĩa, các đặc tính và các idiom hữu hiệu nhất của nó. Mỗi chapter của cuốn sách này sẽ đề cập đến một phạm vi chủ đề khác nhau của effective JavaScript. Chương đầu tiên này bắt đầu với một vài topic cơ bản nhất.
JavaScript rất khoan dung khi gặp các lỗi về kiểu dữ liệu (type). Rất nhiều ngôn ngữ sẽ coi biểu thức (expression) kiểu như
3 + true; // 4
là lỗi bởi vì các biểu thức luận lý (boolean expression), ví dụ như true, sẽ không tương thích với số học (arithmetic). Trong một ngôn ngữ kiểu tĩnh (statically typed language), một chương trình với biểu thức như vậy thậm chí còn không được cho phép chạy. Trong một vào ngôn ngữ kiểu động (dynamically typed language), chương trình được phép chạy nhưng những biểu thức như vậy có thể gây ra lỗi. JavaScript không chỉ cho phép cho chương trình chạy, nó còn trả về kết quả là 4.
Có một vài trường hợp trong JavaScript sẽ sinh ra lỗi ngay lập tức. Ví dụ như khi gọi một nonfunction hoặc cố gắng lấy ra một property của null:
"hello"(1); // error: not a function null.x; // error: cannot read property 'x' of null
Nhưng trong nhiều trường hợp, thay vì raise lên lỗi, JavaScript ngầm convert một giá trị thành kiểu mong muốn bằng cách tuân theo các giao thức convert tự động. Ví dụ, toán tử số học -, *, / hay % sẽ cố gắng convert các tham biến của nó thành number trước khi thực hiện các phép toán. Toán tử + thì tinh vi hơn, bởi vì nó sẽ thực hiện hoặc là phép cộng hai number hoặc là nối hai string, tùy thuộc vào tham biến:
2 + 3; // 5 "hello" + " world"; // "hello world"
Bây giờ, điều gì sẽ xảy ra nếu bạn kết hợp number và string? JavaScript sẽ ưu tiên string:
"2" + 3; // "23" 2 + "3"; // "23"
Đôi khi sự hỗn tạp này gây ra sự nhầm lẫn, đặc biệt là vì thứ tự của các phép toán:
1 + 2 + "3"; // "33"
1 + "2" + 3; // "123"
Các phép toán theo bit (bitwise operation) không chỉ convert string thành number mà còn thành tập con của các number có thể biểu diễn như là các number nguyên 32 bit đã được đề cập đến trong Item 2. Những phép toán này bao gồm các toán tử số học theo bit (bitwise arithmetic operator - ~, &, ^, |) và toán tử dịch bit (shift operator - <<, >>, >>>).
Những thao tác convert ngầm này vô cùng thuận tiện - ví dụ như khi convert tự động các string là input từ người dùng, là một file text hoặc là một dòng nội dung số (network stream):
"17" * 3; // 51 "8" | "1"; // 9
Nhưng việc convert có thể sẽ ẩn đi các lỗi. Một biến (variable) là null sẽ không gây ra lỗi trong tính toán số học nhưng sẽ được convert ngầm thành 0. Một biến undefined sẽ được convert thành giá trị dấu phẩy động đặc biệt (special floating-point value) NaN ("not a number" number). Thay vì ném ra một exception ngay lập tức, những thao tác convert ngầm này có thể dẫn tới các kết quả không thể lường trước. Thực sự thì việc test giá trị NaN rất khó khăn. Có hai lý do. Đầu tiên chính là việc JavaScript tuân theo yêu cầu phức tạp của chuẩn IEEE về dấu phẩy động - NaN không bằng chính nó. Vì vậy chúng ta không thể kiểm tra một giá trị có bằng NaN hay không:
var x = NaN; x === NaN; // false
Hơn nữa, hàm thư viện chuẩn isNaN không đáng tin bởi thao tác convert ngầm của nó - tham biến sẽ bị convert thành một number trước khi kiểm tra giá trị. Đáng nhẽ ra, một cái tên chính xác hơn coercesToNaN nên được dùng thay vì isNaN. Nếu bạn đã biết một giá trị là number thì bạn có thể dùng isNaN:
isNaN(NaN); // true
Nhưng các giá trị khác hoàn toàn không phải là NaN lại không thể phân biệt được với NaN:
isNaN("foo"); // true isNaN(undefined); // true isNaN({}); // true isNaN({ valueOf: "foo" }); // true
May mắn là có một idiom đáng tin và ngắn gọn để kiểm tra giá trị NaN. Hãy chú ý NaN là giá trị JavaScript duy nhất không bằng chính nó:
var a = NaN; a !== a; // true var b = "foo"; b !== b; // false var c = undefined; c !== c; // false var d = {}; d !== d; // false var e = { valueOf: "foo" }; e !== e; // false
Vì vậy, chúng ta có thể tự viết hàm sau để so sánh một giá trị với NaN:
function isReallyNaN(x) { return x !== x; }
Nhưng như chúng ta đã thấy, việc kiểm tra một giá trị không bằng chính nó quá ngắn gọn nên chúng ta không cần viết hàm như bên trên. Chỉ cần viết như sau là ổn:
if (x !== x) { // Do something when x is NaN } else { // Do something when x is not NaN }
Các thao tác convert ngầm có thể khiến việc debug một chương trình lỗi trở nên khó khăn bởi vì chúng sẽ che lấp đi nhiều lỗi và làm cho chúng khó bị phát hiện. Khi một tính toán đi chệch hướng thì cách tiếp cận tốt nhất để debug chính là kiểm tra các kết quả trung gian của một tính toán. Chúng ta sẽ tìm ra vị trí lỗi. Từ đó, bạn có thể kiểm tra các tham biến của một phép toán để tìm ra các tham biến có kiểu dữ liệu không đúng. Tùy vào bug, đó có thể là một lỗi về logic (như là việc dùng sai toán tử) hoặc một lỗi về kiểu dữ liệu (truyền undefined thay vì một number).
Các object có thể được convert ngầm thành các kiểu dữ liệu nguyên thủy (primitive):
"the Math object: " + Math; // "the Math object: [object Math]" "the JSON object: " + JSON; // "the JSON object: [object JSON]"
Các object được convert thành string bằng cách ngầm gọi method toString của chúng. Bạn có thể tự mình kiểm tra method toString:
Math.toString(); // "[object Math]" JSON.toString(); // "[object JSON]"
Một cách tương tự, object có thể được convert thành number thông qua method valueOf của nó. Bạn có thể điều khiển việc convert các object bằng cách định nghĩa các method này:
"J" + { toString: function() { return "S"; } }; // "JS" 2 * { valueOf: function() { return 3; } }; // 6
Một lần nữa, mọi thứ trở nên phức tạp khi ta muốn sử dụng toán tử + với các object có cả hai method toString và valueOf. Về mặt lý thuyết, chúng ta sẽ không biết phải chọn sao cho hợp lý. Nhưng thực tế thì JavaScript lại giải quyết vấn đề mập mờ này bằng việc lựa chọn valueOf một cách mù quáng thay vì toString. Điều này có nghĩa là nếu bạn muốn thực hiện việc nối string với một object thì bạn có thể nhận được kết quả không mong muốn:
var obj = { toString: function() { return "[object MyObject]"; }, valueOf: function() { return 17; } }; "object: " + obj; // "object: 17"
Bài học của câu chuyện này chính là việc method valueOf thực sự được thiết kế cho các object biểu diễn các giá trị số (ví dụ như các object Number). Đối với các object này, method toString và valueOf sẽ trả về các kết quả thống nhất - một biểu diễn string hoặc một biểu diễn số của cùng một number - vậy nên toán tử + sẽ hoạt động một cách nhất quán mà chẳng cần quan tâm rằng là object đó được sử dụng để nối string hay dành cho phép toán cộng. Nhìn chung, việc convert ngầm thành string phổ biến hơn là thành number. Tốt nhất chúng ta nên tránh valueOf trừ phi object của bạn thực sự là một numeric abstraction và obj.toString() sẽ trả về một biểu diễn string của obj.valueOf().
Kiểu convert ngầm cuối cùng chính là truthiness. Các toán tử như if, || và && làm việc với các giá trị boolean nhưng thực tế lại chấp nhận tất cả các giá trị. Các giá trị JavaScript được hiểu như là các giá trị boolean theo một phép convert ngầm đơn giản. Hầu hết các giá trị JavaScript là truthy, tức là, sẽ được convert ngầm thành true. Điều này được áp dụng với tất cả các object. Có một chú ý là không giống như convert ngầm đối với string hay number, truthiness không gọi ngầm bất cứ method convert nào. Có đúng 7 giá trị falsy: false, 0, -0, "", NaN, null và undefined. Tất cả các giá trị khác là truthy. Bởi vì number và string có thể là falsy nên không phải khi nào việc sử dụng truthiness để check một tham biến hàm (function argument) hoặc một thuộc tính object (object property) có là defined hay không cũng là an toàn. Hãy xét một hàm nhận các tham biến tùy chọn với các giá trị mặc định:
function point(x, y) { if (!x) { x = 320; } if (!y) { y = 240; } return { x: x, y: y }; }
Hàm này bỏ qua các tham biến falsy, kể cả 0:
point(0, 0); // { x: 320, y: 240 }
Cách chính xác hơn để kiểm tra undefined chính là sử dụng typeof:
function point(x, y) { if (typeof x === "undefined") { x = 320; } if (typeof y === "undefined") { y = 240; } return { x: x, y: y }; }
Phiên bản này của hàm point phân biệt chính xác 0 với undefined:
point(); // { x: 320, y: 240 } point(0, 0); // { x: 0, y: 0 }
Một cách tiếp cận khác chính là so sánh với undefined:
if (x === undefined) { ... }
Item 54 sẽ đưa ra một số gợi ý về việc kiểm tra truthiness trong thiết kế library và API.
Things to Remember:
-
Các lỗi về kiểu dữ liệu có thể được giấu đi nhờ các thao tác convert ngầm
-
Toán tử + sẽ thực hiện phép toán cộng hoặc nối string tùy thuộc vào tham biến
-
Object được convert ngầm thành number thông qua method valueOf và thành string nhờ method toString
-
Object với method valueOf nên implement method toString trả về biểu diễn string của number sinh ra bởi method valueOf
-
Sử dụng method typeof hoặc so sánh với undefined thay vì truthiness để kiểm tra các giá trị undefined
Trên đây là phần dịch cho Item 3 trong 68 item được đề cập đến trong cuốn sách Effective JavaScript của tác giả David Herman.
Homepage: http://effectivejs.com/