Độ ưu tiên các toán tử trong JavaScript

Biểu thức điều kiện là một trong những khía cạnh rất quan trong trong mọi ngôn ngữ lập trình. Chúng ta đã quen với các mệnh đề điều kiện như

(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
6 hay
(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
7. Chúng là những cú pháp hữu dụng để tạo ra những quyết định trong lập trình.

Bài viết này sẽ tập trung vào các biểu thức điều kiện trong Javascript và cách sử dụng chúng một cách ngắn gọn so với các mệnh đề điều kiện.

Biểu thức và Mệnh đề (Expressions vs Statements)

Trước khi đi vào nội dung chính, chúng ta cần phân biệt giữa biểu thức và mệnh đề trong Javascript. Nếu coi Javascript là một ngôn ngữ có ngữ pháp thì biểu thức chính là cụm từ, trong khi đó mệnh đề là một câu hoàn chỉnh.

Biểu thức có thể là bất kỳ thể hiện nào mà Javascript engine có thể tính toán và trả về một giá trị. Chẳng hạn như: thể hiện của biến, phép gán, biểu thức hàm, phép logic, toán tử bitwise, phép truy cập thuộc tính của đối tượng, lời gọi hàm, eval, ....

Đoạn code dưới đây chỉ ra một số biểu thức Javascript:

// number literal
0xFF

// array literal
[]

// object literal
{}

// regexp literal
/^\d+$/

// logical AND operation
(x && y)

// bitwise XOR operation
(x ^ y)

// ternary operation
(x ? y : z)

// arithmetic operation
(x + y) / z

// assignment
x = 'string'

// function expression
(function x(y) {})

// function invocation
x(100)

// object property access
obj.students[0].name

Mệnh đề là bất kỳ thể hiện hay câu lệnh nào mà Javascript engine có thể thực thi để chạy chương trình hoặc gây ra những tác động kèm theo khác. Ví dụ: mệnh đề điều kiện, khai báo biến hoặc hàm, vòng lặp, throw, return, try/catch/finally, ...

Một số biểu thức Javascript như phép gán và lời gọi hàm có thể gây ra những tác động kèm theo. Do đó, chúng lại được coi là một mệnh đề (mệnh đề biểu thức).

Phép điều kiện và giá trị kiểu boolean

Trong Javascript, phép điều kiện có thể là bất kỳ biểu thức hợp lệ nào. Thông thường, biểu thức điều kiện sẽ được tính toán để trả về một trong hai giá trị kiểu booleans:

(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
8 hoặc
(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
9

Việc hiểu đúng cách mà Javascript engine chuyển đổi biểu thức điều kiện về giá trị boolean là điều cần thiết để viết các phép logic điều kiện một cách chính xác và khả dự.

Hai khái niệm cơ bản có thể hiểu về phép chuyển đổi này:

  • Xác định giá trị truthy and falsy
  • Hiểu về phép logic short-circuiting

Truthy vs Falsy

Mọi giá trị trong Javascript đều có thể phân loại thành truthy hay falsy. Những giá trị sau được coi là falsy trong Javascript.

  • (a || b) === a; // `a` is truthy
    (a || b) === b; // `a` is falsy
    
    0 hoặc
    (a || b) === a; // `a` is truthy
    (a || b) === b; // `a` is falsy
    
    1 hoặc
    (a || b) === a; // `a` is truthy
    (a || b) === b; // `a` is falsy
    
    2 (chuỗi rỗng)
  • (a || b) === a; // `a` is truthy
    (a || b) === b; // `a` is falsy
    
    3 hoặc
    (a || b) === a; // `a` is truthy
    (a || b) === b; // `a` is falsy
    
    4 (số
    (a || b) === a; // `a` is truthy
    (a || b) === b; // `a` is falsy
    
    3)
  • (a || b) === a; // `a` is truthy
    (a || b) === b; // `a` is falsy
    
    6
  • (a || b) === a; // `a` is truthy
    (a || b) === b; // `a` is falsy
    
    7
  • (a || b) === a; // `a` is truthy
    (a || b) === b; // `a` is falsy
    
    8
  • (a && b) === a; // `a` is falsy
    (a && b) === b; // `a` is truthy
    
    9

Những giá trị khác các giá trị trên sẽ được coi là truthy. Các giá trị truthy sẽ được ép kiểu ngầm định thành giá trị

(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
8. Trong khi giá trị falsy sẽ trả về giá trị
(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
9.

Tuy nhiên, việc chuyển đổi này có thể được khai báo tường minh nhờ hàm Boolean.

function toBoolean(value) {
  return Boolean(value);
}

Ngoài ra, toán tử logic NOT (

if (user && user.canDeletePost) {
  deletePost();
}
2) cũng chuyển đổi một giá trị về kiểu boolean. Toán tử
if (user && user.canDeletePost) {
  deletePost();
}
2 chuyển đổi toán hạng của nó thành giá trị phủ định ở kiểu boolean. Do đó, giá trị này luôn là giá trị boolean.

Sử dụng toán tử

if (user && user.canDeletePost) {
  deletePost();
}
2 sẽ trả về
(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
9 trên những giá trị truthy và
(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
8 trên những giá trị falsy. Để chuyển về giá trị boolean tương ứng, chúng ta cần sử dụng
if (user && user.canDeletePost) {
  deletePost();
}
2 hai lần.

function toBoolean(value) {
  return !!value;
}

Short-Circuiting (Mạch chập)

Toán tử AND (

if (user && user.canDeletePost) {
  deletePost();
}
8) và OR (
if (user && user.canDeletePost) {
  deletePost();
}
9) đều yêu cầu hai toán hạng và được sử dụng để thực hiện phép toán Boolean trên những toán hạng này.

Cho hai toán hạng kiểu boolean (

(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
8 hoặc
(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
9)

  • if (user && user.canDeletePost) {
      deletePost();
    }
    
    8 chỉ trả về
    (a && b) === a; // `a` is falsy
    (a && b) === b; // `a` is truthy
    
    8 nếu cả hai toán hạng có giá trị kiểu boolean là
    (a && b) === a; // `a` is falsy
    (a && b) === b; // `a` is truthy
    
    8, nếu không nó sẽ trả về
    user && user.canDeletePost && deletePost();
    
    5.
  • if (user && user.canDeletePost) {
      deletePost();
    }
    
    9 chỉ trả về
    (a && b) === a; // `a` is falsy
    (a && b) === b; // `a` is truthy
    
    9 nếu cả hai toán hạng có giá trị kiểu boolean là
    (a && b) === a; // `a` is falsy
    (a && b) === b; // `a` is truthy
    
    9, nếu không nó sẽ trả về
    (a && b) === a; // `a` is falsy
    (a && b) === b; // `a` is truthy
    
    8.

Chú ý rằng toán tử

if (user && user.canDeletePost) {
  deletePost();
}
8 có độ ưu tiên hơn
if (user && user.canDeletePost) {
  deletePost();
}
9. Do đó, nó sẽ được tính toán trước. Khi sử dụng cả hai toán tử này trong cùng một biểu thức, có thể sử dụng dấu ngoặc
let strength = null;

if (password.length > 7) {
  strength = 'Strong';
} else {
  strength = 'Weak';
}
2 để nhóm các phép tính theo thứ tự ưu tiên.

false && true || true; // true
false && (true || true); // false

Khi sử dụng những toán tử này, toán hạng đầu tiên luôn được tính toán trước. Tuy nhiên, toán hạng thứ hai có thể không bao giờ được sử dụng tùy theo kết quả tính toán của toán hạng đầu tiên. Điều này được gọi là short-circuiting (mạnh chập hay đoản mạch).

Toán tử

if (user && user.canDeletePost) {
  deletePost();
}
8 và
if (user && user.canDeletePost) {
  deletePost();
}
9 không phải lúc nào cũng trả về giá trị kiểu boolean. Thông thường, chúng có thể trả về bất kỳ giá trị nào. Dưới đây là mô tả chính xác về tính đoản mạch của chúng:

  • Toán tử
    if (user && user.canDeletePost) {
      deletePost();
    }
    
    8 sẽ kiểm tra toán hạng đầu tiên. Nếu kết quả là truthy thì toán hạng sẽ được tính toán và kết quả trả về là giá trị của toán hạng thứ hai. Tuy nhiên nếu giá trị của toán hạng đầu tiên là falsy thì toán hạng thứ hai không bao giờ được tính toán, kết quả trả về là giá trị falsy của toán hạng đầu.
(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
  • Toán tử
    if (user && user.canDeletePost) {
      deletePost();
    }
    
    9 cũng sẽ kiểm tra toán hạng đầu tiên. Nếu kết quả là truthy thì toán hạng thứ hai sẽ không bao giờ được tính toán, kết quả trả về là giá trị truthy từ toán hạng đầu tiên. Tuy nhiên, nếu giá trị của toán hạng đầu là falsy thì kết quả trả về là giá trị của toán hạng thứ hai.
(a || b) === a; // `a` is truthy
(a || b) === b; // `a` is falsy
Thay thế mệnh đề bằng biểu thức

1. Đơn giản hóa mệnh đề let strength = null; if (password.length > 7) { strength = 'Strong'; } else { strength = 'Weak'; } 7

Rất nhiều câu điện kiện

let strength = null;

if (password.length > 7) {
  strength = 'Strong';
} else {
  strength = 'Weak';
}
8 có thể dễ dàng được thay thế bởi biểu thức điều kiện bằng cách áp dụng khái niệm mạch chập. Xem xét ví dụ sau:

if (user && user.canDeletePost) {
  deletePost();
}

Trong đoạn code này, mệnh đề

let strength = null;

if (password.length > 7) {
  strength = 'Strong';
} else {
  strength = 'Weak';
}
8 đảm bảo rằng hàm
const strength = (password.length > 7) && 'Strong' || 'Weak';
0 chỉ được gọi khi phép điều kiện trả về
(a && b) === a; // `a` is falsy
(a && b) === b; // `a` is truthy
8

Mệnh đề

let strength = null;

if (password.length > 7) {
  strength = 'Strong';
} else {
  strength = 'Weak';
}
8 trên có thể được thay thế bằng biểu thức điều kiện rất đơn giản như sau:

user && user.canDeletePost && deletePost();

Mặc dù biểu thức điều kiện này có cách thực thi giống với mệnh đề điều kiện trên, nhưng thực chất chúng khác nhau.

Mệnh đề điều kiện trả về một giá trị. Do đó, nó có thể được gán cho một biến hoặc được sử dụng ở nơi khác mà yêu cầu một giá trị cụ thể.

Việc sử dụng biểu thức điều kiện như này đồng nghĩa với việc phải rất cận trọng về khái niệm mạch chập. Rất có thể toán hạng không được thực thi như đã đề cập ở mục trước của bài viết về mạch chập.

2. Mệnh đề If...Else

Xem xét ví dụ đơn giản sau để xác định độ mạnh của mật khẩu:

let strength = null;

if (password.length > 7) {
  strength = 'Strong';
} else {
  strength = 'Weak';
}

Ý tưởng của đoạn code trên rất đơn giản: Kiểm tra nếu mật khẩu có độ dài lớn hơn 7 ký tự thì gán trị cho biến là

const strength = (password.length > 7) && 'Strong' || 'Weak';
3 , ngược lại gán à
const strength = (password.length > 7) && 'Strong' || 'Weak';
4

Đoạn code trước có thể được rút gọn như sau:

const strength = (password.length > 7) && 'Strong' || 'Weak';

Đoạn code này thực hiện giống hệt như đoạn code lúc trước, tất cả chỉ trong một dòng. Điều này trông khá ổn. Đoạn code dưới đây sẽ giải thích cơ chế tính toán của biểu thức điều kiện ở đoạn code trên.

function toBoolean(value) {
  return Boolean(value);
}
0

Có một cách khác để viết lại những biểu thức điện kiện

const strength = (password.length > 7) && 'Strong' || 'Weak';
5 là sử dụng toán tử điều kiện, hay còn được gọi là toán tử ba ngôi (ternary operator). Cú pháp như sau:

function toBoolean(value) {
  return Boolean(value);
}
1

Đoạn code kiểm tra mật khẩu lúc trước có thể được viết lại sử dụng toán tử ba ngôi như sau:

function toBoolean(value) {
  return Boolean(value);
}
2

Mặc dùng trong ví dụ này, toán tử ba ngôi và toán tử logic hoạt động giống nhau nhưng cần nhớ rằng chúng không thể thay thế được cho nhau.

Tốt hơn hết là nên sử dụng toán tử ba ngôi trong những trường hợp không biết rõ về toán hạng sẽ được thực hiện.

Xem xét đoạn code sau để hiểu về sự nguy hiểm của việc sử dụng toán tử logic cho những trường hợp như này:

function toBoolean(value) {
  return Boolean(value);
}
3

Dưới đây là một mệnh đề rất quen thuộc thường được tìm thấy ở các thư viện AJAX đa nền tảng:

function toBoolean(value) {
  return Boolean(value);
}
4

Sử dụng toán tử logic:

function toBoolean(value) {
  return Boolean(value);
}
5

Sử dụng toán tử ba ngôi:

function toBoolean(value) {
  return Boolean(value);
}
6Mẹo nhỏ và cú pháp viết tắt.

Dưới đây là một số mẹo và cú pháp viết tắt hữu dụng khi sử dụng các toán tử logic và toán tử điều kiện:

Chuẩn hóa về kiểu Boolean

Cho giá trị

const strength = (password.length > 7) && 'Strong' || 'Weak';
6 cần chuẩn hóa để luôn trả về giá trị kiểu boolean theo quy tắc sau:

  • Nếu
    const strength = (password.length > 7) && 'Strong' || 'Weak';
    
    6 là giá trị boolean thì trả về giá trị
    const strength = (password.length > 7) && 'Strong' || 'Weak';
    
    6
  • Nếu
    const strength = (password.length > 7) && 'Strong' || 'Weak';
    
    6 không phải giá trị boolean , giá trị boolean mặc định sẽ được trả về (
    (a && b) === a; // `a` is falsy
    (a && b) === b; // `a` is truthy
    
    8 hoặc
    user && user.canDeletePost && deletePost();
    
    5)

Đoạn code dưới đây mô tả phép chuẩn hóa trên:

function toBoolean(value) {
  return Boolean(value);
}
7

Định lý De Morgan

Định lý toán học quen thuộc này có thể được mô tả như sau:

function toBoolean(value) {
  return Boolean(value);
}
8

Sự đồng nhất của Boolean

Khi xử lý với giá trị boolean, có một số phép đồng nhất luôn đúng. Cho

function toBoolean(value) {
  return Boolean(value);
}
02,
function toBoolean(value) {
  return Boolean(value);
}
03 và
function toBoolean(value) {
  return Boolean(value);
}
04 là những giá trị boolean, đoạn code sau sẽ chỉ ra sự đồng nhất này:

function toBoolean(value) {
  return Boolean(value);
}
9

Những toán tử ba ngôi phức hợp

Như đã đề cập ở các phần trước của bài viết, toán tử ba ngôi có thể được lồng với nhau để xử lý những đoạn logic liên quan đến mệnh đề

const strength = (password.length > 7) && 'Strong' || 'Weak';
5

Tuy nhiên, để có thể sử dụng chúng hiệu quả trong những biểu thức phức hợp thì cần hiểu về độ ưu tiên và tính kết hợp của toán tử ba ngôi.

  • Toán tử ba ngôi có độ ưu tiên thấp nhất so với các toán tử khác. Do đó nó được tính toán cuối cùng khi được sử dụng chung với các toán tử khác có độ ưu tiên cao hơn.
function toBoolean(value) {
  return !!value;
}
0
  • Toán tử ba ngôi có tính kết hợp từ phải qua trái. Do đó, đối với các toán tử ba ngôi được sử dụng trong cùng một biểu thức, chúng được phân tích từ phải sang trái.
function toBoolean(value) {
  return !!value;
}
1

Khi sử dụng nhiều toán tử ba ngôi trong cùng một biểu thức, dấu ngoặc

let strength = null;

if (password.length > 7) {
  strength = 'Strong';
} else {
  strength = 'Weak';
}
2 có thể cần được sử dụng để thay đổi thứ tự tính toán. Ví dụ: