Control flow and error handling

JavaScript hỗ trợ một tập hợp lệnh gọn nhẹ, các lệnh điều khiển luồng chuyên biệt, mà khi kết hợp lại có thể tăng tính tương tác cho ứng dụng của bạn lên đáng kể. Chương này giới thiệu sơ qua về các lệnh này.

Mục JavaScript reference chứa các chi tiết đầy đủ về các câu lệnh trong chương này. Dấu chấm phẩy (;) được dùng để ngăn cách giữa các lệnh trong code JavaScript.

Bât cứ một biểu thức JavaScript nào cũng đều là một câu lệnh. Xem Expressions and operators để biết thông tin đầy đủ về các biểu thức.

Block statement

Một trong những lệnh căn bản nhất là khối lệnh (block statement), được dùng để nhóm lại các câu lệnh. Khối lệnh được phân định bởi cặp dấu ngoặc nhọn:

{
  lệnh_1;
  lệnh_2;
  .
  .
  .
  lệnh_n;
}

Ví dụ

Khối lệnh thường được dùng với lệnh điều khiển luồng (như là if, for, while).

while (x < 10) {
  x++;
}

Ở đây, { x++; } là một khối lệnh.

Quan trọng: Trước ECMAScript2015 (phiên bản thứ 6), JavaScript chưa có phạm vi khối (block-scope). Trong phiên bản JavaScript cũ hơn, biến được khai báo trong khối được đặt phạm vi theo hàm hoặc đoạn mã bao bọc khối đó, và ảnh hưởng của việc thiết lập chúng sẽ vượt ra khỏi phạm vi của khối. Nói cách khác, khối lệnh không định nghĩa phạm vi. Các khối độc lập ("standalone" - tức là đi kèm với lệnh điều khiển nào) trong JavaScript có thể sản sinh kết quả khác so với khối lệnh tương tự trong Java hay C. Chẳng hạn:

var x = 1;
{
  var x = 2;
}
console.log(x); // trả về 2

Đoạn mã này trả về 2 bởi vì câu lệnh var x bên trong khối có cùng phạm vi với câu lệnh var x trước khối. Trong C hoặc Java, đoạn mã này sẽ trả về 1.

Kể từ ECMAScript2015, hai kiểu khai báo biến mới là letconst đều được đặt trong phạm vi khối lệnh. Tham khảo letconst để biết thêm chi tiết.

Lệnh điều kiện

Lệnh điều kiện là tập hợp các dòng lệnh sẽ thực thi nếu một điều kiện nhất định trả về true. JavaScript hỗ trợ hai lệnh điều kiện: if...elseswitch.

Lệnh if...else

Dùng mệnh đề if để thực thi lệnh nếu điều kiện trả về true. Có thể dùng thêm mệnh đề else để thực thi lệnh nếu điều kiện trả về false. Lệnh if trông như sau:

if (điều_kiện) {
  lệnh_1;
} else {
  lệnh_2;
}

Ở đây, điều_kiện có thể là bất cứ biểu thức nào để đánh giá true hoặc false. Xem Boolean để hiểu cách thức trả về truefalse. Nếu điều_kiện trả về true, lệnh_1 sẽ được thực thi; trái lại, lệnh_2 sẽ được thực thi. lệnh_1lệnh_2 có thể là bất cứ câu lệnh nào, bao gồm các câu lệnh if lồng nhau.

Bạn cũng có thể dùng lệnh else if để có một dãy điều kiện liên tiếp nhau, chẳng hạn như sau:

if (điều_kiện_1) {
  lệnh_1;
} else if (điều_kiện_2) {
  lệnh_2;
} else if (điều_kiện_n) {
  lệnh_n;
} else {
  lệnh_cuối;
} 

Trong trường hợp có nhiều điều kiện, chỉ thực thi các lệnh trong điều kiện đầu tiên trả về true. Để thực thi nhiều lệnh cùng lúc, hãy nhóm chúng lại trong một khối lệnh ({ ... }) . Nói tóm lại, ta luôn nên dùng khối lệnh, nhất là khi lồng trong câu lệnh if:

if (điều_kiện) {
  lệnh_1_nếu_điều_kiện_là_true;
  lệnh_2_nếu_điều_kiện_là_true;
} else {
  lệnh_3_nếu_điều_kiện_là_false;
  lệnh_4_nếu_điều_kiện_là_false;
}
Đừng dùng lệnh gán trong biểu thức điều kiện, bởi vì lệnh gán có thể bị hiểu lầm với so sánh ngang bằng nếu xem sơ qua đoạn code. Chẳng hạn, đừng viết như sau:
// Dễ bị hiểu lầm là "x == y" trong khi ở bên dưới là gán giá trị y cho x
if (x = y) {
  /* tập hợp các câu lệnh */
}

