Tìm hiểu function JavaScript

Posted on May 11th, 2017

Function JavaScript (hàm số) là một thành phần không thể thiếu trong cấu trúc chương trình. Function giúp cho chương trình trở nên rõ ràng, dễ hiểu bằng cách gộp những đoạn code lặp lại thành một hàm số. Nhờ vậy mà việc bảo trì phần mềm cũng dễ dàng hơn.

Ngoài ra, khi đã xây dựng được một hàm số tốt (độc lập và thực hiện một chức năng nhất định), bạn có thể tái sử dụng nó trong các project khác nhau. Điều này làm nâng cao hiệu suất công việc của bạn.

Bài viết này, mình sẽ giới thiệu những thành phần cơ bản của hàm số trong JavaScript. Qua đó, bạn sẽ biết cách xây dựng hàm số và ứng dụng vào các project của mình.

Định nghĩa function JavaScript

Để định nghĩa hàm trong JavaScript, bạn phải sử dụng từ khoá function. Cấu trúc chung của function là:

function functionName([parameter1], [parameter2],...){
  statement1;
  statement2;
  ...
}

Hoặc bạn có thể sử dụng một biến và gán giá trị cho nó là nội dung function. Khi đó, tên biến số cũng chính là tên của hàm số.

var param = function([parameter1], [parameter2],...){
  statement1;
  statement2;
  ...
};

Một số đặc điểm của function JavaScript là:

  • Hàm số có thể không có tham số, 1 tham số hay thậm chí là nhiều tham số
  • Nội dung của hàm phải được đặt trong cặp dấu ngoặc nhọn ({}), dù cho không có câu lệnh nào hoặc nhiều câu lệnh bên trong nó.
  • Hàm số có thể có giá trị trả về hoặc không có giá trị trả về.
  • Kiểu của tham số hay giá trị trả về trong hàm số là không được quy định. Do đó, bạn thường phải kiểm tra kiểu của giá trị truyền vào hàm. Nếu không, bạn sẽ rất dễ bị sai.
var greet = function(name){
  console.log("Hello " + name + '!');
};

greet('Lam Pham'); // => Hello Lam Pham!

function min(a, b){
  return (a < b ? a : b);
}

console.log(min(3, 4)); // => 3

Tham số và phạm vi (scope)

Tham số của hàm giống như là một biến số với giá trị của nó được khởi tạo khi function được gọi.

Một đặc điểm quan trọng của function JavaScript là tham số và biến số khởi tạo trong hàm số có phạm vi cục bộ (local). Nghĩa là nó chỉ có ý nghĩa và chỉ được truy cập ở trong hàm. Hãy xem ví dụ sau:

function increase(number){
  var x = 5;
  number += x;
  console.log('number inside function:' + number);
}

var number = 3;
increase(number);
// => number inside function: 8

console.log(number);
// => 3 (không thay đổi)

console.log(x);
// => Uncaught ReferenceError: x is not defined

Bạn có thể thấy rằng là: giá trị của number trong function là 8. Tuy nhiên, giá trị của nó ở bên ngoài hàm vẫn là 3. Và biến x được khai báo ở trong hàm, nhưng lại không được nhìn thấy phía ngoài hàm.

Tính chất này có ưu điểm là: khi bạn khai báo biến số ở trong hàm, bạn không cần quan tâm xem nó có trùng với biến số nào khác ở bên ngoài không. Bạn hoàn toàn có thể đặt tên chúng giống nhau mà không có chút xung đột nào.

Nested scope

Ngoài biến số ra, bạn còn có thể khai báo function JavaScript ở trong một hàm số khác. Tính chất này gọi là nested scope. Tạm dịch là "phạm vi lồng vào nhau" (thực tế, mình không biết dịch thế nào cho hợp lý, nên tốt nhất là dùng từ tiếng anh cho chuẩn bài).

Tương tự như biến số, nếu bạn sử dụng hàm nested scope, thì những hàm này cũng có phạm vi sử dụng cục bộ trong hàm chứa nó. Bạn không thể gọi những hàm này ở global.

function displayMax(a, b, c, d){
  var max = function(m, n){
  if(m > n) return m;
  return n;
  }

  var t1 = max(a, b);
  var t2 = max(c, d);
  var t3 = max(t1, t2);

  console.log(t3);
}

displayMax(3, 5, 4, 10); // => 10

var t = max(10, 11); // => Uncaught ReferenceError: max is not defined

Ở ví dụ trên, hàm displayMax chứa hàm max. Do đó, hàm max có thể được gọi bên trong hàm displayMax. Nhưng khi mình gọi hàm max ở phạm vi global thì nó báo lỗi hàm max không được định nghĩa.

Functions và values

Như mình đã đề cập trong bài viết trước, function JavaScript là một loại value. Hay nói cách khác: tên hàm là biến số và nội dung hàm là giá trị của biến số.

Nghĩa là bạn có thể gán giá trị khác cho biến số đó bằng cách:

  • Định nghĩa lại hàm số khác (trường hợp này khiến mình liên tưởng đến con trỏ hàm trong C/C++)
  • Gán cho biến số đó một giá trị khác như: number, string, boolean...
var minValue = function(a, b){
  if(a < b) return a;
  return b;
}
console.log(minValue(4, 6));
// => 4

minValue = function(a, b, c){
  var min = a;
  if(b < min) min = b;
  if(c < min) min = c;
  return min;
}
console.log(minValue(3, 5, 8));
// => 3

minValue = 0;
console.log(minValue);
// => 0

Hoặc bạn có thể sử dụng hàm số trong biểu thức (expression):

function sum(a, b){
  return a + b;
}

function hello(){
  console.log('Hello');
}

var a = 1, b = 2;
var x = 5 + sum(1, 2);
console.log(x);
// => 8

var z = hello();
// => Hello
console.log(z);
// => underfined

Trường hợp, function JavaScript không có giá trị trả về thì mặc định giá trị trả về là underfined (hàm hello() ở ví dụ trên).

Vị trí khai báo function JavaScript

Chú ý rằng: nếu bạn sử dụng hàm số theo ví dụ trên thì chương trình vẫn sẽ thực hiện tuần tự từ trên xuống dưới.

Tuy nhiên, bạn có thể gọi hàm trước khi định nghĩa hàm số.

console.log('Dog: ' + dog());
// => Dog: go go go

function dog(){
  return "go go go";
}

Khi thực hiện, trình duyệt sẽ đưa phần định nghĩa hàm số lên trên đầu của chương trình. Do đó, bạn có thể gọi hàm ở bất cứ đâu bạn muốn.

Hay hiểu theo cách khác là: trình duyệt sẽ bỏ qua phần định nghĩa function JavaScript, cho đến khi hàm này được gọi thì trình duyệt sẽ tìm đến vị trí hàm được định nghĩa.

Có một vị trí khác để định nghĩa hàm số là ở trong block if hoặc loop (for, while,...). Tuy nhiên, cách này không được khuyến khích sử dụng vì một số trình duyệt không hỗ trợ và đã bị cấm trong phiên bản mới nhất của JavaScript.

Call stack

Ở bài viết về cấu trúc chương trình, bạn đã biết về một số loại flow. Tuy nhiên, nếu mình sử dụng hàm số thì flow chương trình sẽ như thế nào?

Hãy xem ví dụ:

function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

Khi đó, luồng chương trình có thể được biểu diễn đơn giản như sau:

top
    greet
        console.log
    greet
top
    console.log
top

Lúc này, máy tính sẽ đưa những lời gọi hàm vào stack. Và thực hiện theo đúng nguyên lý của nó: vào sau ra trước. Cho đến khi stack rỗng thì chương trình kết thúc.

Việc lưu lời gọi hàm vào stack sẽ tốn không gian bộ nhớ (memory). Do đó, trong trường hợp bạn thực hiện quá nhiều lời gọi hàm (đệ quy vô hạn) thì bạn sẽ bị lỗi maximum call stack size exceed hay stack overflow. Tức là tràn stack. Do đó, khi viết đệ quy bạn cần phải chú ý đến điều kiện dừng.

function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + " came first.");
// → Uncaught RangeError: Maximum call stack size exceeded

Truyền tham số vào function JavaScript

Trong C/C++, chúng ta phải định nghĩa rõ ràng kiểu tham số, số lượng tham số của hàm. Và khi gọi hàm thì phải gọi đúng như vậy. Nếu muốn đa dạng các kiểu giá trị thì bạn có thể dùng template. Nếu không muốn truyền một tham số nào vào hàm thì bạn có thể dùng giá trị mặc định (default). Điều này giúp cho C/C++ trở nên rất chặt chẽ.

Ngược lại, JavaScript cung cấp cho chúng ta cách thức sử dụng function rất linh động. Bạn không cần phải quy định kiểu giá trị truyền vào hay trả về. Bạn có thể đưa vào số lượng tham số bằng, nhiều hơn hay ít hơn so với định nghĩa.

Nếu bạn đưa vào nhiều tham số hơn so với định nghĩa thì nó sẽ bỏ qua thành phần thừa. Còn nếu bạn đưa vào ít tham số hơn thì những thành phần thiếu mặc định sẽ là undefined.

Ví dụ về hàm tính luỹ thừa:

function power(base, exponent) {
  if (exponent == undefined)
    exponent = 2;
  var result = 1;
  for (var count = 0; count < exponent; count++)
    result *= base;
  return result;
}

console.log(power(4));
// => 16
console.log(power(4, 3));
// => 64
console.log(power(3, 2, 1));
// => 9

Trường hợp bạn không truyền giá trị cho tham số exponent thì mặc định exponent bằng undefined. Khi đó, mình gán exponent bằng 2. Ngược lại, mình sẽ tính như bình thường.

Closure

Thực sự, khái niệm closure đối với mình cũng khá trừu tượng. Do đó, mình sẽ cố gắng hết sức có thể để giải thích khái niệm này.

Closure tạm dịch là sự đóng kín.

Hãy xem ví dụ sau:

function wrapValue(n) {
  var localVariable = n;
  return function() { return localVariable; };
}

var wrap1 = wrapValue(1);
var wrap2 = wrapValue(2);
console.log(wrap1());
// => 1
console.log(wrap2());
// => 2

Trong hàm wrapValue, mình có khởi tạo một biến số là localVariable. Khi mình gọi wrapValue(1) thì kết quả trả về là 1. Tức là localVariable = 1.  Và khi mình gọi wrapValue(2) thì kết quả trả về là 2. Chứng tỏ, localVariable = 2.

Như vậy, sau khi hàm số được gọi thì biến số cục bộ sẽ ở đâu?

Thực tế, bạn không cần quan tâm đến vấn đề này. Vì dù sao, việc này cũng không làm ảnh hưởng gì đến kết quả chương trình của bạn.

Tuy nhiên, JavaScript đã đưa ra khái niệm closure, giúp bạn không cần phải bận tâm đến vòng đời của những biến cục bộ nữa. Hơn nữa, closure còn mang đến cho bạn sự linh hoạt và sáng tạo khi sử dụng hàm số.

function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

var twice = multiplier(2);
console.log(twice(5));
// => 10

Đây là một ví dụ về hàm closure. Bạn có thể thấy hàm multiplier không hề chứa một biến cục bộ nào. Như vậy, ta có thể hiểu đóng kín tức là nó bao gói lấy biến cục bộ trong hàm.

Khái niệm closure là khá mới mẻ và trông cũng rất thú vị. Vì vậy, mình quyết định sẽ dành riêng một bài viết về hàm closure. Qua đó, bạn có thể hiểu rõ hơn về hàm này và những ứng dụng của nó.

Recursion

Recursion hay còn gọi là hàm đệ quy. Recursion cho phép một hàm số có thể gọi đến chính nó, miễn sao không bị tràn stack.

Sau đây là một ví dụ về đệ quy:

function power(base, exponent) {
  if (exponent == 0)
    return 1;
  else
    return base * power(base, exponent - 1);
}

console.log(power(2, 3));
// => 8

Ví dụ trên miêu tả cách tính luỹ thừa sử dụng đệ quy. Và bạn có thể thấy rằng cách biểu diễn này rất gần với định nghĩa mà chúng ta đã học trong toán học.

a^b = 1, nếu b = 0
a^b = a * a^(b-1), nếu b > 0

Khi sử dụng recursion trong một số bài toán, bạn chỉ cần chuyển từ công thức toán học sang ngôn ngữ lập trình một cách đơn giản. Do đó, sử dụng đệ quy thường ngắn gọn và dễ dàng hơn so với sử dụng vòng lặp.

Nhưng trong JavaScript, việc sử dụng đệ quy sẽ chậm hơn khoảng 10 lần so với sử dụng vòng lặp. Do đó, bạn nên cân nhắc trước khi quyết định sử dụng phương án này.

Cụ thể, bạn nên so sánh giữa cách triển khai và tốc độ thực hiện chương trình. Cách triển khai có thể hiểu là tính rõ ràng, dễ hiểu khi viết code. Thông thường, hai yếu tố này sẽ tỉ lệ nghịch với nhau. Và bạn sẽ phải tìm ra một điểm cân bằng.

Sự phát sinh hàm số trong chương trình

