深入理解:函數、匿名函數、自執行函數、閉包


深入理解:函數、匿名函數、自執行函數、閉包

原文:https://www.jianshu.com/p/a72395741c50

1 定義函數的方式

  1. 函數的聲明
  2. 函數表達式

1.1 函數聲明

下面是函數聲明的結構:

function sum(x, y) { alert(x+y); } sum(1, 2); 

由於javascript具有“函數聲明提升”的特性,即執行代碼之前,先讀取函數聲明,意味着函數聲明可以放在調用它的語句之后。如下代碼可以正常執行:

sum(1, 2); function sum(x, y) { alert(x+y); } 

1.2 函數表達式

函數表達式中有幾種不同的語法。最常見和最具代表性的一種如代碼所示:

var ss = function(x, y) { alert(x+y); } ss(1, 2); 

這種形式看起來好像是常規的變量賦值語句。但是函數表達式和函數聲明的區別在於,函數表達式在使用前必須先賦值。所以一下代碼執行的時候就會出錯:

ss(1, 2); // 報錯,顯示undefined is not a function var ss = function(x, y) { alert(x+y); } 

造成這種現象是因為解析器在向執行環境中加載數據時,解析器會率先讀取函數聲明,並使其在執行任何代碼前可用;至於函數表達式,則必須等到解析器執行到它的所在的的代碼行,才會真正的被解析。
函數表達式中,創建的函數叫做匿名函數,因為function關鍵字后面沒有標識符。

2 匿名函數

匿名函數,顧名思義就是沒有名字的函數。上面的函數表達式中的創建,即創建一個匿名函數,並將匿名函數賦值給變量ss,用ss來進行函數的調用,調用的方式就是在變量ss后面加上一對括號(),如果有參數傳入的話就是ss(1,2),這就是匿名函數的一種調用方式。

2.1 匿名函數的調用方式

還有一種匿名函數的調用方式是:1)將匿名函數用()括起來;2)然后在后面加一對小括號(包含參數列表)。我們再看一下以下例子:

alert((function(x, y){return x+y;})(2, 3)); // Function 對象(類),其中的每個arg都是一個參數,最后一個參數是函數主體,且參數必須是字符串。 這種函數定義方式的場景是在NodeJS和瀏覽器的全局環境中 alert((new Function('x', 'y', 'return x+y;'))(2, 3)); 

在javascript中,是沒有塊級作用域這種說法的,以上代碼的這種方式就是模仿了塊級作用域(通常成為私有作用域),語法如下所示:

function() { // 代碼塊級作用域 }(); 

以上代碼定義並立即調用了一個匿名函數。經函數聲明包含在一對圓括號中,表示它實際上是一個函數表達式。而緊隨期后的另一個圓括號會立即調用這個函數。

function() { // 代碼塊級作用域 }(); 

注意:以上代碼會報錯Uncaught SyntaxError: Unexpected token (
因為Javascript將function關鍵字當作一個函數聲明的開始,而函數聲明后面不能加圓括號,如果你不顯示告訴編譯器,它會默認生成一個缺少名字的function,並且拋出一個語法錯誤,因為function聲明需要一個名字。有趣的是,即便你為上面那個錯誤的代碼加上一個名字,他也會提示語法錯誤,只不過和上面的原因不一樣。在一個表達式后面加上括號(),該表達式會立即執行,但是在一個語句后面加上括號(),是完全不一樣的意思,他的只是分組操作符

// 這個function在語法上是沒問題的,但是依然只是一個語句 // 加上括號()以后依然會報錯,因為分組操作符需要包含表達式 function foo(){ /* code */ }(); // SyntaxError: Unexpected token ) // 但是如果你在括弧()里傳入一個表達式,將不會有異常拋出 // 但是foo函數依然不會執行 function foo(){ /* code */ }( 1 ); // 因為它完全等價於下面這個代碼,一個function聲明后面,又聲明了一個毫無關系的表達式: function foo(){ /* code */ } ( 1 ); 

因此,以上代碼要想正確實現,就必須要實現賦值,如a = function(){}(),"a="這個告訴了編譯器這是一個函數表達式,而不是函數聲明,因為函數表達式后面可以跟()。因此下面兩段代碼是等價的。

var aa = function(x) { alert(x); }(5); // 5 
(function(x){alert(x);})(5); 

有上面對於函數和匿名函數的了解,我們引申出來了一個概念,即自執行函數,讓我們更加深入的了解為什么。a = function(){}()這個表示可以讓編譯器認為這個是一個函數表達式而不是一個函數的聲明。

3 自執行函數

我們創建了一個匿名的函數,並立即執行它,由於外部無法引用它內部的變量,因此在執行完后很快就會被釋放,關鍵是這種機制不會污染全局對象。
自執行函數,即定義和調用合為一體。

自執行函數的一些表達方式:

// 下面2個括號()都會立即執行 (function () { /* code */ } ()); // 推薦使用這個 (function () { /* code */ })(); // 但是這個也是可以用的 

4 閉包

閉包是指有權訪問另一個函數作用域的變量的函數。創建閉包的的常用方式,就是一個函數內部創建另一個函數。
閉包主要涉及到js的幾個其他的特性:作用域鏈,垃圾(內存)回收機制,函數嵌套,等等。

4.1 閉包簡單例子

一般情況下,函數內部可以訪問函數外部的全局變量

 var a = 1;//全局變量 function f1(){ alert(a); } f1();//1 

函數外部不能訪問函數內部的局部變量,

function f2(){ var a = 1 ; //局部變量 } alert(a); //error 

有時候我們想得到函數內部的局部變量,那應該如何實現呢?這就引入了閉包的概念。

function fn1() { var n = 1; return function() { alert(n); } } result = fn1(); result(); 

4.2 閉包的作用

  • 讀取函數內部的變量
  • 將變量的值始終保存在內存中

一般來講,當函數執行完畢之后,函數內部的局部活動對象就會被銷毀,內存中僅保存全局作用域,即js的內存回收機制。
如果這個函數內部又嵌套了另一個函數,而這個函數是有可能在外部被調用到的.並且這個內部函數又使用了外部函數的某些變量的話.這種內存回收機制就會出現問題。如果在外部函數返回后,又直接調用了內部函數,那么內部函數就無法讀取到他所需要的外部函數中變量的值了.所以js解釋器在遇到函數定義的時候,會自動把函數和他可能使用的變量(包括本地變量和父級和祖先級函數的變量(自由變量))一起保存起來.也就是構建一個閉包,這些變量將不會被內存回收器所回收,只有當內部的函數不可能被調用以后(例如被刪除了,或者沒有了指針),才會銷毀這個閉包,而沒有任何一個閉包引用的變量才會被下一次內存回收啟動時所回收。

4.3 注意事項

  1. 由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。
  2. 閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值。

5 自執行函數與閉包的使用

很多情況下,可以利用自執行函數和閉包來保存某個特殊狀態中的值。
由於作用域鏈的配置機制,使得閉包只能取得包含函數中任何變量的最后一個值。即說明了閉包中所保存的是整個變量對象,而不是某一個特殊的變量。我們 用下面這個例子來說明這個問題。
例子1:

function createFunction() { var result = new Array(); for( var i = 0; i<10; i++) { result[i] = function() { return i; }; } return result; } var aa = createFunction(); alert(aa[0]()); //10 alert(aa[1]()); //10 
  • 在這個函數中,我們直接將閉包賦值給數組。這個函數會返回一個函數數組。表面上來看,似乎每個函數都應該返回自己的索引,即位置0的函數返回0,位置1的函數返回1一次類推。但實際上,如同上面例子,每個函數都返回了10。因為每個函數的作用域鏈中都保存createFunctions()函數的活動對象,所以它們引用的都是同一個變量i。當createFunctions()函數返回后,變量i的值死10,此時每個函數都引用着保存變量i的同一個變量對象。所以在每個函數內部i的值都是10。
  • 所以,我們可以通過如下例子,創建一個自執行函數(匿名函數)強制讓閉包的行為符合預期。
    例子2:
function createFunction() { var result = new Array(); for( var i = 0; i<10; i++) { result[i] = function(num) { return function() { return num; }; }(i); } return result; } var bb = createFunction(); alert(aa[0]()); //0 alert(aa[1]()); //1 

在例子2中,我們沒有直接將閉包賦值給數組,而是定義了一個匿名函數,並將立即執行該匿名函數的結果賦值給數組。對於立即執行的匿名函數來說,由於外部無法引用它內部的變量,因此在執行完后很快就會被釋放。所以這里的匿名函數有一個參數num,也就是最終的函數要返回的值。在調用每個匿名函數時,我們傳入了變量i。由於函數是按值傳遞的,所以會將變量i的當前值賦值給參數num,而這個匿名函數內部,又創建並返回了一個返回num的閉包。這樣一來,result數組中的每個函數都有自己num的一個副本,因此就可以返回各自不同的數值了。



作者:CPChen
鏈接:https://www.jianshu.com/p/a72395741c50
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM