Học bằng cách đọc theo thứ tự

Khai báo hàm và biểu thức hàm — tham số và return

Học cách khai báo hàm bằng function, truyền đối số và nhận giá trị qua return, biểu thức hàm, và vì sao quên return thì hàm trả về undefined.

Nếu bạn viết thẳng công thức tính giá đã gồm thuế ở cả 3 nơi — danh sách sản phẩm, giỏ hàng và màn hình xác nhận đơn — thì khi thuế suất thay đổi, bạn phải sửa cả 3 chỗ, và màn hình nào bị sót sẽ vẫn hiện số tiền cũ.

Bài này nói về khai báo function để đặt tên cho một đoạn xử lý rồi gọi nó, biểu thức hàm để gán hàm vào biến như một giá trị, và giá trị mà return trả về.

Đặt tên cho đoạn xử lý để gọi lại — khai báo function và tham số

Cách tính giá đã gồm thuế cho từng sản phẩm lần nào cũng giống nhau, chỉ có đơn giá là khác. Vì vậy, bạn có thể đặt tên cho chính phép tính đó, rồi ở danh sách hay giỏ hàng chỉ việc gọi tên ấy.

Hàm (function — một khối code được đặt tên để bạn gọi lại bao nhiêu lần cũng được) được khai báo theo dạng function tên(tham số) { thân hàm }. Tham số viết trong cặp ngoặc tròn là biến nhận một giá trị ở mỗi lần gọi, còn giá trị bạn viết sau return (giá trị trả về) sẽ được trả về cho bên gọi.

// Đặt tên taxIncluded cho đoạn xử lý tính giá đã gồm thuế
function taxIncluded(price) {
  const tax = Math.floor(price * 0.1);
  return price + tax;             // Trả giá trị vừa tính về cho bên gọi
}

// Gọi cùng một hàm với các đơn giá khác nhau
console.log(taxIncluded(1200));   // 1320
console.log(taxIncluded(980));    // 1078

// Nếu quên truyền giá trị, price sẽ không nhận được gì
console.log(taxIncluded());       // NaN
Ba lần gọi khác nhau cho cùng một hàm
Định nghĩataxIncluded(price)taxIncluded(1200)taxIncluded(980)taxIncluded()price là 1200tax là 120price là 980tax là 98price là undefinedtax là NaNTrả về 1320Trả về 1078Trả về NaNtruyền giá trịtruyền giá trịkhông truyền gì
Giá trị đưa vào price thay đổi thì giá trị trả về cũng thay đổi theo. Ở lần gọi không truyền giá trị, price là undefined và kết quả là NaN.

price chỉ nhận giá trị, nên dù bạn viết thẳng 1200 hay truyền một biến như itemPrice, hàm vẫn chạy y như nhau.

Hiển thị thời lượng khóa đào tạo theo dạng "◯ giờ ◯ phút". lectureMinutes và practiceMinutes đã được khai báo sẵn.

① Hãy khai báo một hàm nhận số phút và trả về chuỗi dạng "◯ giờ ◯ phút".

② Hãy hiển thị thời lượng phần lý thuyết theo dạng "Lý thuyết: ◯ giờ ◯ phút".

③ Hãy hiển thị thời lượng phần thực hành theo dạng "Thực hành: ◯ giờ ◯ phút".

④ Hãy hiển thị tổng thời lượng của cả hai theo dạng "Tổng: ◯ giờ ◯ phút".

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Nhận nhiều đối số — khớp theo vị trí

Khi tỷ lệ giảm giá thay đổi theo hạng thành viên hay theo đợt khuyến mãi, một hàm chỉ nhận đơn giá là không đủ. Nếu bên gọi truyền được cả tỷ lệ giảm giá, phép tính vẫn chỉ nằm ở một chỗ, và tỷ lệ thì do bên gọi quyết định.

Các tham số được ngăn cách bằng dấu phẩy. Các giá trị trong lời gọi được gán vào tham số theo đúng thứ tự bạn viết: giá trị thứ nhất vào tham số thứ nhất, giá trị thứ hai vào tham số thứ hai. JavaScript không khớp theo tên, nên dù bạn truyền nhầm thứ tự, code vẫn đúng cú pháp và không có lỗi nào được báo.

// Nhận đơn giá và tỷ lệ giảm giá, trả về giá sau khi giảm
function applyDiscount(price, rate) {
  return Math.floor(price * (1 - rate));
}

