Giải thích cú pháp hàm ẩn danh được đóng gói


372

Tóm lược

Bạn có thể giải thích lý do đằng sau cú pháp cho các hàm ẩn danh được đóng gói trong JavaScript không? Tại sao điều này hoạt động: (function(){})();nhưng điều này không : function(){}();?


Những gì tôi biết

Trong JavaScript, người ta tạo một hàm có tên như thế này:

function twoPlusTwo(){
    alert(2 + 2);
}
twoPlusTwo();

Bạn cũng có thể tạo một hàm ẩn danh và gán nó cho một biến:

var twoPlusTwo = function(){
    alert(2 + 2);
};
twoPlusTwo();

Bạn có thể gói gọn một khối mã bằng cách tạo một hàm ẩn danh, sau đó gói nó trong ngoặc và thực thi nó ngay lập tức:

(function(){
    alert(2 + 2);
})();

Điều này hữu ích khi tạo các tập lệnh được mô đun hóa, để tránh làm lộn xộn phạm vi hiện tại hoặc phạm vi toàn cầu, với các biến có khả năng xung đột - như trong trường hợp tập lệnh Greasemonkey, plugin jQuery, v.v.

Bây giờ, tôi hiểu tại sao điều này hoạt động. Các dấu ngoặc bao quanh nội dung và chỉ hiển thị kết quả (tôi chắc chắn có cách tốt hơn để mô tả điều đó), chẳng hạn như với (2 + 2) === 4.


Những gì tôi không hiểu

Nhưng tôi không hiểu tại sao điều này không hoạt động như nhau:

function(){
    alert(2 + 2);
}();

Bạn có thể giải thích điều đó với tôi không?


39
Tôi nghĩ rằng tất cả các ký hiệu và cách xác định / cài đặt / chức năng gọi khác nhau này là phần khó hiểu nhất khi làm việc với javascript. Mọi người có xu hướng không nói về họ. Đây không phải là một điểm nhấn mạnh trong hướng dẫn hoặc blog. Nó làm tôi suy nghĩ vì đó là điều khó hiểu đối với hầu hết mọi người và những người thông thạo js cũng phải trải qua điều đó. Nó giống như thực tế cấm kỵ trống rỗng này không bao giờ được nói về.
ahnbizcad

1
Cũng đọc về mục đích của cấu trúc này , hoặc kiểm tra một lời giải thích ( kỹ thuật ) (cũng ở đây ). Đối với vị trí của dấu ngoặc đơn, xem câu hỏi này về vị trí của họ .
Bergi

OT: Đối với những người muốn biết nơi các chức năng ẩn danh được sử dụng rất nhiều, vui lòng đọc adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html
Alireza Fattahi

Đây là một trường hợp điển hình của Biểu thức hàm được gọi ngay lập tức (IIFE).
Aminu Kano

Câu trả lời:


410

Nó không hoạt động vì nó được phân tích cú pháp dưới dạng FunctionDeclarationvà định danh tên của khai báo hàm là bắt buộc .

Khi bạn bao quanh nó bằng dấu ngoặc đơn, nó được đánh giá là a FunctionExpressionvà các biểu thức hàm có thể được đặt tên hoặc không.

Ngữ pháp của một FunctionDeclarationhình như thế này:

function Identifier ( FormalParameterListopt ) { FunctionBody }

FunctionExpressions:

function Identifieropt ( FormalParameterListopt ) { FunctionBody }

Như bạn có thể thấy mã thông báo Identifier(Định danh opt ) trong FunctionExpressionlà tùy chọn, do đó chúng ta có thể có biểu thức hàm mà không có tên được xác định:

(function () {
    alert(2 + 2);
}());

Hoặc biểu thức hàm được đặt tên :

(function foo() {
    alert(2 + 2);
}());

Dấu ngoặc đơn (chính thức được gọi là Toán tử nhóm ) chỉ có thể bao quanh các biểu thức và một biểu thức hàm được ước tính.

Hai sản phẩm ngữ pháp có thể mơ hồ và chúng có thể trông giống hệt nhau, ví dụ:

function foo () {} // FunctionDeclaration

0,function foo () {} // FunctionExpression

Trình phân tích cú pháp biết đó là a FunctionDeclarationhay a FunctionExpression, tùy thuộc vào ngữ cảnh nơi nó xuất hiện.

Trong ví dụ trên, cái thứ hai là một biểu thức vì toán tử Comma cũng chỉ có thể xử lý các biểu thức.

Mặt khác, FunctionDeclarations thực sự chỉ có thể xuất hiện trong Programmã được gọi là " ", nghĩa là mã bên ngoài trong phạm vi toàn cầu và bên trong FunctionBodycác chức năng khác.

Nên tránh các chức năng bên trong các khối, bởi vì chúng có thể dẫn đến một hành vi không thể đoán trước, ví dụ:

if (true) {
  function foo() {
    alert('true');
  }
} else {
  function foo() {
    alert('false!');
  }
}

foo(); // true? false? why?

Đoạn mã trên thực sự sẽ tạo ra một SyntaxError, vì Blockchỉ có thể chứa các câu lệnh (và Đặc tả ECMAScript không định nghĩa bất kỳ câu lệnh hàm nào), nhưng hầu hết các triển khai đều có thể chấp nhận được và chỉ đơn giản là sẽ lấy hàm thứ hai, một cảnh báo 'false!'.

Việc triển khai Mozilla -Rhino, SpiderMonkey, - có một hành vi khác. Ngữ pháp của họ chứa Tuyên bố hàm không chuẩn , nghĩa là hàm sẽ được đánh giá trong thời gian chạy , không phải lúc phân tích cú pháp, như xảy ra với FunctionDeclarations. Trong những triển khai đó, chúng ta sẽ có được hàm đầu tiên được xác định.


Các hàm có thể được khai báo theo nhiều cách khác nhau, so sánh như sau :

1- Một hàm được xác định với hàm tạo Hàm được gán cho biến nhân :

var multiply = new Function("x", "y", "return x * y;");

2- Một khai báo hàm của một hàm có tên là bội :

function multiply(x, y) {
    return x * y;
}

3- Một biểu thức hàm được gán cho biến nhân :

var multiply = function (x, y) {
    return x * y;
};

4- Một biểu thức hàm có tên func_name , được gán cho biến nhân :

var multiply = function func_name(x, y) {
    return x * y;
};

2
Câu trả lời của CMS là chính xác. Để được giải thích sâu sắc về khai báo hàm và biểu thức, xem bài viết này của kangax .
Tim xuống

Đây là một câu trả lời tuyệt vời. Nó dường như được liên kết mật thiết với cách văn bản nguồn được phân tích cú pháp - và cấu trúc của BNF. trong ví dụ 3 của bạn, tôi có nên nói rằng đó là một biểu thức hàm bởi vì nó tuân theo dấu bằng, các hình thức đó là một tuyên bố / tuyên bố hàm khi nó tự xuất hiện trên một dòng? Tôi tự hỏi mục đích của việc đó sẽ là gì - nó chỉ được hiểu là một khai báo hàm có tên, nhưng không có tên? Mục đích nào phục vụ nếu bạn không gán nó cho một biến, đặt tên hoặc gọi nó?
Breton

1
Aha. Rất hữu ích. Cảm ơn, CMS. Phần tài liệu Mozilla này mà bạn đã liên kết đặc biệt khai sáng: developer.mozilla.org/En/Core_JavaScript_1.5_Reference/
trộm

1
+1, mặc dù bạn có khung đóng ở vị trí sai trong biểu thức chức năng :-)
NickFitz

1
@GovindRai, số Khai báo hàm được xử lý tại thời điểm biên dịch và khai báo hàm trùng lặp ghi đè khai báo trước đó. Trong thời gian chạy, khai báo hàm đã có sẵn và trong trường hợp này, cái có sẵn là cái có cảnh báo "false". Để biết thêm thông tin, hãy đọc bạn không biết JS
Saurabh Misra

50

