Effective JavaScript - Chapter 1 - Accustoming Yourself to JavaScript (Part V)
Bài đăng này đã không được cập nhật trong 7 năm
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.
Item 6: Learn the Limits of Semicolon Insertion
Một trong những tiện lợi của JavaScript chính là khả năng lược bỏ các dấu chấm phẩy kết thúc lệnh (statement-terminating semicolon) (từ giờ ta gọi tắt là DCP):
function Point(x, y) {
this.x = x || 0
this.y = y || 0
}
Point.prototype.isOrigin = function() {
return this.x === 0 && this.y === 0
}
Đoạn code này có thể chạy là nhờ chương trình tự động thêm DCP (automatic semicolon insertion). Chương trình này tự động phát hiện và thêm DCP vào các chỗ bị thiếu. Chính chuẩn ECMAScript đã đặc tả cơ chế này.
Nhưng tương tự như với thao tác ép kiểu ngầm, việc thêm DCP cũng có những cạm bẫy và đơn giản là bạn không thể tránh được việc học các quy tắc. Thậm chí nếu bạn không bao giờ bỏ qua các DCP, sẽ vẫn có những ràng buộc về cú pháp JavaScript mà bạn cần biết. Những ràng buộc này là hệ quả của việc thêm DCP. Tin tốt lành là một khi bạn học các quy tắc về việc thêm DCP, bạn có thể tự do loại bỏ các DCP không cần thiết.
Quy tắc đầu tiên là: Các DCP chỉ được thêm vào trước thẻ }, sau newline hoặc ở cuối đầu vào chương trình.
Nói cách khác, bạn có thể bỏ các DCP ở cuối dòng, cuối block hay cuối chương trình. Những function sau là hợp lệ:
function square(x) {
var n = +x
return n * n
}
function area(r) { r = +r; return Math.PI * r * r }
function add1(x) { return x + 1 }
Nhưng function này thì không:
function area(r) { r = +r return Math.PI * r * r } // error
Quy tắc thứ hai là: Các DCP chỉ được thêm khi thẻ đầu vào tiếp theo không được parse.
Nói cách khác, việc thêm DCP là một cơ chế sửa lỗi. Ví dụ, đoạn code:
a = b
(f());
sẽ được coi như là một câu lệnh đơn:
a = b(f());
Có nghĩa là chẳng có DCP nào được thêm vào. Ngược lại, đoạn code:
a = b
f();
sẽ được parse như là hai câu lệnh riêng biệt, bởi vì:
a = b f();
là một lỗi về parsing.
Quy tắc này có một ngụ ý: Bạn phải luôn luôn chú ý tới đoạn bắt đầu của câu lệnh tiếp theo để nhận ra xem là mình có thể bỏ qua DCP hay không. Bạn không thể bỏ qua DCP của một câu lệnh nếu như thẻ khởi đầu của dòng tiếp theo được hiểu như là sự tiếp nối của câu lệnh hiện tại.
Chính xác là có 5 ký tự mà chúng ta cần coi chừng: (
, [
, +
, -
và /
. Mỗi ký tự thực hiện một chức năng - có thể là một toán tử biểu thức (expression operator) hoặc là một prefix của một câu lệnh, tùy vào ngữ cảnh. Vậy nên hãy coi chừng các câu lệnh kết thúc với một biểu thức giống như câu lệnh gán (assignment statement) ở trên. Nếu dòng tiếp theo bắt đầu với bất cứ ký tự nào trong năm ký tự trên, sẽ không có DCP nào được thêm vào. Trường hợp phổ biến nhất mà điều này xảy ra chính là khi một câu lệnh bắt đầu với dấu ngoặc đơn, giống như ví dụ ở trên. Một trường hợp phổ biến khác chính là khi sử dụng array literal:
a = b
["r", "g", "b"].forEach(function(key) {
background[key] = foreground[key] / 2;
});
Trông thì có vẻ là hai câu lệnh nhưng do có ký tự [
nên đoạn code trên tương đương với:
a = b["r", "g", "b"].forEach(function(key) {
background[key] = foreground[key] / 2;
});
Các biểu thức sẽ được thực hiện từ trái qua phải. Chú ý một chút, b["r", "g", "b"]
chính là b["b"]
. Vì sao thì các bạn tự tìm hiểu một chút nhé Do đó, cuối cùng đoạn code trên lại có thể rút gọn còn:
a = b["b"].forEach(function(key) {
background[key] = foreground[key] / 2;
});
Các thẻ +
, -
, /
ít được thấy ở đầu các câu lệnh hơn. Trường hợp của /
thì khá tinh vi: Ở dầu một câu lệnh, nó không hoàn toàn là một thẻ mà là khởi đầu của một thẻ regular expression:
/Error/i.test(str) && fail();
Câu lệnh này kiểm tra một string với regular expression /Error/i
. Nếu match, câu lệnh sẽ gọi đến hàm fail
. Nhưng nếu đoạn code này theo sau một phép gán chưa kết thúc:
a = b
/Error/i.test(str) && fail();
thì đoạn code sẽ được parse thành một câu lệnh:
a = b / Error / i.test(str) && fail();
Nói cách khác, thẻ /
được coi như là toán tử chia (division operator).
Các lập trình viên JavaScript lão luyện sẽ nhìn vào dòng kế tiếp một câu lệnh bất cứ khi nào họ muốn để lại DCP để đảm bảo rằng là câu lệnh sẽ không bị parse sai. Họ cũng cẩn trọng khi refactor code. Ví dụ, một chương trình đúng với 3 DCP:
a = b // semicolon inferred
var x // semicolon inferred
(f()) // semicolon inferred
có thể sẽ thay đổi thành:
var x // semicolon inferred
a = b // no semicolon inferred
(f()) // semicolon inferred
Đoạn code đầu tiên sẽ có ba DCP được thêm vào tự động:
a = b;
var x;
(f());
Trong khi ở đoạn code thứ hai, số DCP được thêm vào chỉ là hai:
var x;
a = b(f());
Một điều chú ý nữa là việc đưa dòng khai báo var x
lên bên trên không gây ra hiệu ứng không mong muốn (Item 12 sẽ đề cập đến variable scope - phạm vi của biến).
Kết quả cuối cùng là bạn luôn cần phải ý thức được về các DCP được bỏ qua và kiểm tra xem dòng tiếp theo có chứa các thẻ ngăn chặn việc tự động thêm DCP hay không.
Thay vào đó, bạn có thể tuân theo quy tắc về việc luôn luôn cố định các câu lệnh bắt đầu bởi (
, [
, +
, -
với một DCP. Ví dụ:
a = b // semicolon inferred
var x // semicolon on next line
;(f()) // semicolon inferred
Bây giờ thì đoạn code đã trở nên an toàn khi chuyển câu lệnh khai báo biến lên trên:
var x // semicolon inferred
a = b // semicolon on next line
;(f()) // semicolon inferred
Một kịch bản phổ biến khác mà các DCP bị bỏ sót có thể gây ra các vấn đề chính là việc nối script (xem Item 1). Mỗi file có thể gồm một biểu thức gọi hàm lớn (xem Item 13 để tìm hiểu về các biết thêm về IIFE - immediately invoked function expressions):
// file1.js
(function() {
// ...
})()
// file2.js
(function() {
// ...
})()
Khi mỗi file được load như là một chương trình riêng biệt, một DCP sẽ được tự động thêm vào cuối - biến việc gọi hàm thành một câu lệnh. Nhưng khi các file được nối:
(function () {
// ...
})()
(function () {
// ...
})()
kết quả được coi như là một câu lệnh đơn:
(function () {
// ...
})()(function () {
// ...
})();
Kết quả cuối cùng: Việc bỏ qua một DCP ở một câu lệnh đòi hỏi chúng ta phải chú ý đến không chỉ thẻ tiếp theo của file hiện tại mà còn cả thẻ tiếp theo có thể theo sau câu lệnh sau khi nối script. Tương tự cách tiếp cận được mô tả ở trên, bạn có thể tránh được hậu quả không mong muốn của việc nối file bằng cách cố định trước các file với một DCP, chí ít khi câu lệnh đầu tiên của nó bắt đầu với một trong năm ký tự đặc biệt (
, [
, +
, -
hay /
:
// file1.js
;(function () {
// ...
})()
// file2.js
;(function () {
// ...
})()
Điều này đảm bảo rằng thậm chí nếu file trước bỏ sót DCP kết thúc, kết quả cuối cùng sẽ vẫn được coi như là các câu lệnh riêng biệt:
;(function () {
// ...
})()
;(function () {
// ...
})()
Tất nhiên, sẽ tốt hơn nếu như quá trình nối script sẽ tự động thêm DCP giữa các file. Nhưng không phải tất cả các tool nối file đều làm tốt điều này, vậy nên tốt hơn hết là bạn chủ động thêm DCP như ở ví dụ bên trên.
Vào thời điểm này, bạn có thể đang nghĩ "Có quá nhiều thứ để quan tâm. Tôi sẽ không bao giờ bỏ sót các DCP và mọi thứ sẽ ổn", chứ không phải: Có một số trường hợp mà JavaScript sẽ thêm DCP một cách ép buộc mặc dù dường như sẽ không có lỗi xảy ra. Những trường hợp này liên quan tới những cái mà người ta gọi là restricted production của cú pháp JavaScript mà ở đó newline không được cho phép xuất hiện giữa hai thẻ. Trường hợp may rủi nhất chính là ở câu lệnh return
- khi mà newline sẽ không được chấp nhận ở giữa keyword return
và tham biến tùy chọn (optional argument) của nó. Vậy nên câu lệnh:
return { };
sẽ trả về một object, trong khi đoạn code:
return
{ };
sẽ được coi parse như là ba câu lệnh riêng biệt:
return;
{ }
;
Nói cách khác, newline cho theo sau keyword return
sẽ dẫn tới việc thêm DCP tự động. Các restricted production khác là:
-
Cậu lệnh
throw
-
Câu lệnh
break
vàcontinue
với một nhãn (label) tường minh -
Toán tử
++
hoặc--
ở vị trí đuôi
Mục đích của quy tắc cuối cùng có ý nghĩa trong đoạn code sau:
a
++
b
Bởi vì ++
có thể là tiền tố hoặc hậu tố nhưng cái thứ hai không thể đứng sau newline. Đoạn code trên sẽ được parse thành:
a; ++b;
Luật thứ ba và cuối cùng của việc thêm DCP là:
Các DCP không bao giờ được thêm vào như là các dấu ngăn cách (separator) ở đầu của một vòng lặp for
hoặc như là các câu lệnh trống (empty statement)
Điều này đơn giản có nghĩa là bạn luôn luôn phải thêm các DCP một cách tường minh ở đầu vòng lặp for
. Nếu không, đoạn code như bên dưới:
for (var i = 0, total = 1 // parse error
i < n
i++) {
total *= i
}
sẽ gây ra parse error. Tương tự, một vòng lặp mà không có thân (body) cũng đòi hỏi một DCP tường minh. Nếu không, việc bỏ sót DCP sẽ dẫn đến parse error:
function infiniteLoop() { while (true) } // parse error
Vậy đây là một trường hợp mà DCP là bắt buộc:
function infiniteLoop() { while (true); }
Things to Remember:
-
DCP chỉ được ngầm hiểu trước
}
, ở cuối dòng hoặc ở cuối chương trình -
DCP chỉ được ngầm hiểu khi thẻ tiếp theo không được parse
-
Đừng bao giờ bỏ sót DCP trước một câu lệnh bắt đầu bởi
(
,[
,+
,-
hoặc/
-
Khi nối script, hãy thêm các DCP một cách tường minh giữa các script
-
Đừng bao giờ đặt một newline trước tham biến của
return
,throw
,break
,continue
,++
hay--
-
DCP không bao giờ được ngầm hiểu như là dấu ngăn cách ở đầu của một vòng lặp
for
hoặc như là các câu lệnh trống
Trên đây là phần dịch cho Item 6 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/
All rights reserved