Menu

Cú pháp và dấu chấm phẩy JavaScript: giải thích quy tắc ASI

Cú pháp JavaScript hoạt động thế nào: câu lệnh, khối lệnh, biểu thức, cùng sự thật về dấu chấm phẩy, cơ chế tự động chèn dấu chấm phẩy và lúc nó gây lỗi.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Code JavaScript là một chuỗi câu lệnh

Một chương trình JavaScript là danh sách các câu lệnh (statement): những chỉ thị mà engine chạy từ trên xuống dưới. Khai báo biến là một câu lệnh. Gọi hàm là một câu lệnh. Một khối if hay for cũng là một câu lệnh. Các câu lệnh được ngăn cách với nhau bằng dấu chấm phẩy.

Ba câu lệnh, ba dấu chấm phẩy. Engine chạy câu thứ nhất, rồi câu thứ hai, rồi câu thứ ba. Khá đơn giản.

Khoảng trắng và thụt lề không có ý nghĩa gì với bộ phân tích cú pháp (parser). Bạn có thể đặt cả ba câu lệnh trên một dòng, cách nhau bằng dấu chấm phẩy, và kết quả vẫn y hệt. Chúng ta thụt lề cho người đọc, không phải cho engine.

Khối lệnh gom các câu lệnh lại với nhau

Cặp ngoặc nhọn { } tạo ra một khối lệnh (block): nhóm câu lệnh mà engine coi như một đơn vị. Bạn sẽ thấy khối lệnh ở khắp nơi: thân hàm, nhánh if, thân vòng lặp.

Hai lời gọi console.log trong cặp ngoặc là thân của if. Để ý rằng không có dấu chấm phẩy sau dấu } đóng: khối lệnh không phải câu lệnh cần kết thúc. Toàn bộ câu lệnh if kết thúc khi khối lệnh kết thúc.

Điều này hay làm khó những ai đến từ ngôn ngữ mà mọi dấu } đều cần một dấu ; theo sau. Trong JavaScript, một khối lệnh đứng riêng hoặc khối điều khiển luồng thì không cần.

Biểu thức và câu lệnh

Có một khác biệt nên nắm sớm: biểu thức (expression) tạo ra một giá trị, còn câu lệnh thì thực hiện một việc gì đó.

  • 2 + 3 là một biểu thức. Nó cho ra giá trị 5.
  • let x = 2 + 3; là một câu lệnh. Nó khai báo một biến và gán kết quả của một biểu thức cho biến đó.
  • console.log("hi") là một biểu thức (lời gọi hàm trả về undefined), nhưng khi viết riêng trên một dòng kèm dấu chấm phẩy, nó trở thành một câu lệnh biểu thức (expression statement).

Phần lớn thời gian bạn không cần nghĩ về điều này. Nó quan trọng khi bạn gặp arrow function, toán tử ba ngôi, và những chỗ JavaScript cần một biểu thức nhưng bạn lại đưa vào một câu lệnh (hoặc ngược lại).

Chuyện dấu chấm phẩy

Câu trả lời thật lòng là: JavaScript có một tính năng gọi là Automatic Semicolon Insertion (ASI, tự động chèn dấu chấm phẩy), tự chèn dấu chấm phẩy còn thiếu cho bạn ở hầu hết các chỗ xuống dòng. Nghĩa là code như thế này vẫn chạy bình thường:

Không có dấu chấm phẩy, cũng không có lỗi. Ở mỗi chỗ xuống dòng, ASI tự hỏi: "token tiếp theo có thể nối tiếp câu lệnh hiện tại không?" Nếu không, nó chèn một dấu chấm phẩy.

Nhờ ASI, ngoài thực tế có hai phong cách đều hợp lệ:

  • Luôn viết dấu chấm phẩy. Đa số codebase, đa số bài hướng dẫn, đa số style guide.
  • Không viết dấu chấm phẩy. Được một số dự án hiện đại dùng (Standard style, một số codebase React). Dựa vào ASI cộng thêm vài mẹo phòng thủ.

Cả hai đều chạy được. Không cái nào là "sai". Điều thật sự sai là thiếu nhất quán: trộn hai phong cách trong cùng một tệp khiến bug khó phát hiện hơn.

Khi nào ASI gây lỗi

