Thứ tự đánh giá và khai báo hàm JavaScript


80

Tại sao một trong những ví dụ đầu tiên không hoạt động, nhưng tất cả những ví dụ khác lại làm được?

// 1 - does not work
(function() {
setTimeout(someFunction1, 10);
var someFunction1 = function() { alert('here1'); };
})();

// 2
(function() {
setTimeout(someFunction2, 10);
function someFunction2() { alert('here2'); }
})();

// 3
(function() {
setTimeout(function() { someFunction3(); }, 10);
var someFunction3 = function() { alert('here3'); };
})();

// 4
(function() {
setTimeout(function() { someFunction4(); }, 10);
function someFunction4() { alert('here4'); }
})();

Câu trả lời:


182

Đâ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áobiể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à:

  1. 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).
  2. Bất cứ thứ gì trong ngoặc đơn ().
  3. 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õ:


// 1
(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 someFunctioncho setTimeout. Và vì vậy nó làm. Thật không may, giá trị hiện tại của someFunctionkhông được xác định.


// 2
(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 someFunctionlà khai báo hàm đã biên dịch của nó.


// 3
(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 someFunctionvà 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 someFunctionnên nó tạo ra một bao đóng cho biến. Tại thời điểm này, giá trị của someFunctionvẫ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 someFunctionkhô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).


7
Đó là một câu trả lời tuyệt vời.
Matt Briggs

1
@Matt: Tôi đã giải thích điều này ở nơi khác (vài lần) trên SO. Một số lời giải thích yêu thích của tôi: stackoverflow.com/questions/3572480/…
slbetman,


3
@Matt: Về mặt kỹ thuật, việc đóng không liên quan đến phạm vi mà là khung ngăn xếp (hay còn gọi là bản ghi kích hoạt). Bao đóng là một biến được chia sẻ giữa các khung ngăn xếp. Khung ngăn xếp là để phân loại một đối tượng là gì. Nói cách khác, phạm vi là những gì lập trình viên cảm nhận được trong cấu trúc mã. Khung ngăn xếp là những gì được tạo trong thời gian chạy trong bộ nhớ. Nó không thực sự như vậy nhưng đủ gần. Khi nghĩ về hành vi thời gian chạy, hiểu biết dựa trên phạm vi đôi khi là không đủ.
slbetman

3
@slebetman cho lời giải thích của bạn về ví dụ 3, bạn đề cập rằng hàm ẩn danh trong setTimeout tạo ra một sự đóng đối với biến someFunction và tại thời điểm này, someFunction vẫn chưa được xác định - điều này có ý nghĩa. Có vẻ như lý do duy nhất mà ví dụ 3 không trả về undefined là do hàm setTimeout (độ trễ 10 mili giây cho phép JavaScript thực thi câu lệnh gán tiếp theo cho someFunction, do đó làm cho nó được định nghĩa) phải không?
wmock

2

Phạm vi của Javascript dựa trên chức năng, không phải phạm vi từ vựng hoàn toàn. đó có nghĩa là

  • Một số chức năng1 được xác định từ đầu của chức năng bao quanh, nhưng nội dung của nó không được xác định cho đến khi được gán.

  • trong ví dụ thứ hai, phép gán là một phần của khai báo, vì vậy nó 'di chuyển' lên trên cùng.

  • trong ví dụ thứ ba, biến tồn tại khi đóng bên trong ẩn danh được xác định, nhưng nó không được sử dụng cho đến 10 giây sau đó, khi đó giá trị đã được gán.

  • ví dụ thứ tư có cả lý do thứ hai và thứ ba để làm việc


1

Bởi vì someFunction1vẫn chưa được chỉ định tại thời điểm cuộc gọi đến setTimeout()được thực hiện.

someFunction3 có thể trông giống như một trường hợp tương tự, nhưng vì bạn đang chuyển một gói hàm someFunction3()đến setTimeout()trong trường hợp này, nên lệnh gọi tới someFunction3()không được đánh giá cho đến sau này.


Nhưng someFunction2vẫn chưa được chỉ định khi lệnh gọi đến setTimeout()được thực hiện ...?
We Are All Monica

1
@jnylen: Khai báo một hàm với functiontừ khóa không chính xác tương đương với việc gán một hàm ẩn danh cho một biến. Các hàm được khai báo là function foo()được "nâng" lên đầu phạm vi hiện tại, trong khi các phép gán biến xảy ra tại điểm chúng được viết.
Chuck

1
+1 cho các chức năng đặc biệt. Tuy nhiên, chỉ vì nó có thể hoạt động không có nghĩa là nó nên được thực hiện. Luôn khai báo trước khi sử dụng.
mway

@mway: trong trường hợp của tôi, tôi đã sắp xếp mã của mình trong một "lớp" thành các phần: biến riêng, trình xử lý sự kiện, hàm riêng, sau đó là hàm công khai. Tôi cần một trong các trình xử lý sự kiện của mình để gọi một trong các hàm riêng tư của tôi. Đối với tôi, việc giữ cho mã được tổ chức theo cách này sẽ giúp ích cho việc sắp xếp các khai báo một cách từ vựng.
We Are All Monica

1

Điều này nghe có vẻ giống như một trường hợp cơ bản của việc tuân theo quy trình tốt để tránh gặp rắc rối. Khai báo các biến và hàm trước khi bạn sử dụng chúng và khai báo các hàm như sau:

function name (arguments) {code}

Tránh khai báo chúng bằng var. Điều này chỉ là cẩu thả và dẫn đến các vấn đề. Nếu bạn có thói quen khai báo mọi thứ trước khi sử dụng, hầu hết các vấn đề của bạn sẽ nhanh chóng biến mất. Khi khai báo các biến, tôi sẽ khởi tạo chúng với một giá trị hợp lệ ngay lập tức để đảm bảo rằng không có biến nào trong số chúng là không xác định. Tôi cũng có xu hướng bao gồm mã kiểm tra các giá trị hợp lệ của các biến toàn cục trước khi một hàm sử dụng chúng. Đây là một biện pháp bảo vệ bổ sung chống lại các lỗi.

Các chi tiết kỹ thuật về cách hoạt động của tất cả điều này giống như vật lý về cách hoạt động của một quả lựu đạn khi bạn chơi với nó. Lời khuyên đơn giản của tôi là ngay từ đầu đừng chơi với lựu đạn.

Một số khai báo đơn giản ở đầu mã có thể giải quyết hầu hết các loại vấn đề này, nhưng một số thao tác dọn dẹp mã vẫn có thể cần thiết.

Lưu ý bổ sung:
Tôi đã chạy một vài thử nghiệm và có vẻ như nếu bạn khai báo tất cả các hàm của mình theo cách được mô tả ở đây, thì thứ tự của chúng không thực sự quan trọng. Nếu hàm A sử dụng hàm B, thì hàm B không phải được khai báo trước hàm A.

Vì vậy, hãy khai báo tất cả các hàm của bạn trước, các biến toàn cục của bạn tiếp theo, sau đó đặt mã khác của bạn sau cùng. Hãy tuân theo các quy tắc ngón tay cái và bạn không thể sai lầm. Thậm chí có thể tốt nhất là đặt các khai báo của bạn ở phần đầu của trang web và mã khác của bạn trong phần nội dung để đảm bảo việc thực thi các quy tắc này.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.