Mặc dù đây là một câu hỏi và câu trả lời cũ, nó thảo luận về một chủ đề mà cho đến ngày nay ném nhiều nhà phát triển cho một vòng lặp. Tôi không thể đếm số lượng ứng viên phát triển JavaScript mà tôi đã phỏng vấn, người không thể cho tôi biết sự khác biệt giữa khai báo hàm biểu thức hàm ai không biết biểu thức hàm được gọi ngay lập tức là gì.

Tuy nhiên, tôi muốn đề cập đến một điều rất quan trọng đó là đoạn mã của Premasagar sẽ không hoạt động ngay cả khi anh ta đã đặt cho nó một mã định danh tên.

function someName() {
    alert(2 + 2);
}();

Lý do điều này không hoạt động là vì công cụ JavaScript diễn giải đây là một khai báo hàm theo sau là một toán tử nhóm hoàn toàn không liên quan không chứa biểu thức và các toán tử nhóm phải chứa một biểu thức. Theo JavaScript, đoạn mã trên tương đương với đoạn mã sau.

function someName() {
    alert(2 + 2);
}

();

Một điều khác mà tôi muốn chỉ ra rằng có thể một số người sử dụng là bất kỳ định danh tên nào bạn cung cấp cho một biểu thức hàm là khá vô dụng trong ngữ cảnh của mã ngoại trừ trong chính định nghĩa hàm.

var a = function b() {
    // do something
};
a(); // works
b(); // doesn't work

var c = function d() {
    window.setTimeout(d, 1000); // works
};

Tất nhiên, sử dụng mã định danh với định nghĩa hàm của bạn luôn hữu ích khi nói đến mã gỡ lỗi, nhưng đó là một thứ hoàn toàn khác ... :-)


17

Câu trả lời tuyệt vời đã được đăng. Nhưng tôi muốn lưu ý rằng các khai báo hàm trả về một bản ghi hoàn thành trống:

14.1.20 - Ngữ nghĩa thời gian chạy: Đánh giá

FunctionDeclaration : function BindingIdentifier ( FormalParameters ) { FunctionBody }

  1. Trả về NormalCompletion (trống).

Thực tế này không dễ quan sát, bởi vì hầu hết các cách cố gắng để nhận được giá trị trả về sẽ chuyển đổi khai báo hàm thành biểu thức hàm. Tuy nhiên, evalcho thấy nó:

var r = eval("function f(){}");
console.log(r); // undefined

Gọi một hồ sơ hoàn thành trống không có ý nghĩa. Đó là lý do tại sao function f(){}()không thể làm việc. Trong thực tế, công cụ JS thậm chí không cố gắng gọi nó, các dấu ngoặc đơn được coi là một phần của một câu lệnh khác.

Nhưng nếu bạn bọc hàm trong ngoặc đơn, nó sẽ trở thành biểu thức hàm:

var r = eval("(function f(){})");
console.log(r); // function f(){}

Biểu thức hàm trả về một đối tượng hàm. Và do đó bạn có thể gọi nó : (function f(){})().


Xấu hổ câu trả lời này bị bỏ qua. Mặc dù không đầy đủ như câu trả lời được chấp nhận, nó cung cấp một số thông tin bổ sung rất hữu ích và xứng đáng được nhiều phiếu bầu hơn
Avrohom Yisroel

10

Trong javascript, cái này được gọi là Biểu thức hàm được gọi ngay lập tức (IIFE) .

Để làm cho nó một biểu thức chức năng bạn phải:

  1. kèm theo nó bằng cách sử dụng ()

  2. đặt một toán tử void trước nó

  3. gán nó cho một biến.

Nếu không, nó sẽ được coi là định nghĩa hàm và sau đó bạn sẽ không thể gọi / gọi nó cùng một lúc bằng cách sau:

 function (arg1) { console.log(arg1) }(); 

Ở trên sẽ cho bạn lỗi. Bởi vì bạn chỉ có thể gọi một biểu thức hàm ngay lập tức.

Điều này có thể đạt được một vài cách: Cách 1:

(function(arg1, arg2){
//some code
})(var1, var2);

Cách 2:

(function(arg1, arg2){
//some code
}(var1, var2));

Cách 3:

