遞回和閉包作為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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/177553.html
標籤:JavaScript
