摘要:可以使用函數表達式的形式將結果賦值給變量即使把函數賦值給了另一個變量函數的名字依然有效所以遞歸依然可以正常運行。
最近一直在復習鞏固知識,以前寫的筆記現在也在看,為了勘誤之前的理解,加深印象,把markdown上所寫的又拿下來再敘述一遍,章節順序都是按照當時看《高程》的順序整理的,如有不對的地方還請拍磚指教,感謝!
========================================================================
函數表達式遞歸、閉包、模仿塊級作用域、私有變量
定義函數有兩種方式,函數聲明和函數表達式,函數聲明的語法是:
function functionName(arguments){ //函數體; }
函數聲明的一個重要特征就是函數聲明提升,在執行代碼之前,都會讀取函數聲明,以下的例子不會出現錯誤:
sayHi(); function sayHi(){ alert("hi"); }
函數表達式的語法是:
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,那么在執行函數就會出錯。
var other = sum; sum = null; other(4);//sum isn"t a function;
使用callee()方法可以解耦,callee保存的是擁有這個arguments參數的函數。
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、value1、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仍然可以作為外部函數的活動對象被引用。通過模仿塊級作用域可以避免全局變量被污染以及函數的命名沖突,而且可以減少閉包占用的內存問題,因為沒有指向匿名函數的引用,只要函數執行完畢,就可以立即銷毀作用域鏈了。
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());//MichaelPerson構造函數與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; }();
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82471.html
摘要:第一種方法是上面已經說到的匿名函數表達式,即將匿名函數賦值給一個變量,然后通過變量名去調用。因為函數聲明是不能被執行符號執行的,除非通過函數名調用,只有表達式才能被執行符號執行匿名函數又沒有函數名,另外的辦法就是把它變成表達式。 函數聲明 function funcName(){ }; console.log(funcName); // 打印結果是funcName這個函數體。 聲明一...
摘要:關鍵詞必須是小寫的,并且必須以與函數名稱相同的大小寫來調用函數。當調用函數時,這些標識符則指代傳入函數的實參。函數表達式其實是忽略函數名稱的,并且不可以使用函數名這種形式調用函數。注意構造函數無法指定函數名稱,它創建的是一個匿名函數。 一、關于函數 JavaScript函數是指一個特定代碼塊,可能包含多條語句,可以通過名字來供其他語句調用以執行函數包含的代碼語句。 比如我們有一個特定的...
摘要:函數表達式的值是在運行時確定,并且在表達式賦值完成后,該函數才能調用 1.定義 在javascript中我們定義函數有以下兩種方式: 函數聲明 function say(){ console.log(函數聲明); } 函數表達式 var say = function(){ console.log(函數表達式); } 2.實例解析 在平時開發中,...
摘要:標識符有效性正是導致函數語句與函數表達式不同的關鍵所在下一小節我們將會展示命名函數表達式的具體行為。歸根結底,只有給函數表達式取個名字,才是最穩妥的辦法,也就是使用命名函數表達式。 前言 網上還沒用發現有人對命名函數表達式進去重復深入的討論,正因為如此,網上出現了各種各樣的誤解,本文將從原理和實踐兩個方面來探討JavaScript關于命名函數表達式的優缺點。簡單的說,命名函數表達式只有...
摘要:函數聲明和函數表達式的區別函數聲明只能出現在程序或函數體內。所以,在等語義為語句的代碼塊中存在函數聲明,由于函數提升特性,會破壞掉原本的語義。 這篇談一下JS函數聲明與函數表達式的區別及要注意的地方: 函數聲明主要有兩種類型: 函數聲明 function fn() {}; 函數表達式 var fn = function () {}; 這兩種函數創建方式...
閱讀 4933·2021-11-25 09:43
閱讀 1186·2021-11-24 09:38
閱讀 1892·2021-09-30 09:54
閱讀 2800·2021-09-23 11:21
閱讀 2367·2021-09-10 10:51
閱讀 2368·2021-09-03 10:45
閱讀 1163·2019-08-30 15:52
閱讀 1766·2019-08-30 14:13