void function(arg1, arg2){
//some code
}(var1, var2);

cách 4:

  var ll = function (arg1, arg2) {
      console.log(arg1, arg2);
  }(var1, var2);

Tất cả ở trên sẽ ngay lập tức gọi biểu thức chức năng.


3

Tôi chỉ có một nhận xét nhỏ khác. Mã của bạn sẽ hoạt động với một thay đổi nhỏ:

var x = function(){
    alert(2 + 2);
}();

Tôi sử dụng cú pháp trên thay vì phiên bản phổ biến rộng rãi hơn:

var module = (function(){
    alert(2 + 2);
})();

bởi vì tôi đã không quản lý để thụt lề hoạt động chính xác cho các tệp javascript trong vim. Có vẻ như vim không thích các dấu ngoặc nhọn trong ngoặc đơn mở.


Vậy tại sao cú pháp này hoạt động khi bạn gán kết quả thực hiện cho một biến, nhưng không độc lập?
paislee

1
@paislee - Bởi vì công cụ JavaScript diễn giải bất kỳ câu lệnh JavaScript hợp lệ nào bắt đầu bằng functiontừ khóa dưới dạng khai báo hàm, trong trường hợp đó, trailing ()được hiểu là toán tử nhóm , theo quy tắc cú pháp JavaScript, chỉ có thể và phải chứa biểu thức JavaScript.
natlee75

1
@bosonix - Cú pháp ưa thích của bạn hoạt động tốt, nhưng nên sử dụng "phiên bản lan truyền rộng rãi hơn" mà bạn đã tham chiếu hoặc biến thể ()được đặt trong toán tử nhóm (một loại mà Douglas Crockford khuyến nghị) để thống nhất: đó là phổ biến để sử dụng IIFE mà không gán chúng cho một biến và thật dễ dàng quên để bao gồm các dấu ngoặc đơn nếu bạn không sử dụng chúng một cách nhất quán.
natlee75

0

Có lẽ câu trả lời ngắn hơn sẽ là

function() { alert( 2 + 2 ); }

là một chức năng theo nghĩa đenđịnh nghĩa một function (nặc danh). Một cặp ()-Pair, được hiểu là một biểu thức, không được mong đợi ở toplevel, chỉ có nghĩa đen.

(function() { alert( 2 + 2 ); })();

là trong một tuyên bố biểu rằng gọi một chức năng ẩn danh.


0
(function(){
     alert(2 + 2);
 })();

Trên đây là cú pháp hợp lệ bởi vì bất cứ điều gì được truyền trong ngoặc đơn đều được coi là biểu thức hàm.

function(){
    alert(2 + 2);
}();

Trên đây không phải là cú pháp hợp lệ. Bởi vì trình phân tích cú pháp cú pháp java tìm kiếm tên hàm sau từ khóa hàm vì nó không tìm thấy bất cứ điều gì nên nó gây ra lỗi.


2
Trong khi câu trả lời của bạn không chính xác, câu trả lời được chấp nhận đã bao gồm tất cả những điều này trong phòng.
Đến Arnold

0

Bạn cũng có thể sử dụng nó như:

! function() { console.log('yeah') }()

hoặc là

!! function() { console.log('yeah') }()

!- phủ định op chuyển đổi định nghĩa fn thành biểu thức fn, do đó, bạn có thể gọi nó ngay lập tức với (). Tương tự như sử dụng 0,fn defhoặcvoid fn def


-1

Chúng có thể được sử dụng với các tham số-tham số như

var x = 3; 
var y = 4;

(function(a,b){alert(a + b)})(x,y)

sẽ có kết quả là 7


-1

Các dấu ngoặc đơn bổ sung đó tạo ra các hàm ẩn danh bổ sung giữa không gian tên toàn cục và hàm ẩn danh có chứa mã. Và trong các hàm Javascript được khai báo bên trong các hàm khác chỉ có thể truy cập không gian tên của hàm cha có chứa chúng. Vì có thêm đối tượng (hàm ẩn danh) giữa phạm vi toàn cầu và phạm vi mã thực tế không được giữ lại.

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.