// Giá trị thứ nhất vào price, giá trị thứ hai vào rate
console.log(applyDiscount(2400, 0.2));    // 1920
console.log(applyDiscount(2400, 0.05));   // 2280

// Dù đảo thứ tự, phép tính vẫn cứ chạy
console.log(applyDiscount(0.2, 2400));    // -480
Đối số được khớp theo vị trí
Truyền(2400, 0.2)price là 2400rate là 0.2Tính2400 × 0.8Trả về 1920Truyền(0.2, 2400)price là 0.2rate là 2400Tính0.2 × -2399Trả về -480
Kết quả khi truyền cùng hai giá trị vào applyDiscount, chỉ đổi thứ tự. Đảo thứ tự không gây lỗi, chỉ có kết quả là thay đổi.

Dòng gọi hàm chỉ có các giá trị, nên nhìn applyDiscount(0.2, 2400) bạn sẽ không nhận ra chỗ nhầm. Khi nhận về một giá trị vô lý như -480, hãy đối chiếu thứ tự các giá trị trong lời gọi với định nghĩa của hàm.

Tính số tiền phải trả của một đơn hàng bằng hai hàm. unitPrice, quantity, shippingFee và couponAmount đã được khai báo sẵn.

① Hãy khai báo một hàm nhận đơn giá và số lượng rồi trả về tạm tính.

② Hãy khai báo một hàm nhận lần lượt tạm tính, phí vận chuyển và số tiền giảm giá, rồi trả về số tiền phải trả.

③ Hãy truyền giá trị trả về của ① vào ② và hiển thị "Số tiền phải trả: ◯ yên".

④ Hãy đặt phí vận chuyển là 0 và hiển thị "Nếu miễn phí vận chuyển: ◯ yên".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Dùng hàm như một giá trị — biểu thức hàm và gán vào biến

Giao hàng thường và giao hàng nhanh tính phí vận chuyển bằng hai công thức hoàn toàn khác nhau. Thay vì ở mỗi chỗ gọi đều phải chỉ rõ dùng công thức nào, bạn có thể gán sẵn hàm cần dùng vào một biến, rồi bên gọi chỉ việc gọi biến đó.

Biểu thức hàm (function expression — cách gán một hàm vào biến như một giá trị) là một lệnh gán, viết theo dạng const tên = function (tham số) { thân hàm };. Khai báo function đã được chuẩn bị sẵn trước khi code bắt đầu chạy từ trên xuống, còn biến của biểu thức hàm chỉ nhận được hàm khi dòng gán được chạy (cơ chế này được giải thích trong bài về scope và hoisting).

// Khai báo function có thể được gọi ngay cả từ những dòng nằm phía trên nó
console.log(expressShipping(1.5));       // 900

function expressShipping(weight) {
  return weight <= 2 ? 900 : 1300;
}

// Biểu thức hàm: gán hàm vào biến như một giá trị
const normalShipping = function (weight) {
  return weight <= 2 ? 500 : 800;
};

// Viết tên hàm không kèm ngoặc tròn thì chính hàm đó được gán sang biến khác
const feeRule = normalShipping;
console.log(feeRule(3));                 // 800
console.log(typeof feeRule);             // function
console.log(typeof normalShipping(3));   // number
Có gọi được từ dòng phía trên không?
Khai báo bằngfunctionGọi ở dòngtrước khai báoSẵn sàng trướckhi code chạyTrả về 900Gán vào biến bằngbiểu thức hàmGọi ở dòngtrước lệnh gánBiến chưanhận được hàmBáo lỗiReferenceError
Nếu gọi ở dòng nằm trước lệnh gán, chương trình dừng lại và báo ReferenceError. Hàm được gán bằng biểu thức hàm chỉ gọi được sau khi dòng gán đã chạy.

Thông báo lỗi đầy đủ là ReferenceError: Cannot access 'normalShipping' before initialization. Ngoài ra, hãy để ý dòng feeRule trong đoạn code trên: khi gán tên hàm mà không kèm ngoặc tròn, chính hàm đó được đưa sang một biến khác.

Có ngoặc tròn và không có ngoặc tròn
feeRule =normalShippingBiến nhậnchính hàm đótypeof làfunctionGọi về sau bằngfeeRule(3)feeRule =normalShipping(3)Thân hàm chạy,biến nhận 800typeof lànumberfeeRule(3) báoTypeError
Không có ngoặc tròn thì biến nhận chính hàm đó, có ngoặc tròn thì biến nhận kết quả sau khi chạy hàm. Nếu lỡ thêm ngoặc tròn vào hàm mà bạn định gọi về sau, biến chỉ còn giữ giá trị trả về của nó.

