12/08/2018, 15:57

Effective JavaScript - Chapter 1 - Accustoming Yourself to JavaScript (Part IV)

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.

Ngoài các object, JavaScript còn có 5 kiểu giá trị nguyên thủy (primitive value): boolean, number, string, null và undefined. Có một điều ngớ ngẩn ở đây chính là việc typeof null trả về kết quả "object" trong khi chuẩn ECMAScript định nghĩa nó là một kiểu riêng biệt. Thư viện chuẩn cũng cung cấp các constructor để wrap các giá trị boolean, number và string như là các object. Ví dụ, bạn có thể tạo ra một object String mà wrap một string value:

var s = new String("hello");

Ở một chừng mực nào đó, object String hoạt động tương tự như string value mà nó wrap. Bạn có thể nối nó với một value khác để tạo ra một string mới:

s + " world";  // "hello world"

Bạn có thể trích ra các substring:

s[4];  //"o"

Nhưng không giống như string nguyên thủy (primitive string), một object String là một object đúng nghĩa:

typeof "hello";  // "string"
typeof s;        // "object"

Đây là một điểm khác biệt quan trọng bởi vì điều này có nghĩa là bạn sẽ không thể so sánh content của hai object String riêng biệt bằng cách sử dụng các toán tử có sẵn (built-in operator):

var s1 = new String("hello");
var s2 = new String("hello");
s1 === s2;  // false

Bởi vì mỗi object String là một object riêng biệt, nó chỉ có thể bằng chính nó mà thôi. Điều tương tự cũng đúng với toán tử bằng không ngặt (nonstrict equality operator):

s1 == s2; // false

Do các wrapper này không hoạt động hoàn toàn đúng nên chúng không có nhiều ý nghĩa. Lý do chính đối với sự tồn tại của chúng chính là các method tiện ích mà chúng có. JavaScript đã làm cho việc tiếp cận các method này trở nên thuận tiện hơn bằng cách chèn vào một thao tác ép kiểu ngầm (coercion): Bạn có thể trích ra các property và gọi các method của một primitive value và nó sẽ hoạt động như thể là bạn đã wrap nó với một wrapper tương ứng. Ví dụ, object String có method toUpperCase chuyển một string về dạng in hoa (uppercase). Bạn có thể sử dụng method này với một giá trị string nguyên thủy (primitive string value):

"hello".toUpperCase();  // "HELLO"

Một kết quả kỳ quặc của việc wrap ngầm này chính là bạn có thể set property cho các primitive value nhưng không hề đem lại tác dụng:

"hello".someProperty = 17;
"hello".someProperty;   // undefined

Bởi vì việc wrap ngầm sẽ tạo ra một object String mới, việc update đối với một object wrapper là không có tác dụng. Mặc dù chẳng có ý nghĩa gì khi set các property cho các primitive value nhưng điều này cũng đáng để chú ý. Đây cũng là một ví dụ khác cho thấy JavaScript có thể giấu các lỗi về kiểu dữ liệu: Nếu bạn muốn set property cho một object nhưng không may đó lại là một giá trị nguyên thủy thì chương trình của bạn sẽ dễ dàng bỏ qua việc update và tiếp tục câu lệnh tiếp theo. Điều này có thể dễ dàng làm cho chương bị lỗi mà khó bị phát hiện.

Things to Remember:

  • Các object wrapper cho các kiểu dữ liệu nguyên thủy không có các hành vi giống như các giá trị nguyên thủy khi so sánh bằng

  • Việc set và set property với các giá trị nguyên thủy sẽ ngầm tạo ra các object wrapper

Bạn nghĩ đâu sẽ là giá trị trả về của biểu thức sau:

"1.0e0" == { valueOf: function() { return true; } };

Hai giá trị dường như không liên quan này thực tế được coi là tương đương bởi vì, giống như các thao tác ép kiểu ngầm được nhắc đến trong Item 3, chúng được chuyển thành các number trước khi được so sánh. String "1.0e0" được trở thành 1. Còn object ép về kiểu number bằng cách gọi method valueOf của nó và sau đó ép giá trị trả về (true) thành một number và 1 cũng là kết quả cuối cùng.

Các thao tác ép kiểu ngầm này được được hiện khi đọc các trường từ một form và so sánh nó với một number:

var today = new Date();
if (form.month.value == (today.getMonth() + 1) &&
    form.day.value == today.getDate()) {
    // happy birthday!
    // ...
}