Nếu cần phải gán trong biểu thức điều kiện, hãy bọc chúng lại trong cặp dấu ngoặc tròn, chẳng hạn:

if ((x = y)) {
  /* tập hợp các câu lệnh */
}

Giá trị Falsy

Các giá trị sau sẽ trả về false (còn được gọi là giá trị Falsy):

  • false
  • undefined
  • null
  • 0
  • NaN
  • Chuỗi rỗng ("")

Mọi giá trị khác, bao gồm tất cả object, trả về true khi được truyền vào câu lệnh điều kiện.

Chú ý: Đừng nhầm lẫn giữa giá trị Boolean sơ khai truefalse với giá trị true và false của object Boolean. Chẳng hạn:

var b = new Boolean(false);
if (b) // điều kiện này trả về true
if (b == true) // điều kiện này trả về false

Ví dụ

Trong ví dụ sau, hàm checkData trả về true nếu số lượng ký tự của object Text là 3; trái lại, một thông báo sẽ hiện ra và hàm sẽ trả về false.

function checkData() {
  if (document.form1.threeChar.value.length == 3) {
    return true;
  } else {
    alert('Enter exactly three characters. ' +
    document.form1.threeChar.value + ' is not valid.');
    return false;
  }
}

Lệnh switch

Lệnh switch cho phép một chương trình đánh giá một biểu thức và nổ lực thử nghiệm dần để tìm ra trường hợp giống nhau giữa giá trị của biểu thức đó với case label. Nếu một sự kết hợp được tìm thấy, chương trình sẽ chạy câu lệnh liên quan. Câu lệnh switch như sau:

switch (expression) {
  case label_1:
    statements_1
    [break;]
  case label_2:
    statements_2
    [break;]
    ...
  default:
    statements_def
    [break;]
}
  • Trước hết, chương trình sẽ tìm mệnh đề case có nhãn giống với giá trị của biểu thức và truyền dẫn việc điểu khiển tới mệnh đề đó, thực thi các lệnh liên quan.
  • Nếu không có nhãn nào khớp, chương trình sẽ tìm mệnh đề mặc định default,
    • Nếu tìm thấy mệnh đề mặc định default, chương trình sẽ truyền luồng điểu khiển tới mệnh đề đó, thực thi các lệnh liên quan.
    • Nếu không tìm thấy mệnh đề mặc định default, chương trình tiếp tục thực thi cho tới cuối lệnh switch. (Theo quy ước, mệnh đề default được viết cuối cùng, nhưng không bắt buộc).

Lệnh break

Lệnh tuỳ chọn break liên kết với từng mệnh đề case nhằm đảm bảo chương trình nhảy ra khỏi switch một khi câu lệnh liên quan tại vị trí mệnh đề phù hợp đã thực hiện xong, và sau đó tiếp tục thực hiện câu lệnh phía sau lệnh switch. Nếu không có break, chương trình sẽ thực thi toàn bộ câu lệnh tiếp theo còn lại trong switch mà không cần đánh giá xem giá trị của biểu thức và case label có khớp nhau không.

Ví dụ

Trong ví dụ sau, nếu fruittype được gán giá trị "Bananas", chương trình sẽ khớp với nhãn "Bananas" và thực thi các lệnh đi kèm. Khi chương trình chạy tới break, chương trình ngừng và thực thi các lệnh sau khối lệnh switch. Nếu không có break, lệnh ứng với nhãn "Cherries" cũng sẽ được thực thi.

switch (fruittype) {
  case 'Oranges':
    console.log('Oranges are $0.59 a pound.');
    break;
  case 'Apples':
    console.log('Apples are $0.32 a pound.');
    break;
  case 'Bananas':
    console.log('Bananas are $0.48 a pound.');
    break;
  case 'Cherries':
    console.log('Cherries are $3.00 a pound.');
    break;
  case 'Mangoes':
    console.log('Mangoes are $0.56 a pound.');
    break;
  case 'Papayas':
    console.log('Mangoes and papayas are $2.79 a pound.');
    break;
  default:
   console.log('Sorry, we are out of ' + fruittype + '.');
}
console.log("Is there anything else you'd like?");

Lệnh xử lý ngoại lệ

Bạn có thể quăng ngoại lệ (exeption) bằng lệnh throw và xử lý chúng bằng lệnh try...catch.

Kiểu ngoại lệ

