遞歸和閉包作為js中很重要的一環,幾乎在前端的面試中都會涉及,特別閉包。今天前端組的組長冷不丁的問了我一下,粗略的回答了一下,感覺不太滿足,於是重新學習了一下,寫下本篇。 在說這個兩個概念之前,我們先回顧一下函數表達式。 function實際上是一種引用對象,和其他引用類型一樣,都有屬性和方法。定義 ...
遞歸和閉包作為js中很重要的一環,幾乎在前端的面試中都會涉及,特別閉包。今天前端組的組長冷不丁的問了我一下,粗略的回答了一下,感覺不太滿足,於是重新學習了一下,寫下本篇。
在說這個兩個概念之前,我們先回顧一下函數表達式。
function實際上是一種引用對象,和其他引用類型一樣,都有屬性和方法。定義函數有函數聲明、函數表達式、以及構造函數
這裡說一下前兩種。
函數聲明
函數聲明語法如下
function functionName(arg1,arg2,arg3){ //函數體 }
其中有一個很重要的特征:函數聲明提升
saber(); function saber() { console.log("excalibar!")//excalibar! }
很奇怪吧,沒有報錯!這是因為在執行這個函數之前,會讀取函數聲明。
函數表達式
函數表達式語法如下:
這種情況下的函數也叫作匿名函數。
var functionName = function(arg1,arg2,arg3) { //函數體 }
但是有一點,同比上面的函數聲明,在使用函數表達式之前得先賦值,不然會報錯
saber(); var saber = function() { console.log("excalibar!")//saber is not a function }
因此,在使用函數表達式之前,必須var saber()
遞歸
好了,哆嗦了一大堆,開始說主題
定義:一個函數通過名字調用自身
很簡單,擼個代碼看看:
//階乘遞歸 function saber(num) { if(num <= 1){ return 1; }else{ return num*saber(num-1)//saber可以換成arguments.callee,這種方式能確保函數怎麼樣都不會出現問題 } } saber(3) console.log(saber(3))//6
這是一個階乘遞歸,但是通過函數名調用自身,可能存在一些弊端,假如,我在外部定義了saber為空的話,就會報錯,因此使用arguments.callee,能確保函數不會出錯。但是也有缺點,在嚴格模式下arguments.callee無法被訪問到。
因此我們可以使用命名函數表達式來解決這個問題
var saber = (function f(num) { if(num <= 1){ return 1; }else{ return num * f(num-1) } }) saber(3) console.log(saber(3))//6
這樣,即便把函數賦值給另一個變數,f()依然有效,而且不受嚴格模式影響
閉包
js很噁心難懂的地方來了
要理解閉包,最重要的一點就是搞清楚作用域鏈,這玩意對理解閉包有很大的作用,作用域鏈可以看我之前的一篇博客https://www.cnblogs.com/SaberInoryKiss/p/11770974.html
定義:指有權訪問另一個函數作用域中的變數的函數
創建閉包:在A函數內部創建另一個函數B
function createSavant(propertyName) { return function(object1,object2) { var value1 = object1[propertyName]; console.log(value1)//saber var value2 = object2[propertyName]; if (value1 < value2){ return -1; }else if(value1 >value2){ return 1; }else{ return 0; } }; } var savant = createSavant("name") var result = savant({name:"saber"},{name:"archer"});savant = null;//解除對匿名函數的引用(以便釋放記憶體)
console.log(result)//0因為字元串不能比較大小,所以返回0,如果設為數字的話,比如var result = savant({name:“1”},{name: “2”}),會返回-1
上面的代碼就是一串閉包,我們在函數creatSavant裡面創建了一個返回函數。我這裡用簡單的大白話解釋一下:
首先,在函數creatSavant里創建的函數會包含外部函數的作用域鏈,也就是說return function()這玩意的作用域鏈中會包含外部creatSavant的活動對象
因此,return function()能夠訪問外部createSavant裡面定義的所有變數,比如上面例子中的value1的值就是訪問外部定義的變數得來的
然後,當函數creatSavant執行完了之後,由於return function()這家伙的作用域鏈還在引用外部creatSavant的活動對象,因此即使creatSavant的執行環境的作用域鏈被銷毀了,creatSavant的對象還是會保存在記憶體中,供內部函數return function()來引用
最後,直到匿名函數結束了罪惡的一生,被銷毀了。外部環境creatSavant的活動對象才會被銷毀。
可能說的話比較直白,有些地方不專業,大家可以指出錯誤,我會虛心學習的。
下麵來說說閉包的優缺點把:
閉包的缺點
(1)占用過多記憶體
首當其衝的,由於閉包會攜帶包含它的函數的作用域,因此會比其他正常的函數占用更多的記憶體。mmp,比如相同體型的人,我比別人多一個啤酒肚,重量不重才怪。所以慎重使用閉包。
(2)閉包只能取到包含任何變數的最後一個值(重要)
這個缺點在很多筆試題面試題中都會出,舉個例子:
function creatFunction() { var result = new Array(); for(var i = 0; i < 10; i++){//var的變數提升機制,導致了最後i只有10這一次 result[i] = function() { return i; }; } return result; } var saber = creatFunction(); for (var i = 0; i < saber.length; i++) { console.log(saber[i]())//10 10 10 10 10 10 10 10 10 10 }
上面的代碼看上去,迴圈的每個函數都應該返回自己的索引值,即0 1 2 3 4 5 6 7 8 9。但實際上確返回了十個10。原因如下:
每個函數的作用域鏈中都保存了creatFunction()函數的活動對象,所以,其實他們都引用了同一個變數 i,結果當creatFuction()返回後,i的值為10,10被保存了下來,於是每個函數都引用著這個值為10的變數i,結果就如上面代碼所示了。
那麼如何解決這個問題呢:
方法一、創建另一個匿名函數,強制達到預期效果:
function creatFunction() { var result = new Array(); for(var i = 0; i < 10; i++){ result[i] = function(num) { return function() { return num; }; }(i);//會生成很多作用域 } return result; } var saber = creatFunction(); for (var i = 0; i < saber.length; i++) { console.log(saber[i]())//0 1 2 3 4 5 6 7 8 9 }
如上面添加的代碼,這裡沒有將閉包直接賦值給數組,而是定義了一個匿名函數,並將匿名函數的結果傳給數組,在調用匿名函數的時候傳入了i,由於函數是按值傳遞的,迴圈的每一個i的當前值都會複製給參數num,然後在匿名函數function(num)的內部,又創建並返回了一個訪問num的閉包
。最終,result數組中的每一個函數都有一個對應的num的副本,就可以返回各自不同的值了。。。。
這種說法好像不好理解,說直白一點,就是把每個i的值都賦給num,然後把所有的num的值放到數組中返回。避免了閉包只取到i的最後一個值得情況的發生。
方法二、使用let
因為es5沒有塊級作用域這一說法,在es6中加入了let來定義變數,使得函數擁有了塊級作用域
function creatFunction() { var result = new Array(); for(let i = 0; i < 10; i++){//let不存在變數提升,每一次迴圈都會執行一次,for的每一次迴圈都是不同的塊級作用域,let聲明的變數都有塊級作用域,所以不存在重覆聲明 result[i] = function() { return i; }; } return result; } var saber = creatFunction(); for (var i = 0; i < saber.length; i++) { console.log(saber[i]())//1 2 3 4 5 6 7 8 9 }
(3)閉包導致this對象通常指向windows
var name = "I am windows" var object = { name: "saber", getName : function() { return function() { return this.name } } } console.log(object.getName()())//I am windows
this是基於函數的執行環境綁定的,而匿名函數的執行環境具有全局性,因此this對象指向windows
解決辦法,把外部作用域中的this對象保存在一個閉包也能訪問到的變數里:
var name = "I am windows" var object = { name: "saber", getName : function() { var that = this return function() { return that.name } } } console.log(object.getName()())//saber
(4)記憶體泄漏
由於匿名函數的存在,導致外部環境的對象會被保存,因此所占用的記憶體不會被垃圾回收機制回收。
function Savant(){ this.age = "18"; this.name = ["saber","archer"]; } Savant.prototype.sayName = function(){ var outer = this; return function(){ return outer.name }; }; var count = new Savant(); console.log(count.sayName()())//[ 'saber', 'archer' ]
我們可以保存變數到一個副本中,然後引用該副本,最後設置為空來釋放記憶體
function Savant(){ this.age = "18"; this.name = ["saber","archer"]; } Savant.prototype.sayName = function(){ var outerName = this.name; return function(){ return outerName }; outerName = null; }; var count = new Savant(); console.log(count.sayName()())//[ 'saber', 'archer' ]
註意一點:即使這樣還是不能解決記憶體泄漏的問題,但是我們能解除其引用,確保正常回收其占用的記憶體
說完了缺點,我們來說一下,閉包的優點把
閉包的優點
(1)模仿塊級作用域
語法:
(function(){ //在這裡是塊級作用域 })();
舉個例子來說明吧:
function saber(num){ for(var i = 0; i < num; i++){ console.log(i)//0 1 2 3 4 } // console.log(i)//5 } saber(5)
可以看到在for迴圈外還是能訪問到i的,那麼,如何裝for迴圈外無法訪問到裡面的i呢
function saber(num){ (function () { for(var i = 0; i < num; i++){ // console.log(i)//0 1 2 3 4 } })(); console.log(i)//i is not defined } saber(5)
這種方式能減少閉包占用的記憶體問題。
(2)在構造函數中定義特權方法
function savant(name){ var name=name; this.sayName=function(){ console.log(name); } }; var savant1=new savant("saber"); var savant2=new savant("archer"); savant1.sayName(); //saber savant2.sayName(); //archer
該例子中的sayName()就是一個特權方法,可以理解為可以用來訪問私有變數的公有方法。
(3)靜態私有變數
在私有作用域中同樣可以使用特權方法
(function (){ var name = ""; Savant = function(value) { name = value; } Savant.prototype.getName = function() { return name; } Savant.prototype.setName = function(value) { name = value; } })(); var Savant1 = new Savant("saber") console.log(Savant1.getName())//saber Savant1.setName("archer"); console.log(Savant1.getName())//archer var Savant2 = new Savant("lancer") console.log(Savant1.getName())//lancer console.log(Savant2.getName())//lancer
但是在私有作用域裡面的特權方法和構造函數中不同的是,私有作用域中的特權方法是在原型上定義的,因此所有的實例都使用同一個函數,只要新建一個Savant實例或者調用setName()就會在原型上賦予name一個新值。導致的結果就是所有的實例都會返回相同的值
(4)模塊模式
單例模式添加私有屬性和私有方法,減少全局變數的使用
語法:
var singleleton = (function(){ // 創建私有變數 var privateNum = 10; // 創建私有函數 function privateFunc(){ // 業務邏輯代碼 } // 返回一個對象包含公有方法和屬性 return { publicProperty: true, publicMethod: function() { //共有方法代碼 } }; })();
該模式在需要對單例進行某些初始化,同時又需要維護其私有變數時是很有用的
增強的模塊模式
function Savant() { this.name = "saber"; }; var application = (function(){ // 定義私有 var privateA = "privateA"; // 定義私有函數 function privateMethodA(){}; // 實例化一個對象後,返回該實例,然後為該實例增加一些公有屬性和方法 var object = new Savant(); // 添加公有屬性 object.publicA = "publicA"; // 添加公有方法 object.publicB = function(){ return privateA; } // 返回該對象 return object; })(); Savant.prototype.getName = function(){ return this.name; } console.log(application.publicA);// publicA console.log(application.publicB()); // privateA console.log(application.name); // saber console.log(application.getName());// saber