Khai báo trùng tên thì hàm viết sau được dùng

Vì khai báo function được chuẩn bị sẵn trước khi code chạy, nếu bạn khai báo hai hàm cùng tên thì ngay cả lời gọi ở dòng ngay sau hàm thứ nhất cũng chạy thân của hàm viết sau. Chương trình không dừng lại, nên một hàm trùng tên nằm ở chỗ xa trong file rất dễ bị bỏ sót. Với biểu thức hàm dùng const, khai báo lần thứ hai sẽ gây SyntaxError và code không chạy.

Chuyển đổi cách tính phí thanh toán theo loại thành viên. amount và isPremium đã được khai báo sẵn.

① Hãy viết standardFee trả về phí 3% và premiumFee trả về phí 1% bằng biểu thức hàm (làm tròn xuống).

② Hãy dùng isPremium để chọn một trong hai hàm và gán vào applyFee.

③ Hãy gọi applyFee và hiển thị "Phí: ◯ yên".

④ Hãy gọi trực tiếp standardFee và hiển thị "Nếu là thành viên thường: ◯ yên".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Khi không có giá trị trả về — quên viết return

Nếu trong hàm có console.log, khi gọi hàm thì chữ sẽ hiện lên màn hình. Vì có kết quả hiện ra nên trông như hàm đã chạy đúng, nhưng khi lưu giá trị trả về vào một biến để dùng, bạn sẽ thấy trong đó không có chuỗi mình mong đợi.

return vừa trả về một giá trị, vừa kết thúc hàm ngay tại đó. Hàm không có return sẽ trả về undefined — hiển thị ra màn hình và trả về giá trị là hai việc khác nhau. console.log chỉ in ra màn hình, không đưa gì về cho bên gọi.

// Hàm có return: chuỗi được trả về cho bên gọi
function formatLabel(name, stock) {
  return `${name} (còn ${stock} cái)`;
  console.log("Dòng này không chạy tới");   // Nằm sau return nên không được chạy
}

// Hàm không có return: vẫn in ra màn hình, nhưng giá trị trả về là undefined
function printLabel(name, stock) {
  console.log(`${name} (còn ${stock} cái)`);
}

const label = formatLabel("Đèn bàn", 3);
console.log(label);      // Đèn bàn (còn 3 cái)

const printed = printLabel("Hub USB", 5);    // Hub USB (còn 5 cái)
console.log(printed);    // undefined
Cách viết hàmĐiều xảy ra khi gọiBên gọi nhận được
Trả chuỗi về bằng returnChuỗi được trả về cho bên gọiĐèn bàn (còn 3 cái)
Chỉ có console.logIn ra nhưng không trả về gìundefined
console.log đặt sau returnDòng sau return không chạyChuỗi vẫn được trả về

Nếu bên gọi sẽ dùng đến giá trị, hãy trả nó về bằng return thay vì hiển thị ra màn hình.

Xuống dòng ngay sau return thì không có giá trị nào được trả về

Nếu bạn xuống dòng ngay sau return rồi viết giá trị ở dòng tiếp theo, JavaScript coi câu lệnh đã kết thúc ở dòng return, và hàm trả về undefined. Lỗi này dễ gặp khi bạn tách một template literal dài xuống dòng riêng cho dễ đọc. Hãy luôn bắt đầu viết giá trị trên cùng dòng với return.

Cộng phí vận chuyển để ra số tiền phải trả. orderTotal, largeOrderTotal và shippingFee (hàm lẽ ra phải trả về phí vận chuyển) đã được khai báo sẵn.

① Hãy hiển thị orderTotal cộng phí vận chuyển theo dạng "Số tiền phải trả: ◯ yên".

② Hãy hiển thị largeOrderTotal theo cùng dạng đó.

③ Hãy khai báo shippingFeeFixed trả về phí vận chuyển dưới dạng số với mọi số tiền.

④ Hãy hiển thị lại kết quả của ① bằng shippingFeeFixed.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Nếu khai báo function double(n) { n * 2; } rồi chạy console.log(double(4));, màn hình sẽ hiển thị gì?

Câu 2Khi gọi function fee(price, rate) { return price * rate; } theo dạng fee(0.2, 1000), price sẽ nhận giá trị nào?

Câu 3Điều gì xảy ra nếu bạn gọi calc(1) ở một dòng nằm trước lệnh gán const calc = function (n) { return n + 1; };?