Gần như mọi object đều có thể bị quăng ra trong JavaScript. Tuy vậy, không phải object nào khi quăng ra cũng được tạo ra tương tự nhau. Trong khi thông thường các số và chuỗi được quăng ra để thông báo lỗi, song sử dụng một trong số những kiểu ngoại lệ chuyên biệt cho mục đích này thường hiệu quả hơn:

Lệnh throw

Lệnh throw để quăng ra ngoại lệ. Khi muốn quăng ra ngoại lệ, bạn phải đặc tả biểu thức chứa giá trị để quăng ra:

throw expression;

Bạn có thể quăng ra biểu thức nào cũng được, không chỉ biểu thức dành riêng cho kiểu ngoại lệ. Ví dụ sau đây quăng ra vô số kiểu:

throw 'Error2';   // String type
throw 42;         // Number type
throw true;       // Boolean type
throw {toString: function() { return "I'm an object!"; } };
Ghi chú: Bạn có thể đặc tả object khi quăng ngoại lệ. Rồi đặt tham chiếu tới thuộc tính của object đó trong khối catch.
// Tạo object có kiểu UserException
function UserException(message) {
  this.message = message;
  this.name = 'UserException';
}

// Bắt ngoại lệ in ra dòng lỗi màu mè một tí 
// (như khi in lên console chả hạn)
UserException.prototype.toString = function() {
  return this.name + ': "' + this.message + '"';
}

// Tạo một instance của object rồi quăng ra
throw new UserException('Value too high');

Lệnh try...catch

Lệnh try...catch đánh dấu một khối để thử, và xác định khi nào sẽ quăng ra ngoại lệ. Khi ngoại lệ bị quăng ra, lệnh try...catch sẽ tóm gọn nó.

Lệnh try...catch bao gồm khối try, chứa một hoặc nhiều câu lệnh, và khối catch, chứa các lệnh dành để xử lý khi có ngoại lệ bị quăng ra trong khối try.

Tức là, bạn muốn lệnh trong khối try thành công, và nhỡ nó không thành công, bạn muốn truyền luồng xử lý xuống khối catch. Nếu bất cứ lệnh nào trong khối try (hoặc hàm được gọi trong khối try) quăng ra ngoại lệ, luồng điều khiển sẽ ngay lập tức chuyển xuống khối catch. Nếu không có ngoại lệ nào bị quăng ra trong khối try, khối catch sẽ bị bỏ qua. Khối finally thực thi sau khi khối trycatch thực thi xong nhưng trước các lệnh đặt ngay sau lệnh try...catch.

Ví dụ sau dùng lệnh try...catch. Trong ví dụ này, ta gọi một hàm nhận vào một giá trị và trả về tên tháng tương ứng. Nếu giá trị truyền vào không tương ứng với số tháng (1-12), một ngoại lệ sẽ bị quăng ra có kiểu "InvalidMonthNo" và lệnh trong khối catch sẽ đặt lại giá trị cho biến monthName thành unknown.

function getMonthName(mo) {
  mo = mo - 1; // Chỉnh lại số tháng cho hợp với chỉ số mảng (1 = Jan, 12 = Dec)
  var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
                'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  if (months[mo]) {
    return months[mo];
  } else {
    throw 'InvalidMonthNo'; // dùng từ khoá throw ở đây nhé
  }
}

try { // lệnh để thử
  monthName = getMonthName(myMonth); // hàm để quăng ra ngoại lệ
}
catch (e) {
  monthName = 'unknown';
  logMyErrors(e); // truyền object ngoại lệ vào hàm xử lý lỗi của bạn
}

Khối catch

Bạn có thể dùng khối catch để xử lý tất cả ngoại lệ sinh ra trong khối try.

catch (catchID) {
  statements
}

Khối catch nhận vào một định danh (catchID như trong cú pháp phía trên) giữ giá trị mà lệnh throw quăng ra; bạn có thể dùng định danh này để lấy thông tin về ngoại lệ bị quăng ra. JavaScript tạo ra định danh này khi chương trình chạy vào khối catch; định danh này chỉ tồn tại trong khối catch; sau khi khối catch thực thi xong, định danh đó sẽ không còn tồn tại nữa.

Chẳng hạn, đoạn code sau quăng ra một ngoại lệ. Khi ngoại lệ xảy ra, điều khiển được truyền xuống khối catch.

try {
  throw 'myException'; // sinh ngoại lệ
}
catch (e) {
  // lệnh xử lý ngoại lệ
  logMyErrors(e); // truyền object ngoại lệ xuống hàm xử lý lỗi
}