ASI làm đúng khoảng 99% trường hợp. 1% còn lại là những dòng bắt đầu bằng một token có thể nối tiếp dòng trước. Những thủ phạm là [, (, `, +, - và /.

Hãy xem ví dụ này:

Bạn sẽ nghĩ x và y được hoán đổi. Thực tế, ASI không chèn dấu chấm phẩy trước [x, y] vì 10[x, y] hợp lệ về cú pháp (truy cập chỉ số trên số 10). Parser đọc dòng 2 và dòng 3 thành một biểu thức lớn, và bạn nhận được lỗi khi chạy hoặc đầu ra vô nghĩa.

Cách sửa là thêm dấu chấm phẩy ở cuối dòng 2:

Hoặc, nếu bạn viết theo phong cách không dấu chấm phẩy, đặt một dấu chấm phẩy ở đầu dòng có rủi ro:

Dấu ; ở đầu dòng chính là mẹo phòng thủ mà các codebase không dấu chấm phẩy hay dùng. Nhìn thì lạ, cho đến khi bạn biết vì sao nó ở đó.

Cái bẫy return

Có một trường hợp ASI nên nhớ kỹ vì nó gây ra bug âm thầm. Nếu bạn xuống dòng ngay sau return, ASI sẽ chèn dấu chấm phẩy ở đó và hàm trả về undefined:

Người viết muốn trả về một đối tượng. ASI thấy return ở cuối dòng và kết thúc câu lệnh ngay tại chỗ. Object literal trở thành code không bao giờ được chạy tới.

Cách sửa là bắt đầu giá trị trên cùng dòng với return:

Quy tắc tương tự áp dụng cho throw, break, continue và yield: đừng xuống dòng giữa từ khóa và giá trị của nó.

Một quy tắc đơn giản mà hiệu quả

Nếu bạn mới bắt đầu, cách dễ nhất là:

  1. Viết rõ dấu chấm phẩy ở cuối mọi câu lệnh.
  2. Không đặt dấu chấm phẩy sau dấu } đóng khối của if, for, while, khai báo hàm hay thân lớp.
  3. Có đặt dấu chấm phẩy sau } với object literal hoặc một biểu thức hàm được gán cho biến: const f = function() {};.
  4. Dùng công cụ định dạng (Prettier, ESLint với một quy tắc style) và thôi bận tâm. Công cụ sẽ áp dụng bất kỳ quy ước nào nhóm bạn đã chọn.

Quy tắc này không phải luật bất biến: đó là quy ước bạn sẽ thấy trong phần lớn code JavaScript mình đọc. Cứ theo nó cho đến khi bạn có lý do để làm khác.

Phân biệt chữ hoa, chữ thường và định danh

Còn hai quy tắc nhỏ cũng thuộc phần này:

  • JavaScript phân biệt chữ hoa và chữ thường. userName, username và UserName là ba định danh khác nhau.
  • Định danh có thể chứa chữ cái, chữ số, _ và $, nhưng không được bắt đầu bằng chữ số. Định danh cũng không được trùng từ khóa dành riêng như class, return hay function.

Ký tự $ là hợp lệ nhưng theo quy ước thường được dành cho những thứ riêng của thư viện (jQuery từng dùng nó, một số hệ thống template vẫn còn dùng). Bạn hiếm khi tự chọn nó.

Tiếp theo: strict mode

JavaScript hiện đại âm thầm chạy trong một biến thể chặt chẽ hơn của ngôn ngữ, gọi là strict mode, biến một số hành vi dễ dãi thành lỗi thật sự. ES module và thân lớp mặc định đã ở strict mode, nhưng bạn vẫn nên biết nó thực sự thay đổi những gì: đó là nội dung của trang tiếp theo.

Câu hỏi thường gặp

JavaScript có cần dấu chấm phẩy không?

Về mặt kỹ thuật thì không: JavaScript có cơ chế Automatic Semicolon Insertion (ASI), tự thêm dấu chấm phẩy cho bạn ở hầu hết các chỗ xuống dòng. Trên thực tế, đa số nhóm vẫn viết rõ dấu chấm phẩy vì ASI có vài trường hợp đặc biệt (dòng bắt đầu bằng [, (, `, +, -, /) mà nó xử lý sai. Hãy chọn một phong cách và để một công cụ định dạng như Prettier áp dụng nó.

Automatic semicolon insertion trong JavaScript là gì?

ASI là quy tắc cho phép JavaScript chèn dấu chấm phẩy còn thiếu ở cuối dòng khi token tiếp theo không thể nối tiếp câu lệnh hiện tại. Nhờ vậy let x = 1 vẫn chạy được mà không cần dấu chấm phẩy. ASI thất bại khi một dòng tình cờ bắt đầu bằng thứ có thể nối tiếp dòng trước, như [ hoặc (, và JavaScript lặng lẽ ghép hai dòng đó làm một.

Cú pháp cơ bản của một câu lệnh JavaScript là gì?

Câu lệnh là một chỉ thị: khai báo biến, gọi hàm, một khối if, một vòng lặp. Các câu lệnh được ngăn cách bằng dấu chấm phẩy (viết rõ hoặc được chèn tự động). Một khối lệnh nằm trong { }. Bộ phân tích cú pháp bỏ qua khoảng trắng và thụt lề, nên định dạng code là để con người đọc, không phải cho engine.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu