Bài viết này có mục tiêu đưa những lập trình viên đã có một chút kinh nghiệm trong lập trình JavaScript lặn sâu xuống thêm một chút, quên đi và làm sáng tỏ lại lần nữa một số khái niệm quan trọng.
Function là đối tượng
Có hai tính chất khiến cho function trong JavaScript trở nên đặc trưng. Thứ nhất, bản thân hàm cũng là một đối tượng, và thứ hai là chúng tạo ra scope.
Hàm là những đối tượng, chúng có thể:
- Được tạo động tại runtime
- Có thể được gán vào biến, tham chiếu của chúng có thể được copy tới biến khác, chúng có thể bị sửa đổi, và đôi khi có thể bị xóa bỏ
- Có thể được truyền đi như những tham số, và có thể được return như những giá trị trả về
- Có thể có thuộc tính và phương thức của riêng chúng
Vậy nên hoàn toàn có khả năng một function A có một phương thức là function B, thứ mà nhận function C làm tham số, và khi thực thi thì trả về function D… Vào lần đầu tiên nhìn thấy thứ như vậy, các lập trình viên quen thuộc với Java, PHP, dotNet… có thể sẽ thấy choáng ngợp. Nhưng một khi đã quen thuộc với nhiều cách ứng dụng khác nhau, bạn sẽ bội phục với khả năng, tính linh hoạt và tính biểu cảm mà JavaScript function có thể mang lại. Mẹo nghĩ đầu tiên ở đây là, bạn phải bỏ thói quen nhìn các function như các “hàm”, bạn phải nhìn function như những đối tượng, mặc dù những đối tượng này có một tính năng đặc biệt là “gọi lên được”, nghĩa là có thể thực thi được.
Nếu vẫn không tin, hãy nhìn khối mã sau, một đối tượng được tạo ra bởi từ khóa new
, và sau đó nó được dùng như hàm. WTF?
// bình thường không ai làm thế nào cả,
// đây chỉ là thí nghiệm
var add = new Function('a, b', 'return a + b');
add(1, 2); // returns 3
Function tạo ra scope
Trước ES6, JavaScript không tồn tại những scope được tạo ra bởi cặp dấu ngoặc móc {}
— chúng không tạo ra scope mới. Function là thứ duy nhất tạo ra scope. Bất kỳ biến nào được tạo ra, bởi từ khóa var
, ở bất kỳ đâu trong function, sẽ là biến local của function đó và được giấu khỏi các tác nhân bên ngoài. Hãy tưởng tượng rằng khi bạn khai báo biến ở trong thân của câu lệnh if
hay vòng lặp for
thì điều đó không hề có ý nghĩa rằng đó là biến của riêng các câu lệnh đó, nó là biến của toàn bộ function, và nếu không hề có function nào đang bao những câu lệnh đó lại thì các biến trở thành biến global.
Mặc dù tới ES6, với từ khóa let
và const
thì tính năng block cope đã xuất hiện, nhưng điều đó không phủ nhận rằng function scope vẫn là không thể thiếu được. Chúng ta cần giữ các biến trong phạm vi điều khiển một cách linh hoạt nhất có thể.
Hàm có tên và không tên
Đối tượng hàm có một thuộc tính chỉ đọc tên là name
. Thuộc tính name
không phải là một phần của chuẩn ES nhưng nó tồn tại trong rất nhiều môi trường thực thi JavaScript. Trong trường hợp hàm không có tên, hay còn gọi là vô danh (anonymous functions), name
sẽ là giá trị undefined
hay ""
tùy vào môi trường.
function foo() {} // declaration
var bar = function () {}; // expression
var baz = function baz() {}; // named expression
foo.name; // "foo"
bar.name; // ""
baz.name; // "baz"
Thuộc tính name
hữu dụng khi debug, hay một số trường hợp cần gọi thực thi đệ quy.
Biểu thức hàm
Hàm là những đối tượng, đối tượng đó có thể được dùng như đối số, hay tham gia vào các biểu thức, tham chiếu của chúng có thể được gán vào các tên. Hàm được tạo ra trong những trường hợp như thế được gọi là biểu thức hàm.
Trước tiên hãy xem đoạn mã sau đây:
// named function expression
var add = function add(a, b) {
return a + b;
};
Hàm ở trên, được gọi là named function expression, nôm na là biểu thức hàm có tên. “Biểu thức” ngụ ý rằng hàm đó được dùng như một khối giá trị có thể gán, tính toán, truyền đi… được. “Có tên” ngụ ý rằng bản thân hàm đó có tên, được đặt là add
, thông qua khai báo function add()
.
Nếu chúng ta bỏ tên của hàm đi, khối mã trên sẽ biến thành biểu thức hàm không tên, nói ngắn gọn là biểu thức hàm, hay nói theo cách thông dụng nhất thì là hàm vô danh — anonymous function. Hàm vô danh luôn được sử dụng như một biểu thức, nên không cần phát biểu là biểu thức hàm vô danh làm gì cho mệt:
// function expression, a.k.a. anonymous function
var add = function (a, b) {
return a + b;
};
Có những khi không thể sử dụng cú pháp khai báo hàm, trong những trường hợp đó biểu thức hàm sẽ hữu dụng:
// đây là biểu thức,
// được truyền cho lời gọi hàm `callMe`
callMe(function () {
// I am an unnamed function expression
// also known as an anonymous function
});
// đây là một biểu thức hàm có tên, tên là `me`
callMe(function me() {
// I am a named function expression
// and my name is "me"
});
// một biểu thức hàm khác
var myobject = {
say: function () {
// I am a function expression
}
};
Khai báo hàm
Khi hàm được tạo ra mà không được gán vào biến, không trở thành thuộc tính của đối tượng nào đó, không được truyền đi như những đối số, cú pháp ở đó được gọi là khai báo hàm — function delcaration. Khai báo hàm chỉ xuất hiện trong global hoặc trong thân của một hàm khác (strict mode từ chối vị trí này).
// global scope
function foo() {}
function local() {
// local scope
function bar() {}
return bar;
}
Khai báo hàm trông tương tự với biểu thức hàm, và đôi khi thì ta không thể phân biệt được hai thứ đó với nhau. Ngoại trừ việc biểu thức hàm thuần túy là một câu lệnh thực thi, thế nên nó cần kết thúc bằng dấu chấm phẩy. Tuy nhiên có những cơ chế khác hẳn nhau đứng đằng sau hai thuật ngữ này.
Thông thường, biểu thức hàm được cân nhắc sử dụng hơn, bởi nó làm lộ ra đặc trưng đối tượng của các hàm, thay vì khiến cho các hàm trông như một cấu trúc thù của ngôn ngữ.
Hoisting
Biểu thức hàm có tên và khai báo hàm không thật sự tương đương nhau. Thứ làm nên sự khác biệt chính là một hành xử đặc biệt của JavaScript được gọi là hoisting – nôm na là cẩu móc.
Chúng ta đã biết rằng mọi biến được khai báo với từ khóa var
sẽ được coi như là được khai báo ở đầu function, không quan trọng bạn đặt lệnh khai báo ở đâu trong function. Đây chính là biểu hiện của hoisting.
Chúng ta cũng biết rằng nếu khai báo biến mà không có từ khóa var
, biến sẽ được hoisting tới không gian global.
(function () {
i = 5;
})();
console.log(i); // 5
Cơ chế hoisting hoạt động với tất cả các biến, kể cả các biến hàm. Nếu biến hàm được tạo ra với từ khóa var
, biến đó sẽ được hoist tới đầu hàm chứa câu lệnh. Và nếu biến hàm được tạo ra mà không có từ khóa var
, biến đó sẽ được hoist tới không gian global:
(function() {
foo = function() {
console.log("foo");
}
})();
foo(); // "foo"
Tương tự như vậy, các khai báo hàm được hoist. Ta có thể sử dụng chúng trước khi gặp khối mã khai báo.
hoisted(); // logs "foo"
function hoisted() {
console.log('foo');
}
Điểm mấu chốt là trong cơ chế hoist, chỉ có tên biến được hoist, còn biểu thức khởi tạo giá trị cho biến thì không. Do đó khi tạo hàm thành các biểu thức, chúng ta không thể sử dụng một hàm trước khi lệnh khai báo biểu thức hàm xuất hiện.
notHoisted === undefined; // true
// mặc dù tên biến được hoist, nhưng định nghĩa của hàm thì không, thế nên biến mới mang giá trị undefined
notHoisted(); // TypeError: notHoisted is not a function
var notHoisted = function() {
console.log('bar');
};
Chính vì sự rối rắm này mà strict mode của ES5 đã giới hạn rằng các khai báo hàm chỉ được phép đặt ở không gian global, nhằm hạn chế các sơ suất xảy ra do lập trình viên không nắm vững cơ chế hosting. Cho tới khi ES6 mang tới các từ khóa let
và const
để khiến cho phạm vi hoạt động của biến được bình thường hơn một chút.
Tổng kết
Đến cuối cùng, vẫn chỉ có hai điều chúng ta cần nhớ:
- Hàm đơn thuần là những đối tượng
- Hàm cung cấp local scope