最近一直在複習鞏固知識,之前寫的筆記如今也在看,爲了勘誤以前的理解,加深印象,把markdown上所寫的又拿下來再敘述一遍,章節順序都是按照當時看《高程》的順序整理的,若有不對的地方還請拍磚指教,感謝!前端
========================================================================瀏覽器
遞歸、閉包、模仿塊級做用域、私有變量安全
定義函數有兩種方式,函數聲明和函數表達式,函數聲明的語法是:markdown
function functionName(arguments){ //函數體; }
函數聲明的一個重要特徵就是函數聲明提高,在執行代碼以前,都會讀取函數聲明,如下的例子不會出現錯誤:閉包
sayHi(); function sayHi(){ alert("hi"); }
函數表達式的語法是:app
var functionName = function(arguments){ //函數體; }
這種狀況下建立的函數是匿名函數,匿名函數的name屬性是空字符串,若是以這樣的方式調用會出現錯誤:
sayHi();//錯誤,函數還不存在; var sayHi = function(){ alert("hi"); }
若是使用if...else語句判斷一個條件,執行同一個functionName的函數聲明,JavaScript的引擎會嘗試修正錯誤,將其轉換爲合理的狀態,而修正的機制不同,大多數會在condition爲true時返回第一個聲明,少部分會爲第二個聲明,所以在if...else中最好使用函數表達式,它會根據condition賦值給變量返回正確的函數結果。經過condition以後,函數賦值給變量,再選擇執行函數。
var sayHi; if(condition){ sayHi = function(){ alert("Hi"); } }else{ sayHi = function(){ alert("Yo!"); } } sayHi();
可以建立函數賦值給變量,固然也能夠把函數做爲其它函數的值返回。如下:函數
function compare(propertyName){ return function(object1,object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if(value1 < value2){ return -1; }else if(value1 > value2){ return 1; }else{ return 0; } } }
遞歸
遞歸函數即本身調用本身,一個階乘函數:優化
function sum(num){ if(num<=1){ return 1; }else{ return num * sum(num-1); } }
這樣一看好像沒有問題,可是若是把函數存在變量中,而後改變函數的引用爲null,那麼在執行函數就會出錯。this
var other = sum; sum = null; other(4);//sum isn't a function;
使用callee()方法能夠解耦,callee保存的是擁有這個arguments參數的函數。prototype
function sum(num){ if(num<=1){ return 1; }else{ return num * arguments.callee(num-1); } }
而在strict模式下,callee是不可用的,而且考慮到代碼安全的因素callee和caller已經被棄用了,因此儘可能不要使用這個方法。可使用函數表達式的形式,將結果賦值給變量,即便把函數賦值給了另外一個變量,函數的名字依然有效,因此遞歸依然能夠正常運行。
var factorial = function sum(num){ if(num<=1){ return 1; }else{ return num * sum(num-1); } } var other = factorial; factorial = null; other(7);//
ES6中關於尾遞歸的優化
函數調用會在內存造成一個"調用記錄",又稱"調用幀"(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用記錄上方,還會造成一個B的調用記錄。等到B運行結束,將結果返回到A,B的調用記錄纔會消失。若是函數B內部還調用函數C,那就還有一個C的調用記錄棧,以此類推。全部的調用記錄,就造成一個"調用棧"-------摘自阮一峯老師的博客
function fac(n,total=1){ if(n == 1){ return total; } return fac(n - 1,n * total); }
2.閉包
執行環境定義了全部變量或函數有權訪問的其餘數據,決定了它們各自的行爲。每一個執行環境都有與之關聯的變量對象,環境中定義的因此變量和函數都保存在這個對象中。當代碼在執行環境中運行時,會建立變量對象的一個做用域鏈(保證對執行環境有權訪問的全部變量和函數的有序訪問)。 當某個函數被調用時,會建立一個執行環境及相應的做用域鏈,而後arguments和其餘命名參數的值來初始化函數的活動對象,但在做用域鏈中,外部函數的活動對象處於第二位,在它以外的函數處於第三位,...直至做爲做用域終點的全局執行環境。
function compare(value1,value2){ if(value1 < value2){ return -1; }else if(value1 > value2){ return 1; }else{ return 0; } } var result = compare(5,10);
在這段代碼中,當調用compare()函數時,會建立一個包含arguments、value一、value2的活動對象,而全局執行環境的變量對象result、compare則在這個做用域鏈的第二位。 每一個執行環境都有一個表示變量的對象-----變量對象,全局環境的對象會一直存在,而compare函數裏的局部環境的變量對象,只在函數執行時存在,建立compare函數時,會建立一個預先包含了全局變量對象compare、result的做用域鏈,此後又有一個活動對象被建立並被推入做用域鏈的前端。compare()的活動對象是arguments[5,10],value1 : 5,value2 : 10。因此此時的做用域鏈包含了本地活動對象和全局變量對象。做用域鏈是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
通常來講,當函數執行完畢後,局部活動對象就會銷燬,內存中僅保存全局做用域,可是閉包的狀況有所不一樣。
在一個函數內部定義的函數,會將外部函數的活動對象添加到它的做用域鏈中。
function createCompare(propertyName){ return function(object1,object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if(value1 < value2){ return -1; }else if(value1 > value2){ return 1; }else{ return 0; } } } var compare = createCompare("name"); var result = compare({name : "Nicholas"},{name : "Greg"});
匿名函數在被返回以後,它的做用域鏈被初始化爲外部函數的活動對象以及全局變量對象,所以它能夠訪問外部函數的全部變量,而且在外部函數執行完畢後,外部函數的做用域鏈會被銷燬,可是它的活動對象仍然保存着,由於此時匿名函數還在引用這個活動對象arguments:"name",propertyName : name。若是:
compare = null;
那麼這時就會解除對匿名函數的引用,以便釋放內存。
1)閉包與變量
閉包的一個反作用就是隻能取得外部函數中任何變量的最後一個值,如下例子能夠說明:
function create(){ var result = new Array(); for(var i = 0;i<10;i++){ result[i] = function(){ return i; } } return result; } console.log(result);
在這個函數內部返回result時,它能夠引用外部函數的活動對象以及全局變量對象,而當create()執行完以後,這個活動對象仍然在被引用,此時i已經變爲了10,因此result只會返回同一個i值,即i=10;能夠修改成:
function create(){ var result = new Array(); for(var i = 0;i<10;i++){ result[i] = function(num){ return function(){ return num; } }(i); } return result; } console.log(create());
修改後的函數閉包獲得的值並不直接賦值給result,而是經過閉包,使這個匿名函數的內部再返回一個匿名函數,這個匿名函數能夠訪問外部的活動對象num,再經過給內部的函數傳遞變量i,賦值給num,因此最後能夠獲得function(1)、function(2)...function(10),而且他們的內部屬性[[scope]]還會包含對應的i值。
2)關於this對象
匿名函數的執行環境具備全局性,所以它的this指向通常指向window
var name = "The Window"; var object = { name : "My object", getName : function(){ return function(){ return this.name; }; } } alert(object.getName()());//The Window
以上例子中,匿名函數的的this指向的是window,因此獲得的是"The Window",
由於this對象實際上是函數執行時的上下文,與如何定義函數並無關係,Object.getName(),指向的是Object對象,可是繼續調用匿名函數,顯然this指向的不是這個對象,此時是全局環境下調用的這個函數,所以this.name爲"The Window".若是在定義匿名函數以前把this對象賦值給一個變量,那麼調用匿名函數時會改變this的指向。
var name = "The Window"; var object = { name : "My object", getName : function(){ var _this = this; return function(){ return _this.name; }; } } alert(object.getName()());//My object
3)內存泄漏
在IE9以前的瀏覽器中,IE中有一部分對象並非原生對象,其DOM、BOM中的對象就是以COM對象的形式實現的,而針對COM對象垃圾回收機制是引用計數,在閉包中很容易出現循環引用的問題,所以可能會出現內存泄漏。
function Handle(){ var elment = document.getElementById("someElement"); element.onclick = function(){ alert(element.id); } }
只要匿名函數存在,對element的引用數也至少爲1,所以它佔用的內存永遠都不會回收,能夠作如下的修改:
function Handle(){ var elment = document.getElementById("someElement"); var id = element.id; element.onclick = function(){ alert(id); } element = null; }
3)模仿塊級做用域
JS沒有塊級做用域的概念,也就是說在塊語句中定義的變量實際上是在包含函數中建立的。如下例子能夠說明:
function outNumbers(){ for(var i = 0;i<10;i++){ alert(i); } var i; alert(i);//10 }
在for循環中i從0-9,i=10會直接給後一個聲明賦值,因此會再次計數,固然在ES6中,若是使用let或const聲明,for語句就會造成塊級做用域。這一節主要討論的是利用匿名函數自執行造成塊級做用域。
若是是匿名函數自執行,那麼在它的內部天然就會造成塊級做用域,例如:
(function(){ //塊級做用域; })();
若是採用函數聲明的形式建立函數表達式:
var someFunction = function(){ //塊級做用域: }; someFunction();
像第一個例子中建立的函數能夠利用匿名函數自執行修改成:
function outNumbers(court){ (function(){ for(var i = 0;i<court;i++){ alert(i); } })(); alert(i);//錯誤 }
匿名函數自執行並不影響閉包的特性,court仍然能夠做爲外部函數的活動對象被引用。經過模仿塊級做用域能夠避免全局變量被污染以及函數的命名衝突,並且能夠減小閉包占用的內存問題,由於沒有指向匿名函數的引用,只要函數執行完畢,就能夠當即銷燬做用域鏈了。
4)私有變量
任何在函數中定義的變量都是私有變量,外部函數不能訪問到這些變量,若是在函數內部建立一個閉包,那麼閉包能夠利用外部函數的活動對象,即外部函數的私有變量,利用這一點能夠建立用於訪問私有變量的特權方法。如下兩種方式均可覺得自定義類型建立私有變量和特權方法。 第一種是在構造函數中定義特權方法,方法以下:
function MyObject(){ var private = 10; function SomeFun = { return false; } this.public = function(){ alert(private++); return someFun(); }; } var person = new MyObject(); console.log(person.public());
在這個方法中必需要建立構造函數,在實例上才能夠調用這個特權方法從而訪問私有變量,可是這樣作會帶來構造函數實例標識符重解析以及不一樣的做用域鏈,這一點和構造函數模式相同。
①.靜態私有變量
第二種方法爲經過私有做用域定於私有變量和函數,依靠原型模式,以下:
(function(){ var private = 10; function PrivateFun(){ return false; } MyObject = function(){ } MyObject.prototype.public = function(){ private++; return PrivateFun(); } })();
使用函數表達式是由於若是使用函數聲明,那麼function內部爲塊級做用域,沒法實現實例化對象的目的,從而也就沒法達到訪問函數內部私有變量和私有函數的目的,而且在匿名函數內部的構造函數也不能聲明,這樣以來變量就成爲了全局變量,可以在這個塊級做用域以外被使用到。示例:
(function(){ var name = ""; Person = function(value){ name = value; }; Person.prototype.setName = function(value){ name = value; }; Person.prototype.getName = function(){ return name; }; })(); var person1 = new Person("Nicholas"); var person2 = new Person("Michael"); alert(person1.getName());//Michael alert(person2.getName());//Michael
Person構造函數與setName()和getName()同樣,均可以訪問匿名函數的私有變量name,變量name就成了靜態的、由全部實例共享的屬性。可是因爲原型鏈的特性,一旦更改了name屬性,那麼全部實例都會受到影響。
閉包和私有變量方法的一個明顯不足之處就是,他們都會多查找做用域鏈的一個層次,這顯然會在必定程度上影響查找速度。
②.模塊模式
若是是隻有一個實例的對象訪問私有變量和函數,在必須以對象的形式建立的前提下,並且須要以某些數據初始化,同時還要公開一些可以訪問這些私有數據的方法,能夠採用這種方式:
var singleton = function(){ var private = 10; function privateFun(){ return false; } return{ public : true, publicMethod : function(){ private++; return privateFun(); } } }();這一個()至關於 singleton(); //當使用公有辦法時,singleton.publicMethod();
爲何使用函數聲明?
這僅僅是一個單例,因此不能將它實例化,僅將函數的返回值以對象的形式返回給變量就可使用公有辦法訪問私有變量、函數。
爲何在內部以對象形式返回?
若是採用this對象形式訪問,這樣至關於構造函數結構,而且在函數聲明的前提下,this對象爲window(嚴格模式下爲undefined)。
下面的這個例子說明模塊模式能夠應用的場景:
var application = function(){ //私有變量和函數 var components = new Array(); //初始化 components.push(new BaseComponent()); //公共 return { getComponent : function(){ return components.length; }, registerComponent : function(component){ if(typeof component == "object"){ components.push(component); } } } }();
在這個單例的公共接口中,前者能夠返回已註冊的組件數目,後者用於註冊新組件。
③.加強的模塊模式
在返回對象以前加入對其加強的代碼,單例是自定義類型的實例,同時還必須添加某些屬性或方法對其增強的狀況下,可使用加強模塊模式。application的例子能夠改寫爲:
var application = function(){ var components = new Array(); components.push(new BaseComponent()); //建立一個自定義類型實例,做爲application的局部副本使用 var app = new BaseComponent(); app.getComponent = function(){ return components.length; }; app.registerComponent = function(component){ if(typeof component == "object"){ components.push(component); } }; //返回副本 return app; }();