Thực tiễn tốt nhất: Khi log error ra console bên trong một catch block, hãy sử dụng console.error() thay vì console.log(), nó sẽ định dạng dòng văn bản như là một lỗi, và thêm nó vào danh sách các dòng văn bản lỗi được tạo ra bởi trang. Điều này tốt cho việc debugging.

Khối finally

Khối finally chứa các lệnh thực thi ngay sau khi thực thi khối trycatch nhưng trước các lệnh liền sau lệnh try...catch. Khối finally vẫn thực thi dù có xảy ra ngoại lệ hay không. Nếu ngoại lệ bị quăng ra, các lệnh trong khối finally vẫn thực thi dù khối catch có xử lý ngoại lệ hay không.

Bạn có thể dùng khối finally để khiến mã của bạn lỗi một cách yên bình nhỡ ngoại lệ xảy ra; chả hạn, bạn muốn giải phóng tài nguyên khỏi đoạn mã của mình. Ví dụ sau mở một tệp tin và thực thi lệnh dùng tài nguyên của tệp đó (JavaScript phía server cho phép bạn truy cập vào tệp tin). Nếu có ngoại lệ bị quăng ra trong lúc đang mở tệp tin, khối finally sẽ đóng đoạn tệp tin lại trước khi đoạn mã bị lỗi.

openMyFile();
try {
  writeMyFile(theData); // Ghi vào tệp, có thể có lỗi
} catch(e) {  
  handleError(e); // Nếu có lỗi thì dùng hàm này để xử lý
} finally {
  closeMyFile(); // Luôn luôn đóng tệp lại
}

Nếu khối finally trả về giá trị thì giá trị đó sẽ trở thành giá trị trả về của cả dãy lệnh try-catch-finally, bỏ qua mọi lệnh return trong khối trycatch:

function f() {
  try {
    console.log(0);
    throw 'bogus';
  } catch(e) {
    console.log(1);
    return true; // lệnh return ở đây bị tạm ngưng
                 // cho tới khi thực thi xong khối finally
    console.log(2); // không thực thi tới
  } finally {
    console.log(3);
    return false; // ghi đè lệnh "return" phía trên
    console.log(4); // không thực thi tới
  }
  // "return false" sẽ thực thi ngay lúc này  
  console.log(5); // không thực thi tới
}
console.log(f()); // 0, 1, 3, false 

Ghi đè giá trị trả về bằng khối finally cũng áp dụng với các ngoại lệ bị quăng ra trong khối catch:

function f() {
  try {
    throw 'bogus';
  } catch(e) {
    console.log('caught inner "bogus"');
    throw e; // lệnh thow này bị tạm ngưng 
             // cho tới khi thực thi xong khối finally
  } finally {
    return false; // ghi đè lệnh "throw" phía trên
  }
  // "return false" sẽ thực thi ngay lúc này
}

try {
  console.log(f());
} catch(e) {
  // khối này sẽ không bao giờ tới được
  // bởi vì khối catch phía trên đã bị ghi đè
  // bởi lệnh return trong finally
  console.log('caught outer "bogus"');
}

// ĐẦU RA
// caught inner "bogus"
// false

Lồng lệnh try...catch

Bạn có thể lồng một hoặc nhiều lệnh try...catch. Nếu lệnh try...catch bên trong không có khối catch :

  1. Nó nên có khối finally
  2. Lệnh try...catch bọc bên ngoài phải bắt được cái gì đấy. Để biết thêm thông tin, hãy đọc lồng khối try trên trang try...catch.

Tận dụng object Error

Tuỳ theo kiểu của lỗi, bạn có thể dùng thuộc tính 'name' và 'message' để lấy ra thông điệp lỗi rõ ràng hơn. 'name' lấy ra class chung của Error (tức là 'DOMException' hoặc 'Error'), trong khi 'message' thường lấy ra thông điệp về lỗi súc tích hơn thông điệp tạo ra bởi ép object lỗi thành xâu ký tự.

Nếu bạn muốn quăng ra ngoại lệ của riêng mình, để tận dụng được những thuộc tính này (ví dụ như khi khối catch không phân biệt giữa ngoại lệ của bạn và của hệ thống), bạn có thể dùng hàm khởi tạo Error. Chẳng hạn:

function doSomethingErrorProne() {
  if (ourCodeMakesAMistake()) {
    throw (new Error('The message'));
  } else {
    doSomethingToGetAJavascriptError();
  }
}
....
try {
  doSomethingErrorProne();
} catch (e) {
  console.log(e.name); // logs 'Error'
  console.log(e.message); // logs 'The message' or a JavaScript error message
}