Nhưng thực sự thì sẽ rất dễ dàng để ép các giá trị thành number một cách tường minh sử dụng Number function toán tử +:

var today = new Date();
if (+form.month.value == (today.getMonth() + 1) &&
    +form.day.value == today.getDate()) {
    // happy birthday!
    // ...
}

Đoạn code này rõ ràng hơn bởi vì nó giúp người đọc biết chính xác việc ép kiểu sẽ diễn ra mà không cần nhớ đến những quy tắc ép kiểu. Một sự thay thế tốt hơn chính là sử dụng toán tử bằng ngặt (strict equality operator - SEO):

var today = new Date();
if (+form.month.value === (today.getMonth() + 1) && // strict
    +form.day.value === today.getDate()) { // strict
    // happy birthday!
    // ...
}

Khi hai tham biến cùng kiểu dữ liệu, hành vi của == và === giống nhau. Vậy nên nếu bạn biết các tham biến có cùng kiểu dữ liệu, chúng có thể thay thế cho nhau. Nhưng sử dụng SEO là một cách tốt để cho người đọc biết là sẽ không có thao tác ép kiểu được diễn ra. Nếu không thì bạn làm cho người đọc buộc phải nhớ về những quy tắc ép kiểu để hiểu được code của bạn.

Hóa ra thì những quy tác ép kiểu không phải lúc nào cũng rõ ràng. Bảng 1.1 liệt kê các quy tắc ép kiểu đối với toán tử == khi các tham biến của nó có kiểu dữ liệu khác nhau. Những quy tắc này có tính đối xứng: Ví dụ, quy tắc đầu tiên đúng với cả null == undefined và undefined == null. Trong phần lớn thời gian, các thao tác ép kiểu cố gắng tạo ra number. Nhưng các quy tắc trở nên tinh vi khi tham biến là object. Object sẽ được cố gắng đưa về một giá trị nguyên thủy bằng cách gọi method valueOf và toString (giá trị đầu tiên mà chúng thu được sẽ được sử dụng). Thậm chứ còn tinh vi hơn khi mà các object Date còn cố gắng thử hai method này với thứ tự ngược lại.

Bảng 1.1 Các quy tắc ép kiểu ngầm với toán tử ==

Đôi khi bạn muốn JavaScript làm theo ý mình nhưng thực tế thì JavaScript lại không thể hiểu được ý của bạn. Ví dụ:

var date = new Date("1999/12/31");
date == "1999/12/31";  // false

Ở đây, việc so sánh sẽ trả về kết quả là "false" đơn giản là vì việc ép kiểu một object Date thành string sẽ tạo ra một string với một format khác:

date.toString();  // "Fri Dec 31 1999 00:00:00 GMT-0800 (PST)"

Đây là một dấu hiệu về sự hiểu lầm quen thuộc hơn về ép kiểu ngầm. Toán tử == không phỏng đoán hay thống nhất các format dữ liệu bất kỳ. Nó yêu cầu cả bạn và người đọc hiểu được những quy tắc ép kiểu ngầm tinh vi ấy. Một cách giải quyết tốt hơn là làm cho ép kiểu trở nên tường minh và sử dụng toán tử bằng ngặt (strict equality operator):

function toYMD(date) {
    var y = date.getYear() + 1900,  // year is 1900-indexed
        m = date.getMonth() + 1,    // month is 0-indexed
        d = date.getDate();
    return y + "/" + (m < 10 ? "0" + m : m) + "/" + (d < 10 ? "0" + d : d);
}
toYMD(date) === "1999/12/31";  // true

Làm cho các thao tác ép kiểu trở nên tường minh đảm bảo rằng bạn không bị nhầm lẫn giữa các quy tắc ép kiểu ngầm của == và thậm chí còn làm cho người đọc thấy dễ dàng hơn.

Things to Remember:

  • Toán tử == áp dụng một tập các thao tác ép kiểu ngầm khi tham biến của nó có kiểu dữ liệu khác nhau

  • Sử dụng === giúp người đọc hiểu được rằng là sẽ không có phép ép kiểu ngầm nào diễn ra

  • Sử dụng các thao tác ép kiểu tường minh khi so sánh các giá trị khác nhau về kiểu dữ liệu giúp cho hành vi của chương trình rõ ràng hơn

Trên đây là phần dịch cho Item 4Item 5 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/

0