Đây không phải là vấn đề phạm vi cũng không phải là vấn đề đóng cửa. Vấn đề là sự hiểu biết giữa khai báo và biểu thức .
Mã JavaScript, kể cả phiên bản JavaScript đầu tiên của Netscape và bản sao đầu tiên của Microsoft, được xử lý theo hai giai đoạn:
Giai đoạn 1: biên dịch - trong giai đoạn này, mã được biên dịch thành một cây cú pháp (và mã bytecode hoặc nhị phân tùy thuộc vào công cụ).
Giai đoạn 2: thực thi - mã được phân tích cú pháp sau đó được diễn giải.
Cú pháp khai báo hàm là:
function name (arguments) {code}
Đối số tất nhiên là tùy chọn (mã cũng là tùy chọn nhưng điểm của điều đó là gì?).
Nhưng JavaScript cũng cho phép bạn tạo các hàm bằng cách sử dụng các biểu thức . Cú pháp của biểu thức hàm tương tự như khai báo hàm ngoại trừ chúng được viết trong ngữ cảnh biểu thức. Và biểu thức là:
- Bất kỳ thứ gì ở bên phải của một
=
dấu hiệu (hoặc :
trên các ký tự đối tượng).
- Bất cứ thứ gì trong ngoặc đơn
()
.
- Các tham số cho các chức năng (điều này thực sự đã được bao gồm trong 2).
Các biểu thức không giống như khai báo được xử lý trong giai đoạn thực thi hơn là giai đoạn biên dịch. Và bởi vì điều này, thứ tự của các biểu thức quan trọng.
Vì vậy, để làm rõ:
(function() {
setTimeout(someFunction, 10);
var someFunction = function() { alert('here1'); };
})();
Giai đoạn 1: biên dịch. Trình biên dịch thấy rằng biến someFunction
được định nghĩa nên nó tạo ra nó. Theo mặc định, tất cả các biến được tạo đều có giá trị là không xác định. Lưu ý rằng trình biên dịch chưa thể gán giá trị tại thời điểm này vì các giá trị có thể cần trình thông dịch thực thi một số mã để trả về một giá trị để gán. Và ở giai đoạn này, chúng tôi vẫn chưa thực thi mã.
Giai đoạn 2: thực hiện. Trình thông dịch thấy bạn muốn chuyển biến someFunction
cho setTimeout. Và vì vậy nó làm. Thật không may, giá trị hiện tại của someFunction
không được xác định.
(function() {
setTimeout(someFunction, 10);
function someFunction() { alert('here2'); }
})();
Giai đoạn 1: biên dịch. Trình biên dịch thấy bạn đang khai báo một hàm với tên someFunction và vì vậy nó tạo ra nó.
Giai đoạn 2: Trình thông dịch thấy bạn muốn chuyển someFunction
đến setTimeout. Và vì vậy nó làm. Giá trị hiện tại của someFunction
là khai báo hàm đã biên dịch của nó.
(function() {
setTimeout(function() { someFunction(); }, 10);
var someFunction = function() { alert('here3'); };
})();
Giai đoạn 1: biên dịch. Trình biên dịch thấy bạn đã khai báo một biến someFunction
và tạo nó. Như trước đây, giá trị của nó là không xác định.
Giai đoạn 2: thực hiện. Trình thông dịch chuyển một hàm ẩn danh để setTimeout được thực thi sau này. Trong hàm này, nó thấy bạn đang sử dụng biến someFunction
nên nó tạo ra một bao đóng cho biến. Tại thời điểm này, giá trị của someFunction
vẫn chưa được xác định. Sau đó, nó thấy bạn chỉ định một chức năng cho someFunction
. Tại thời điểm này, giá trị của someFunction
không còn là không xác định. 1/100 giây sau setTimeout kích hoạt và someFunction được gọi. Vì giá trị của nó không còn là không xác định nên nó hoạt động.
Trường hợp 4 thực sự là một phiên bản khác của trường hợp 2 với một chút của trường hợp 3 được đưa vào. Tại điểm someFunction
được chuyển tới setTimeout, nó đã tồn tại do nó được khai báo.
Làm rõ thêm:
Bạn có thể thắc mắc tại sao setTimeout(someFunction, 10)
không tạo một bao đóng giữa bản sao cục bộ của someFunction và bản sao được chuyển đến setTimeout. Câu trả lời cho điều đó là các đối số hàm trong JavaScript luôn luôn được chuyển theo giá trị nếu chúng là số hoặc chuỗi hoặc tham chiếu cho mọi thứ khác. Vì vậy, setTimeout không thực sự nhận biến someFunction được chuyển cho nó (điều này có nghĩa là một bao đóng được tạo) mà chỉ lấy đối tượng mà someFunction tham chiếu đến (trong trường hợp này là một hàm). Đây là cơ chế được sử dụng rộng rãi nhất trong JavaScript để phá vỡ các bao đóng (ví dụ: trong các vòng lặp).