閉包函數


閉包函數

什么是閉包函數?

閉包函數是一種函數的使用方式,最常見的如下:

function fn1(){ function fn(){ } return fn; }

這種函數的嵌套方式就是閉包函數,這種模式的好處是可以讓內層函數訪問到外層函數的變量,並且讓函數整體不至於因為函數的執行完畢而被銷毀。

例如:

function fn1(){ var a =10; function fn(){ console.log(a); // 10 } return fn; } 

再比如下面的代碼,隨着函數的每次執行,變量的值都會進行遞增1,原因是因為外層函數的變量處於內層函數的作用域鏈當中,被內層函數所使用着,當js垃圾回收機制讀取到這一情況后就不會進行垃圾回收。

例如:

function fn1(){ var a = 1; function fn(){ a++; console.log(a); } return fn; } // 調用函數 var x = fn1(); x(); // 2 x();//3

閉包函數在js的開發當中是非常常見的寫法,例如下面這種寫法,功能是實現了對數組的一些常規操作的封裝,也是屬於對閉包函數的一種應用。

let Utils = (function(){ var list = []; return { add:function(item){ if(list.indexOf(item)>-1) return; // 如果數組內元素存在,那么不在重復添加 list.push(item); }, remove:function(item){ if(list.indexOf(item) < 0) return; // 如果要刪除的數組數組之內不存在,那么就返回 list.splice(list.indexOf(item),1); }, get_length:function(){ return list.length; }, get_showData:function() { return list; } } })(); Utils.add("hello,world"); Utils.add("this is test"); console.log(Utils.get_showData());// ["hello,world","this is test"]

在上面的代碼中,函數嵌套函數形成了閉包函數的結構,在開發中是比較常見的寫法。

閉包的概念:

閉包是指有權限訪問上一級父作用域的變量的函數.

立即執行函數(IIFE)

在js開發中,經常碰到立即執行函數的寫法。大體如下:

// 下面的這種寫法就是立即執行函數 // 在函數內部的內容會自動執行 (function(){ var a = 10; var b = 20; console.log(a+b); // 30 })();

我們也可以通過第二個括號內傳入參數:

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

這種自調用的寫法本質上來講也是一個閉包函數

通過這種閉包函數,我們可以有效的避免變量污染等問題,從而創建一個獨立的作用域。

但是問題相對來說也很明顯,就是在這個獨立的作用域當中,我們沒有辦法將其中的函數或者變量讓外部訪問的到。所以如果我們在外部需要
訪問這個立即執行函數中的變量或者方法,我們就需要通過第二個括號將window這個全局的變量對象傳入,並且將需要外部訪問的變量或者函數賦值
給window,這樣做相當於將其暴露在了全局的作用域范圍之內。

需要注意的是,通常情況下我們只需要將必要的方法暴露,這樣才能保證代碼並不會相互產生過多的影響,從而降低耦合度。

例如:

(function (window){ var a = 10; // 私有屬性 function show(){ return a++; } function sayHello(){ // 私有方法 alert("hello,world"); } window.show = show;// 將show方法暴露在外部 })(window); 

需要理解的是,在很多的代碼中,總是在(function(){})()的最前面加上一個;,目的是為了防止合並代碼的時候js將代碼解析成(function(){})()(function(){})()這種情況。

閉包函數的變異

因為js的特殊性,所以很多時候我們在學習js的時候,除了js代碼的語法以外,還要學習很多為了解決實際問題的方案,例如下面的這種寫法就是為了
實現module的寫法。

例如:

var testModule = function(){ var name = "張三"; // 私有屬性,外部無法訪問 return { get_name:function(){ // 暴露在外部的方法 alert(name); }, set_name:function(new_name){ // 暴露在外部的方法 name = new_name; } } }

我們也可以將這種寫法進行升級,和立即執行函數進行適度的結合也是常見的寫法:

例如:

var blogModule = (function (my) { my.name = "zhangsan"; // 添加一些功能 my.sayHello = function(){ console.log(this.name) } return my; } (blogModule || {})); console.log(blogModule.sayHello())

自調用函數(自執行匿名函數(Self-executing anonymous function))和立即執行函數的區別

自調用函數其實也就是遞歸函數

自調用函數顧名思義,就是調用自身的函數,而立即執行函數則是立即會及執行的函數。

下面是二者的一些比較:

// 這是一個自執行的函數,函數內部執行自身,遞歸 function foo() { foo(); } // 這是一個自執行的匿名函數,因為沒有標示名稱 // 必須使用arguments.callee屬性來執行自己 var foo = function () { arguments.callee(); }; // 這可能也是一個自執行的匿名函數,僅僅是foo標示名稱引用它自身 // 如果你將foo改變成其它的,你將得到一個used-to-self-execute匿名函數 var foo = function () { foo(); }; // 有些人叫這個是自執行的匿名函數(即便它不是),因為它沒有調用自身,它只是立即執行而已。 (function () { /* code */ } ()); // 為函數表達式添加一個標示名稱,可以方便Debug // 但一定命名了,這個函數就不再是匿名的了 (function foo() { /* code */ } ()); // 立即調用的函數表達式(IIFE)也可以自執行,不過可能不常用罷了 (function () { arguments.callee(); } ()); (function foo() { foo(); } ());

作用域與作用域鏈

  1. 作用域

所謂的作用域,指的就是變量函數可訪問范圍,控制着變量和函數的可見性與生命周期,
在JavaScript中變量的作用域有全局作用域函數作用域以及ES6新增加的塊級作用域

例如,在函數外部通過var關鍵字聲明的變量就是全局變量,作用域的范圍也就是全局作用域,而在函數內部
通過var聲明或者let聲明的就是局部變量,作用域僅限於函數內部,在{}內部或者流程控制語句或者循環語句內部
通過let聲明的變量作用域范圍則僅限於當前作用域。函數的參數在()內部,只能在函數內部使用,作用域范圍也僅限於函數。
同時window對象的所有屬性也擁有全局作用域。

例如:


// 作用域范圍 var a = 10; // 全局 function fn1(a,b){ // 函數fn1內 c = 30; // 全局 var x = 30; // 函數fn1內 function fn2(){ var s = "hello"; // 函數fn2內 console.log(x); // 30 函數內部可以訪問外層的變量 } } for(var i =0;i<10;i++){} // 循環體內聲明的計數變量i也是一個全局 console.log(i); // 10 for(let j = 0;i<10;j++){} // let 聲明的計數變量j 是一個局部 console.log(j);// 出錯,訪問不到
  1. 執行環境上下文

上面我們說到了作用域,下面再來說下執行環境(execution context)。

什么是執行環境呢?

簡單點說,執行環境定義了變量或者函數有權訪問的其他的數據,並且也決定了他們各自的行為。

需要知道的是,每一個執行環境當中,都有着一個與之關聯的變量對象(variable object),執行環境中定義的所有變量和函數都會保存在這個對象中,解析器在處理數據的時候就會訪問這個內部對象。

而全局執行環境是最外層的一個執行環境,在web瀏覽器中最外層的執行環境關聯的對象是window,

所以我們可以這樣說,所有的全局變量和函數 都是作為window對象的屬性和方法創建的。

我們創建的每一個函數都有自己的執行環境,當執行流進行到函數的時候,函數的環境會被推入到一

個函數執行棧當中,而在函數執行完畢后執行環境出棧並被銷毀,保存在其中的所有變量和函數定義隨之銷毀,

控制權返回到之前的執行環境中,全局的執行環境在應用程序退出(瀏覽器關閉)才會被銷毀。

  1. 作用域鏈

當代碼在一個執行環境執行之時,會創建一個變量對象的一個作用域鏈(scope chain),來保證在執行環境中
,對執行環境有權訪問的變量和函數的有序訪問。

作用域第一個也d就是頂層對象始終是當前執行代碼所在環境的變量對象(VO)。

例如:

function fn1(){}

fn1在創建的時候作用域鏈被添加進全局對象,全局對象中擁有所有的全局變量。

例如上面的fn1在創建的時候,所處的環境是全局環境,所以此時的this就指向window。

在函數運行過程中標識符的解析是沿着作用域鏈一級一級搜索的過程,從第一個對象開始,逐級向后回溯,直到找到同名標識符為止,找到后不再繼續遍歷,找不到就報錯。

如果執行環境是函數,那么將其活動對象(activation object, AO)作為作用域鏈第一個對象,第二個對象是包含環境,下一個是包含環境上一層的包含環境...

也就是說所謂的作用域鏈,就是指具體的某個變量或者函數從其第一個對象(活動對象)一直到頂層執行環境。這中間的聯系就是作用域鏈。

被人誤解的閉包函數

談及閉包函數的概念,經常會有人錯誤的將其理解為從父上下文中返回內部函數,甚至理解成只有匿名函數才能是閉包。

而實際來說,因為作用域鏈,使得所有的函數都是閉包(與函數類型無關: 匿名函數,FE,NFE,FD都是閉包)。

注意:這里只有一類函數除外,那就是通過Function構造器創建的函數,因為其[[Scope]]只包含全局對象。

閉包函數的應用

閉包函數是js當中非常重要的概念,在諸多的地方可以應用到閉包,通過閉包,我們可以寫出很多優秀的代碼,下面是一些常見的內容:

例如:

// 數組排序 [1, 2, 3].sort(function (a, b) { ... // 排序條件 }); // map方法的應用,根據函數中定義的條件將原數組映射到一個新的數組中 [1, 2, 3].map(function (element) { return element * 2; }); // [2, 4, 6] // 常用的 forEach [1, 2, 3].forEach(function (element) { if (element % 2 != 0) { alert(element); } }); // 1, 3

例如我們常用的call和apply方法,它們是兩個應用函數,也就是應用到參數中的函數(在apply中是參數列表,在call中是獨立的參數):

例如:

(function () { alert([].join.call(arguments, ';')); // 1;2;3 }).apply(this, [1, 2, 3]);

還有最常使用的寫法:

var a = 10; setTimeout(function () { alert(a); // 10, after one second }, 1000);

當然,ajax的寫法也就是回調函數其實本質也是閉包:

//... var x = 10; // only for example xmlHttpRequestObject.onreadystatechange = function () { // 當數據就緒的時候,才會調用; // 這里,不論是在哪個上下文中創建 // 此時變量“x”的值已經存在了 alert(x); // 10 }; //...

當然也包括我們上邊說的封裝獨立作用域的寫法:

例如:

var foo = {}; // 初始化 (function (object) { var x = 10; object.getX = function _getX() { return x; }; })(foo); alert(foo.getX()); // 獲得閉包 "x" – 10


免責聲明!

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



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