Tại sao sử dụng biểu thức hàm được đặt tên?


93

Chúng tôi có hai cách khác nhau để thực hiện biểu thức hàm trong JavaScript:

Biểu thức hàm được đặt tên (NFE) :

var boo = function boo () {
  alert(1);
};

Biểu thức hàm ẩn danh :

var boo = function () {
  alert(1);
};

Và cả hai đều có thể được gọi bằng boo();. Tôi thực sự không thể hiểu tại sao / khi nào tôi nên sử dụng các hàm ẩn danh và khi nào tôi nên sử dụng Biểu thức hàm được đặt tên. Có gì khác biệt giữa chúng?


Câu trả lời:


86

Trong trường hợp của biểu thức hàm ẩn danh, hàm là ẩn danh  - theo nghĩa đen, nó không có tên. Biến mà bạn đang gán nó có tên, nhưng hàm thì không. (Cập nhật: Điều đó đúng thông qua ES5. Kể từ ES2015 [hay còn gọi là ES6], thường một hàm được tạo bằng biểu thức ẩn danh sẽ nhận được tên đúng [nhưng không phải là số nhận dạng tự động], hãy đọc tiếp ...)

Tên rất hữu ích. Tên có thể được nhìn thấy trong dấu vết ngăn xếp, ngăn xếp cuộc gọi, danh sách các điểm ngắt, v.v. Tên là một Điều tốt ™.

(Bạn đã từng phải cẩn thận với các biểu thức hàm được đặt tên trong các phiên bản IE cũ hơn [IE8 trở xuống], vì chúng đã tạo nhầm hai đối tượng hàm hoàn toàn riêng biệt vào hai thời điểm hoàn toàn khác nhau [thêm trong bài viết blog Double take của tôi ]. Nếu bạn cần hỗ trợ IE8 [!!], có lẽ tốt nhất nên sử dụng các biểu thức hàm ẩn danh hoặc khai báo hàm , nhưng tránh các biểu thức hàm được đặt tên.)

Một điều quan trọng về biểu thức hàm được đặt tên là nó tạo một mã định danh trong phạm vi có tên đó cho hàm trong phần thân hàm:

var x = function example() {
    console.log(typeof example); // "function"
};
x();
console.log(typeof example);     // "undefined"

Tuy nhiên, kể từ ES2015, rất nhiều biểu thức hàm "ẩn danh" tạo ra các hàm có tên và điều này đã có trước bởi các công cụ JavaScript hiện đại khác khá thông minh trong việc suy ra tên từ ngữ cảnh. Trong ES2015, biểu thức hàm ẩn danh của bạn dẫn đến một hàm có tên boo. Tuy nhiên, ngay cả với ngữ nghĩa ES2015 +, số nhận dạng tự động không được tạo:

var obj = {
    x: function() {
       console.log(typeof x);   // "undefined"
       console.log(obj.x.name); // "x"
    },
    y: function y() {
       console.log(typeof y);   // "function"
       console.log(obj.y.name); // "y"
    }
};
obj.x();
obj.y();

Việc gán cho tên của hàm được thực hiện với hoạt động trừu tượng SetFunctionName được sử dụng trong các hoạt động khác nhau trong đặc tả.

Phiên bản ngắn về cơ bản là bất cứ khi nào một biểu thức hàm ẩn danh xuất hiện ở phía bên phải của một cái gì đó như một phép gán hoặc khởi tạo, như:

var boo = function() { /*...*/ };

(hoặc có thể là lethoặc constđúng hơn var) , hoặc

var obj = {
    boo: function() { /*...*/ }
};

hoặc là

doSomething({
    boo: function() { /*...*/ }
});

(hai hàm cuối cùng thực sự giống nhau) , hàm kết quả sẽ có tên ( boo, trong các ví dụ).

Có một ngoại lệ quan trọng và có chủ đích: Gán cho một thuộc tính trên một đối tượng hiện có:

obj.boo = function() { /*...*/ }; // <== Does not get a name

Điều này là do lo ngại về rò rỉ thông tin được đưa ra khi tính năng mới đang trong quá trình được thêm vào; chi tiết trong câu trả lời của tôi cho một câu hỏi khác ở đây .


1
Cần lưu ý rằng có ít nhất hai vị trí trong đó việc sử dụng NFE vẫn mang lại lợi thế cụ thể: thứ nhất, đối với các hàm dự định được sử dụng làm hàm tạo thông qua newtoán tử (việc đặt tất cả các tên hàm như vậy làm cho thuộc .constructortính hữu ích hơn trong quá trình gỡ lỗi để tìm ra cái quái gì một số đối tượng là một thể hiện của), và đối với các ký tự của hàm được truyền trực tiếp vào một hàm mà không được gán trước cho một thuộc tính hoặc biến (ví dụ setTimeout(function () {/*do stuff*/});). Ngay cả Chrome cũng hiển thị những điều này (anonymous function)trừ khi bạn giúp nó bằng cách đặt tên cho chúng.
Mark Amery,

4
@MarkAmery: "điều này có còn đúng không? Tôi ... đã cố gắng CTRL-F cho các quy tắc này và không thể tìm thấy chúng" Ồ vâng. :-) Nó nằm rải rác khắp thông số kỹ thuật thay vì ở một nơi xác định một bộ quy tắc, chỉ cần tìm kiếm "setFunctionName". Tôi đã thêm một tập hợp nhỏ các liên kết ở trên, nhưng hiện tại nó hiển thị ở ~ 29 nơi khác nhau. Tôi chỉ hơi ngạc nhiên nếu setTimeoutví dụ của bạn không lấy tên từ đối số chính thức được khai báo cho setTimeout, nếu nó có một. :-) Nhưng có, NFE chắc chắn hữu ích nếu bạn biết rằng bạn sẽ không xử lý các trình duyệt cũ tạo ra một hàm băm của chúng.
TJ Crowder

24

Đặt tên các hàm rất hữu ích nếu chúng cần tham chiếu đến chính chúng (ví dụ: đối với các cuộc gọi đệ quy). Thật vậy, nếu bạn đang truyền trực tiếp một biểu thức hàm dưới dạng đối số tới một hàm khác, thì biểu thức hàm đó không thể trực tiếp tham chiếu chính nó trong chế độ nghiêm ngặt của ES5 trừ khi nó được đặt tên.

Ví dụ: hãy xem xét mã này:

setTimeout(function sayMoo() {
    alert('MOO');
    setTimeout(sayMoo, 1000);
}, 1000);

Sẽ không thể viết mã này một cách rõ ràng như thế này nếu biểu thức hàm được chuyển đến setTimeoutlà ẩn danh; thay vào đó chúng ta cần gán nó cho một biến trước khi setTimeoutgọi. Bằng cách này, với một biểu thức hàm được đặt tên, ngắn hơn và gọn gàng hơn một chút.

Trong lịch sử, có thể viết mã như thế này ngay cả khi sử dụng một biểu thức hàm ẩn danh, bằng cách khai thác arguments.callee...

setTimeout(function () {
    alert('MOO');
    setTimeout(arguments.callee, 1000);
}, 1000);

... nhưng arguments.calleekhông được dùng nữa và hoàn toàn bị cấm ở chế độ nghiêm ngặt của ES5. Do đó MDN khuyên:

Tránh sử dụng bằng arguments.callee()cách đặt tên cho biểu thức hàm hoặc sử dụng khai báo hàm trong đó hàm phải gọi chính nó.

(nhấn mạnh của tôi)


3

Nếu một hàm được chỉ định dưới dạng Biểu thức Hàm, nó có thể được đặt tên.

Nó sẽ chỉ có sẵn bên trong chức năng (ngoại trừ IE8-).

var f = function sayHi(name) {
  alert( sayHi ); // Inside the function you can see the function code
};

alert( sayHi ); // (Error: undefined variable 'sayHi')

Tên này dành cho một lệnh gọi hàm đệ quy đáng tin cậy, ngay cả khi nó được ghi vào một biến khác.

Ngoài ra, tên NFE (Biểu thức hàm được đặt tên) CÓ THỂ được ghi đè bằng Object.defineProperty(...)phương thức như sau:

var test = function sayHi(name) {
  Object.defineProperty(test, 'name', { value: 'foo', configurable: true });
  alert( test.name ); // foo
};

test();

Lưu ý: điều này không thể thực hiện được với Khai báo Hàm. Tên hàm nội bộ "đặc biệt" này chỉ được chỉ định trong cú pháp Biểu thức hàm.


2

Bạn nên luôn sử dụng các biểu thức hàm được đặt tên , đó là lý do tại sao:

  1. Bạn có thể sử dụng tên của hàm đó khi cần đệ quy.

  2. Các hàm ẩn danh không hữu ích khi gỡ lỗi vì bạn không thể thấy tên của hàm gây ra sự cố.

  3. Khi bạn không đặt tên cho một hàm, sau này sẽ khó hiểu nó đang làm gì. Đặt tên cho nó dễ hiểu hơn.

var foo = function bar() {
    //some code...
};

foo();
bar(); // Error!

Ở đây, ví dụ, vì thanh tên được sử dụng trong một biểu thức hàm, nó không được khai báo trong phạm vi bên ngoài. Với các biểu thức hàm được đặt tên, tên của biểu thức hàm được đặt trong phạm vi riêng của nó.


1

Sử dụng các biểu thức hàm đã đặt tên sẽ tốt hơn khi bạn muốn có thể tham chiếu đến hàm được đề cập mà không cần phải dựa vào các tính năng không dùng nữa như arguments.callee.


3
Đó là một bình luận hơn là một câu trả lời. Có lẽ xây dựng sẽ có lợi
vsync
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.