Khi viết chương trình, thông thường sẽ có 2 khả năng khiến một hàm số được sinh ra:

  • Một đoạn chương trình được lặp đi, lặp lại nhiều lần. Việc gom chúng lại thành một hàm sẽ giúp chương trình ngắn gọn hơn, dễ hiểu hơn. Và khi cần thay đổi, bạn chỉ cần sửa một lần trong nội dung hàm, thay vì phải sửa nhiều chỗ.
  • Khi viết chương trình, bạn cần tách toàn bộ chương trình thành những phần nhỏ hơn. Hay còn gọi là module hoá. Lúc này, bạn có thể xây dựng logic cho toàn bộ chương trình trước khi thật sự định nghĩa nội dung hàm. Tại những công ty lớn, thường bạn sẽ chỉ được làm những module nhỏ trong toàn bộ một project lớn. Sau đó, team leader hay project leader... sẽ là người ghép những module nhỏ đó thành một chương trình hoàn chỉnh. Do đó, việc phân chia chương trình thành những hàm số tốt là vô cùng quan trọng.

Như thế nào là một hàm số tốt?

Đây là một câu hỏi rất khó để trả lời. Theo mình, hàm số tốt là hàm số thỏa mãn một số điều kiện sau:

  • một hàm số chỉ thực hiện một chức năng duy nhất
  • độc lập với các yếu tố bên ngoài (biến toàn cục, môi trường thực thi,...)
  • dễ dàng tái sử dụng

Tuy nhiên, không phải lúc nào bạn cũng có thể thoả mãn những yêu cầu trên. Hoặc không nhất thiết phải thoả mãn tất cả những yêu cầu trên.

Pure function và non-pure function

Nhìn chung, có hai loại function JavaScript là pure function (hàm thuần khiết) và non-pure function (hàm không thuần khiết).

Pure function là hàm không phụ thuộc vào yếu tố bên ngoài (biến toàn cục, môi trường thực thi,...). Bất cứ khi nào bạn gọi hàm pure function với cùng một đối số thì kết quả trả về luôn giống nhau. Do đó, pure function rất dễ để sử dụng, tái sử dụng hay bảo trì,... Theo mình, pure function là một hàm số tốt.

None-pure function, ngược lại với pure function, là hàm phụ thuộc vào biến toàn cục hay môi trường thực thi. Do đó, khi bạn gọi cùng một hàm số với cùng một đối số, nhưng kết quả lại khác nhau.

// pure function
function f1(number, factor){
  return number * factor;
}
console.log(f1(2, 10)); // => 20

// non-pure function
var factor = 10;
function f2(number){
  return number * factor;
}
console.log(f2(3)); // => 30
factor = 11;
console.log(f2(3)); // => 33

Trong ví dụ trên, hàm f1 là pure function. Còn hàm f2 là hàm non-pure function (hai lần gọi f2(3), nhưng kết quả một lần là 30 và một lần là 33).

Rõ ràng, hàm non-pure function là khó để tái sử dụng. Nhưng trong rất nhiều trường hợp, mình vẫn phải sử dụng hàm này để đảm bảo hiệu năng chương trình hoặc đơn giản là nó dễ viết hơn.

Tổng kết

Trên đây là những kiến thức cơ bản về function JavaScript. Hy vọng qua đây bạn có thể hiểu về hàm số là gì, cách xây dựng hàm số sao cho phù hợp với yêu cầu của từng bài toán.

Như đã trình bày ở trên, bài viết sau mình sẽ trình bày chi tiết về closure trong JavaScript. Đây là kiến thức rất hay mà mình muốn tìm hiểu và áp dụng nó.

Còn bây giờ, đã đến lúc thực hành những kiến thức được học.

Practice makes perfect.

Thực hành

Bài 1:

Viết function JavaScript kiểm tra xem một số nhập vào là số nguyên tố hay không. Nếu là số nguyên tố thì trả về true. Ngược lại, trả về false.

Xem code tại đây.

Bài 2:

Cho dãy fibonaci có công thức như sau:

f(0) = f(1) = 1;
f(n) = f(n-1) + f(n-2) với n là số nguyên, n >= 2

Hãy viết hàm số tính f(6) sử dụng hai cách (vòng lặp và đệ quy).

Xem code tại đây.

Chỉ có 2 bài đơn giản vậy thôi.

Cuối cùng hẹn gặp lại bạn ở bài viết sau, thân ái!

Tham khảo


★ Nếu bạn thấy bài viết này hay thì hãy theo dõi mình trên Facebook để nhận được thông báo khi có bài viết mới nhất nhé: