整理了JavaScript中函數Function的各種,感覺函數就是一大對象啊,各種知識點都能牽扯進來,不單單是 Function 這個本身原生的引用類型的各種用法,還包含執行環境,作用域,閉包,上下文,私有變數等知識點的深入理解。 函數中的return Function類型函數實際上是對象,每個函
整理了JavaScript中函數Function的各種,感覺函數就是一大對象啊,各種知識點都能牽扯進來,不單單是 Function 這個本身原生的引用類型的各種用法,還包含執行環境,作用域,閉包,上下文,私有變數等知識點的深入理解。
函數中的return
- return 語句可以不帶有任何返回值,在這種情況下( return; 或函數中不含 return 語句時),函數在停止執行後將返回 undefiend 值。這種用法一般在需要提前停止函數執行而又不需要返回值的情況下。
- return false 可以阻止元素的預設事件。
- return 返回的是其所在函數的返回值
function n(){ (function(){ return 5; })(); } n();// undefined //立即執行匿名函數中的return語句其實是返回給它所在的匿名函數的。 function n(){ var num= (function(){ return 5; })(); console.log(num); }
Function類型
函數實際上是對象,每個函數實際上都是 Function 類型的實例。而且與其他引用類型一樣具有屬性和方法。函數名實際上是一個指向記憶體堆中某個函數對象的指針。
定義函數的方式
- 函數聲明
function sum(num1,num2){ return num1+num2; }
- 函數表達式
var sum=function(num1,num2){ return num1+num2; };
定義了一個變數 sum 並將其初始化為一個函數,註意到 function 關鍵字後面並沒有函數名,這是因為在使用函數表達式定義函數,沒必要使用函數名,通過變數 sum 即可引用函數。還要註意函數末尾有個分號,就像聲明其他變數一樣。 - new 構造函數,雖然這種用法也是函數表達式,但該用法不推薦。因為這種語法會導致解析兩次代碼(第一次是解析常規的ECMAScript代碼,第二次是解析傳入構造函數中的字元串),影響性能。
使用 Function 構造函數,構造函數可以接受任意數量的參數,但最後一個參數始終都被看成是函數體,前面的參數則枚舉出了新函數的參數。
var sum=new Function('num1','num2','return num1+num2;'); sum;// function anonymous(num1,num2 /**/) { return num1+num2; }
當使用不帶圓括弧的函數名是訪問函數指針,而非調用函數。
理解參數
ECMAScript中所有參數傳遞的都是值(即使是引用也是傳遞的地址值,不是引用傳遞參數(可參考 JavaScript傳遞參數是按值傳遞還是按引用傳遞))。ECMAScript函數不介意傳遞進來多少個參數,也不在乎傳進來的參數是什麼數據類型。之所以這樣,是因為ECMAScript中的參數在內部是用一個數組表示的。函數接收到的始終都是這個數組,而不關心數組中包含哪些參數。在函數體內,可以通過 arguments 對象來訪問這個數組。從而獲取傳遞給函數的每個參數。
function func(){ console.log(Object.prototype.toString.call(arguments)); } func();// [object Arguments]
- 關於 arguments 的行為,它的值永遠與對應命名參數的值保持同步。因為 arguments 對象中的值會自動反映到對應的命名參數。所以修改 arguments[1] ,也就修改了 num2 。不過這並不是說讀取這兩個值會訪問相同的記憶體空間,它們的記憶體空間是獨立的,但他們值會同步(WHY??),要是JavaScript能直接訪問記憶體就好了驗證一下。
- 但如果只傳入了一個參數,那麼 arguments[1] 設置的值不會反映到命名參數中,這是因為 arguments 對象的長度是由傳入參數個數決定的,不是由定義函數時的命名參數個數決定的,沒有傳遞值的命名參數將自動被賦予 undefiend 值,這就跟定義了變數但沒初始化一樣。
function doAdd(num1,num2){ console.log(arguments.length); console.log(num2) arguments[1]=10; console.log(num2); } doAdd(5,0);//2 0 10 doAdd(5);//1 undefiend undefined
沒有重載
ECMAScript函數不能像傳統意義上那樣實現重載,而在其他語言中(Java),可以為一個函數編寫兩個定義,只要這兩個定義的簽名(接收參數的類型和數量)不同即可。
不能實現重載的原因:
- ECMAScript函數沒有簽名,因為其參數是由包含零個或多個值的數組來表示的。沒有函數簽名,真正的重載是不可能做到的。在ECMAScript中定義兩個名字相同的的函數,則該名字只屬於後定義的函數。如何實現類似於Java中的重載呢,其實可以通過判斷傳入函數的參數類型和個數來做出不同響應。
function reload(){ if(arguments.length==0){ console.log('沒傳參'); }else if(arguments.legth==1){ console.log('傳了一個參數'); } }
- 深入理解:將函數名想象為指針,也有助於理解為什麼ECMAScript中沒有函數重載的概念。
function add(){ return 100; } function add(num){ return num+200; } //實際上和下麵代碼沒什麼區別 function add(){ return 100; } add=function(num){ return num+200; }
函數聲明和函數表達式
實際上解析器在向執行環境中載入數據時,對函數聲明和函數表達式並非一視同仁。
JavaScript運行機制淺探 中瞭解到對於解釋型語言來說,編譯步驟為:
- 詞法分析(將字元流轉換為記號流,是一對一的硬性翻譯得到的是一堆難理解的記號流)
- 語法分析(這裡進行所謂的變數提升操作,其實我覺得是把這些提升的變數保存在語法樹中。要構造語法樹,若發現無法構造就會報語法錯誤,並結束整個代碼塊的解析)
- 之後可能有語義檢查,代碼優化等。得到語法樹後就開始解釋執行了。解釋性語言沒有編譯成二進位代碼而是從語法樹開始執行。
解析器會先讀取函數聲明,並使其在執行任何代碼之前可用。至於函數表達式,則必須等到執行階段才會被真正賦值。什麼意思呢?雖然兩者都進行了變數提升,待真正執行時構造活動對象從語法樹種取聲明添加到執行環境中,但一個是函數提升,一個是變數提升。
//函數聲明 console.log(func);//function func(){} function func(){ } //函數表達式 console.log(func1);// undefined var func1=function(){}; console.log(func1);// function(){}
作為值的函數
因為ECMAScript中的函數名本身就是變數,所以函數也可以作為值來使用。不僅可以像傳遞參數一樣把一個函數傳遞給另一個函數,而且可以將一個函數作為另一個函數的結果返回。
function callSomeFunction(someFunction,someArgument){ return someFunction(someArgument); } function concated(str){ return "Hi "+str; } callSomeFunction(concated,'xx');// 'Hi xx'
從一個函數中返回另一個函數的應用:假設有一個對象數組,想要根據某個對象屬性對數組進行排序,但傳給 sort() 方法的比較函數要接收兩個參數,即要比較的值。我們需要一種方式來指明按照哪個屬性來排序。我們可以定義一個函數它接收一個屬性名,然後根據這個屬性名來創建一個比較函數。預設情況下, sort 函數會調用每個對象的 toString() 方法以確定它們的次序。
function createCompare(property){ return function(obj1,obj2){ var value1=obj1[property], value2=obj2[property]; if(value1<value2) return -1; else if(value1>value2) return 1; else return 0; } }
var data=[{name:'aa',age:20},{name:'bb',age:12},{name:'cc',age:30}]; data.sort(createCompare("age"));// [{name:'bb',age:12},{name:'aa',age:20},{name:'bb',age:30}]
函數的內部屬性
arguments :類數組對象,包含傳入函數中所有參數。是每個函數自身的屬性,之所以可以直接訪問 arguments ,是因為命名空間??以下變化是為了加強JavaScript語言的安全性,這樣第三方代碼就不能在相同的環境下窺視其他代碼了。
- callee 屬性:是一個指針,指向擁有 arguments 對象的函數。嚴格模式訪問會導致錯誤。
//一般階乘函數 function factorial(num){ if(num<=1){ return 1;} else { return num*factorial(num-1); } }
定義階乘函數用到遞歸演算法,這樣定義是沒問題。
缺點:這個函數的執行與函數名 factorial 緊緊耦合在一起。萬一齣現改變函數指向的這種情況就不太好了,factorial=function(){} factorial(3);// undefiend
為了消除這種現象。
-
function factorial(num){ if(num<=1){ return 1; } else{ return num*arguments.callee(num-1); } }
這樣無論引用函數使用的是什麼名字都可以保證完成遞歸。
- caller 屬性:不過在非嚴格模式下這個屬性始終是 undefiend 。即使在嚴格模式下訪問也會出錯。增加這個屬性是為了分清 arguments.caller 和函數對象上的 caller 屬性。
function a(){ return Object.getOwnPropertyNames(arguments); } a();// ["length", "callee"]
this :行為與Java/C#中的 this 大致類似。 this 引用的是函數據以執行環境對象(當在網頁的全局作用域中調用函數時, this 對象引用的就是 window )。
caller :不止是ECMAScript5中新增函數對象上的屬性,還是 arguments 上的屬性。保存著調用當前函數的函數的引用。如果是在全局作用域中調用當前函數,它的值為 null 。
Object.getOwnPropertyNames(Function);// ["length", "name", "arguments", "caller", "prototype"]
function outer(){ inner(); } function inner(){ console.log(inner.caller); //為了實現更鬆散的耦合,arguments.callee.caller } outer();// function outer(){ inner()}
嚴格模式下不能為函數的 caller 屬性賦值,否則會導致出錯。
函數的屬性和方法
- length:表示函數希望接收的命名參數的個數(也就是定義的形參的個數)。
function sayName(name){ // } function sum(num1,num2){ // } function sayHi(){ // } sayName.length;// 1 sum.length;// 2 sayHi.length;// 0
- prototype:對於ECMAScript中的引用類型而言,prototype是保存它們所有實例方法的真正所在。諸如toString和valueOf等方法實際上都保存在Object.prototype名下(原生構造函數比如Function,Array等 在自己原型上重寫了toString)。在ECMAScript5中,prototype屬性是不可枚舉的,因此使用for-in無法發現。 Object.getOwnPropertyDescriptor(Function,'prototype');// Object {writable: false, enumerable: false, configurable: false}
- 每個函數上有兩個可用的方法:apply和call。這兩個方法實際上是在Function.prototype上, Object.getOwnPropertyNames(Function.prototype);// ["length", "name", "arguments", "caller", "apply", "bind", "call", "toString", "constructor"] 它是在JavaScript引擎內部實現的。因為是屬於Function.prototype,所以每個Function的實例都可以用(自定義的函數也是Function的實例)。都是在特定的作用域或自定義的上下文中調用執行函數,實際上等於設置函數體內 this 對象的值。
- apply :參數一為在其中運行函數的作用域,參數二為參數數組(可以是數組,也可以是 arguments 對象)。
function sum(num1,num2){ return num1+num2; } function callSum1(num1,num2){ return sum.apply(this,arguments);//sum.apply(this,[num1,num2]) } callSum1(10,30);// 40
嚴格模式下,未指定環境對象而調用函數, this 值不會轉型為 window 。除非明確把函數添加到某個對象或者調用 apply 或 call ,否則 this 值將是 undefined
- call :參數一沒有變化,變化的是其餘參數都是直接傳遞給函數,參數必須都列出來。
function callSum1(num1,num2){ retrun sum.call(this,num1,num2); } callSum1(10,30);// 40
call 和 apply 真正強大的地方是能夠擴充函數賴以運行的作用域,改變函數的執行環境。
- bind :ECMAScript5定義的方法,也是 Function.prototype 上的方法。用於控制函數的執行上下文,返回一個新函數,這個函數的 this 值會被綁定到傳給 bind() 函數中的值。
window.color="red"; var o={color:'blue'}; function sayColor(){ console.log(this.color); } var newobj=sayColor.bind(o); newobj;// function sayColor(){ console.log(this.color); } newobj==sayColor;// false newobj();// blue
深入理解:可以將函數綁定到指定環境的函數。接收一個函數和一個環境,返回在給定環境中調用給定函數的函數。
function bind(func,context){ return function(){ func.apply(context,arguments);//這裡創建了一個閉包,arguments使用的返回的函數的,而不是bind的 } }
當調用返回的函數時,它會在給定環境中執行被傳入的函數並給出所有參數。
function bind(func,context,args){ return function(){ func.call(context,args); }; }
- toString,toLocaleString :返回函數代碼的字元串形式,返回格式因瀏覽器而異,有的返回源碼,有的返回函數代碼的內部表示,由於存在差異,用這個也實現不了什麼功能。
- valueOf :返回函數的自身引用。
變數,作用域,記憶體問題
JavaScript接近詞法作用域,變數的作用域是在定義時決定而不是在執行時決定,也就是說詞法作用域取決於源碼。
JavaScript引擎在執行每個函數實例時,都會為其創建一個執行環境,執行環境中包含一個AO變數對象,用來保存內部變數表,內嵌函數表,父級引用列表等語法分析結構(變數提升在語法分析階段就已經得到了,並保存在語法樹中,函數實例執行時會將這些信息複製到AO上)。
ECMA-262定義,JavaScript鬆散類型的本質決定了它只在特定時間用於保存特定值的一個名字而已,由於不存在定義某個變數必須要保存何種數據類型值得規則,變數的值及其數據類型可在腳本的生命周期內改變。
- 基本類型和引用類型的值:ECMAScript變數可能包含兩種不同數據類型的值:基本類型值,引用類型值。
- 基本類型值:簡單的數據段。
- 引用類型值:那些可能由多個值構成的對象。是保存在記憶體中的對象,JavaScript不允許直接訪問記憶體中的位置,也就說不能直接操作對象的記憶體空間。在操作對象時實際上是在操作對象的引用而不是實際的對象。為此,引用類型值是按引用訪問的。(這種說法不嚴密,當複製保存著對象的某個變數時,操作的是對象的引用。但在為對象添加屬性時,操作的是實際的對象)
在將一個值賦給變數時,解析器必須確定這個值是基本類型值還是引用類型值。5種基本數據類型: Undefined,Null,Boolean,Number,String (很多語言中字元串以對象形式來表示因此被認為是引用類型,但ECMAScript放棄這一傳統)。這5種基本類型是按值訪問的,因此可以操作保存在變數中的實際的值。
- 動態的屬性
- 複製變數的值:在從一個變數向另一個變數複製基本類型值和引用類型值時,也存在不同。
如果從一個變數向另一個變數複製基本類型的值,會在變數對象上創建一個新值,然後把該值複製到為新變數分配的位置上。
當從一個變數向另一個變數賦值引用類型值值時,同樣也會將存儲在變數對象中的值複製一份放到為新變數分配的空間中,不同的是,這個值的副本實際上是個指針(可以理解為複製了地址值),而這個指針指向存儲在堆中一個對象。複製操作結束後兩個變數實際上將引用同一個對象。 - 傳遞參數:ECMAScript中所有函數的參數都是按值傳遞的,把函數外部的值複製給函數內部的參數,就和把值從一個變數複製到另一個變數一樣。基本類型值得傳遞如同基本類型變數的複製一樣,引用類型值的傳遞如同引用類型變數的複製一樣。很多人錯誤認為:在局部作用域中修改的對象會在全局作用域中反映出來這就說明是按引用傳遞的。為了證明對象是按值傳遞的,
function setName(obj){ obj.name="xx"; obj=new Object(); obj.name="bb"; } var p=new Object(); setName(p); p.name;// "xx"
如果是按引用傳遞的,即傳遞的不是地址值而是堆記憶體中整個p對象,在 setName 中為其添加了一個新名字叫 obj ,又給其添加 name 屬性後,將這個 obj 內容重新填充為新對象,那麼之前的那個對象就不存在了更別說有 "xx" 的名字屬性,但是 p.name 仍然訪問到了。這說明即使在函數內部修改了參數值,但原始的引用仍然保持未變。實際上,當在函數內部重寫 obj 時,這個變數引用的就是一個局部對象了,而這個局部對象會在函數執行完畢後被立即銷毀。
- 類型檢測:檢測一個變數是不是基本數據類型用 typeof 是最佳工具,但如果變數的值是除了函數的對象或 null typeof [];// "object" typeof null;// "object" ,變數值為函數時 typeof function(){};// "function" (ECMA-262規定任何在內部實現 [[call]] 方法的對象都應該在應用 typeof 操作符返回 "function" )。但在檢測引用類型值時,這個操作符用處不大,因為我們並不是想知道它是個對象,而是想知道它是某種類型對象。如果變數是給定引用類型的實例, instanceof 操作符會返回 true 。所有引用類型值都是 Object 的實例。如果使用 instanceof 操作符檢測基本類型的值,則該操作符始終會返回 false ,因為基本類型不是對象。
執行環境及作用域
- 執行環境(execution context):也稱為作用域,定義了變數或函數有權訪問的其他數據,決定了它們各自的行為。全局執行環境是最外圍的一個執行環境,跟據ECMAScript實現所在的宿主環境不同,表示執行環境的對象也不一樣,web瀏覽器中全局執行環境是 window 對象。某個執行環境中所有代碼執行完畢後該環境被銷毀,保存在其中的所有變數和函數定義也隨之銷毀(全局執行環境直到應用程式退出例如關閉網頁或瀏覽器時才被銷毀)。每個函數都有自己的執行環境,當執行流進入一個函數時,函數的環境就會被推入一個環境棧中,在函數執行後,棧將其環境彈出,將控制權返回給之前的執行環境。ECMAScript程式中的執行流正是由這個機制控制著。函數的每次調用都會創建一個新的執行環境。執行環境分為創建和執行兩個階段,
- 創建:解析器初始化變數對象或者活動對象,它由定義在執行環境中的變數,函數聲明,參數組成。在這個階段,作用域鏈會被初始化,this的值也最終會被確定。
- 執行:代碼被解釋執行
- 變數對象(variable object):環境中定義的所有變數和函數都保存在這個對象中。雖然用代碼無法訪問它,但解析器在處理數據時會在後臺使用它。如果這個環境是函數,則將活動對象(activation object)作變數對象
- 作用域(scope)和上下文(context):函數的每次調用都有與之緊密相關的作用域和上下文。作用域是基於函數的,上下文是基於對象的。作用域涉及到被調函數中變數的訪問,上下文始終是 this 關鍵字的值,它是擁有當前所執行代碼的對象的引用。上下文通常取決於函數是如何被調用的。
- 作用域鏈(scope chain):當代碼在一個環境中執行時,會創建變數對象的一個作用域鏈。它是保證對執行環境有權訪問的所有變數和函數的有序訪問。作用域鏈的前端始終都是當前執行的代碼所在環境的變數對象。活動對象在最開始時只包含一個變數即 arguments 對象(這個對象在全局環境中不存在),作用域鏈的下一個變數對象來自包含(外部)環境,再下一個變數對象則來自下一個包含環境,這樣一直延續到全局執行環境。
var color = "blue"; function changeColor(){ if(color=="blue"){ color="red"; }else{ color="blue"; } } changeColor(); console.log(color);// red
標識符解析是沿著作用域鏈一級一級地搜索標識符的過程,函數 changeColor 作用域鏈包含兩個對象:它自己的變數對象(其中定義著 arguments 對象)和全局環境的變數對象。可以在函數內部訪問到變數 color 就是因為可以在這個作用域鏈中找到它。內部環境可以通過作用域鏈訪問所有外部環境,但外部環境不能訪問內部環境的任何變數和函數。函數參數也被當作變數來對待,因此其訪問規則與執行環境中的其他變數相同。
- 延長作用域鏈:有些語句可以在作用域的前端臨時添加一個變數對象,該變數對象會在代碼執行後被移除。當執行流進入下列語句時,作用域鏈就會加長。
- try-catch 語句的 catch 塊:對 catch 語句來說,會創建一個新的變數對象,其中包含的是被拋出的錯誤對象的聲明。<=IE8版本中,在 catch 語句中捕獲的錯誤對象會被添加到執行環境的變數對象而不是 catch 語句的變數對象,換句話說,即使是在 catch 塊的外部也可以訪問到錯誤對象。
- with 語句:會將指定的對象添加到作用域鏈中。
function buildUrl(){ var qs="?debug=true"; with(location){ var url=href+qs; } return url; }
buildUrl();// "http://i.cnblogs.com/EditPosts.aspx?postid=5280805?debug=true"with 語句接收的是一個 location 對象,因此其變數對象中就含有 location 對象的所有屬性和方法,且這個變數對象被添加到了作用域鏈的最前端。當在 with 語句中引用變數 href (實際引用的是 location.href )可以在當前的執行環境中找到,當引用變數 qs 時,引用的則是在下一級執行環境中的變數。由於JavaScript中沒有塊級作用域,所以在函數內部可以訪問 url 才能 return 成功,說明 url 並不是添加到 location 所在的變數對象中。
這兩個語句都會在作用域的前端添加一個變數對象。
- 沒有塊級作用域:在其他類C的語言中,由花括弧封閉的代碼塊都有自己的作用域(如果用ECMAScript的話來講,就是他們自己的執行環境),因而支持根據條件來定義變數。如果是在C/C++/Java中, color 會在 if 語句執行完後被銷毀,但在JavaScript中, if 語句中的變數聲明會將變數添加到當前的執行環境中。
if(true){ var color="red"; } console.log(color);// red
- 聲明變數:使用 var 聲明的變數會自動被添加到最接近的環境中。在函數內部,最接近的環境就是函數的局部環境;在 with 語句中,最接近的環境是函數環境。如果初始化變數時沒有使用 var 聲明,該變數會自動被添加到全局環境。
- 查詢標識符:當在某個環境中為了讀取或寫入而引用一個標識符時,必須通過搜索來確定該標識符代表什麼。搜索過程從作用域鏈的前端開始,向上逐級查詢與給定名字匹配的標識符。如果在局部環境中找到了該標識符,搜索過程停止,變數就緒。如果在局部環境中未找到該變數名,則繼續沿作用域鏈向上搜索。搜索過程將一直追溯到全局環境。如果局部環境存在同名標識符,就不會使用位於父環境中的標識符。
函數表達式
if(condition){ function sayHi(){ console.log("Hi"); } }else{ function sayHi(){ console.log("Yo"); } }
以上代碼會在 condition 為 true 時使用 sayHi() 的定義,否則就使用另一個定義。實際上這在ECMAScript中屬於無效語法,JavaScript引擎會嘗試修正錯誤,將其轉換為合理的狀態。但問題是瀏覽器嘗試修正的做法不一樣。大多數瀏覽器會返回第二個聲明。此種方式很危險,不應該出現你的代碼中。在chrome中:
if(true){ function sayHi(){ console.log("Hi"); } }else{ function sayHi(){ console.log("Yo"); } }//function sayHi(){ 沒有函數聲明的變數提升?? console.log("Hi"); }
if(false){ function say(){ console.log("Hi"); } }else{ function say(){ console.log("Yo"); } }//function say(){ console.log("Yo"); }
console.log(sa);//undefined 能輸出undefiend說明函數聲明並沒有提升而是進行的變數提升 if(false){ function sa(){ console.log("Hi"); } }else{ function sa(){ console.log("Yo"); } }//function sa(){ console.log("Yo"); }
修正:使用函數表達式,那就沒什麼問題了。
var sayHi; if(condition){ sayHi=function(){ console.log("Hi"); } }else{ sayHi=function(){ console.log("Yo"); } }
遞歸
在嚴格模式下,不能通過腳本訪問 arguments.callee 。不過可以使用命名函數表達式來完成相同結果。
var factorial=(function f(num){ if(num<=1){ return 1; }else{ return num*f(num-1); } });
註意是用命名函數表達式,單單把命名函數賦值給 factorial 也可以,但是並不能通過f的名字訪問
閉包
閉包是指有權訪問另一個函數作用域中的變數的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數。之所以能夠訪問外部作用域的變數,是因為內部函數的作用域鏈中包含外部作用域。當一個函數被調用的時候,
- 創建一個執行環境(execution context)及相應的作用域鏈
- 使用 arguments 和其他命名參數的值來初始化活動對象(activation object),但在作用域鏈中,外部函數的活動對象始終始終處於第二位...直至作為作用域鏈終點的全局執行環境。
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() 函數時,會創建一個預先包含全局對象的作用域鏈,這個作用域鏈被保存在 compare 內部的 [[Scope]] 屬性中。當調用 compare() 函數時,會為函數創建一個執行環境,然後通過複製函數的 [[Scope]] 屬性中的對象構建起執行環境的作用域鏈。此後又有一個活動對象被創建並被推入執行環境作用域鏈的最前端。對於這個例子中, compare 函數的執行環境而言,其作用鏈包含兩個變數對象:本地活動對象和全局變數對象。顯然,作用域鏈的本質上是一個指向變數對象的指針列表,它只引用但不包含實際的變數對象。
無論什麼時候在函數中訪問一個變數,就會從作用域鏈中搜索具有相應名字的變數,一般來講當函數執行完後,局部活動對象會被銷毀,記憶體中僅保留著全局作用域(全局執行環境的變數對象)。但是閉包的情況又有所不同。在另一個函數內部定義的函數會將包含函數(外部函數)的活動對象添加到它的作用域鏈里,當外部函數執行完後其活動對象不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。換句話說只是外部函數它自己的作用域鏈被銷毀,但活動對象還存在記憶體中。直到內部函數被銷毀後(例如在外部解除了對閉包即內部函數的引用: func=null; ,解除相當於是閉包僅是執行完後),外部函數的活動對象才會被銷毀。
由於閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的記憶體。過多使用閉包可能會導致記憶體占用過多,建議只在絕對必要再考慮使用。但有的優化後的JavaScript引擎如V8會嘗試回收被閉包占用的記憶體。
閉包缺點:作用域鏈的這種配置機制引出了一個副作用即閉包只能取得包含函數中任何變數的最後一個值。因為閉包保存的是整個變數對象,而不是某個特殊的變數。
function createFunctions(){ var result=new Array(); for(var i=0;i<3;i++){ result[i]=function(){ return i; }; } return result; }
createFunctions()[0]();// 3
createFunctions()[1]();// 3
createFunctions()[2]();// 3
當執行 createFunctions 時,它的活動對象里有 arguments=[] , result=undefiend , i=undefiend ,執行完 createFunctions 後, result=[function(){return i},function(){return i},function(){return i}],i=3 ;當此時執行 result 數組時,訪問到的i的值總是為3,因為沿著 function(){return i;} 的作用域鏈查找變數,在外層函數的活動對象上找到i總是為3。數組中每個函數的作用域鏈中都保存著 createFunctions 的活動對象,所以這些函數們引用的都是同一個活動對象,同一個變數i。
解決方案:要的就是當時執行時的變數i,那麼當時把這個i臨時保存一下就可以了,但是保存在哪呢?將i保存在 function(){return i;} 的活動對象中,怎麼保存呢?傳給 arguments 就好了,只傳進來還不行
function createFunctions(){ var result=new Array(); for(var i=0;i<3;i++){ result[i]=function(i){ return i; }; } return result; } createFunctions()[0]();// undefiend
因為訪問i的時候先從自己所在函數的執行環境的活動對象搜索起,找到i發現 i=undefiend 有值就停止向上搜索了。問題就出在上一步中將i保存在活動對象中, result[i]=function(i){return i;} 這句的執行並沒有給匿名函數傳參,這隻是表達式的賦值操作,又不是執行匿名函數。所以現在需要的就是通過某種方式去執行函數的操作把i的值當實參傳進去,簡單!在匿名函數外部加一層立即執行的匿名函數(這也增加了一層作用域了)。
function createFunctions(){ var result=new Array(); for(var i=0;i<3;i++){ result[i]=(function(i){ return function(){ return i; } })(i); } return result; } createFunctins()[0]();// 0
this對象
this 對象是在運行時基於函數的執行環境綁定的:
- 全局函數中, this 等於 window
- 函數被作為某個對象的方法調用時, this 等於那個對象
- 匿名函數的執行環境具有全局性, this 指向 window
- 通過 call() 或 apply() 改變函數執行環境的情況下, this 就會指向其他對象。
由於閉包編寫的方式不同, this 的表現:
var name="the window"; var obj={ name:"the obj", getNameFunc:function(){ //console.log(this==obj); return function(){ console.log(this.name); } } } obj.getNameFunc()();// the window
obj.getNameFunc() 返回了一個新函數,然後在再全局環境中執行該函數。為什麼匿名函數沒有取得其包含作用域(外部作用域)的 this 對象呢?每個函數在被調用時,都會自動獲得兩個特殊的變數: this (創建作用域時獲得)和 arguments (創建活動對象獲得),內部函數在搜索這兩個變數時,只會搜索到自己的活動對象為止,因此永遠不可能直接訪問外部函數的這兩個變數。不過把外部函數作用域的 this 保存在一個閉包能夠訪問到的變數里就可以讓閉包訪問該對象了。
下麵幾種情況特殊的 this :
var name="the window"; var obj={ name:"the obj", getName:function(){ return this.name; } }; obj.getName();// "the obj" (obj.getName)();// "the obj" (obj.getName=obj.getName)();// "the window"
第一個是直接調用,第二個是調用後立即執行的表達式,第三個是執行了一條賦值語句,然後再調用返回的結果,賦值語句的返回了一個函數,然後全局環境下調用這個函數,見下圖
模仿塊級作用域
function outputNumber(count){ for(var i=0;i<count;i++){ console.log(i); } var i;// 只變數提升,到後面執行代碼步驟時候略過此 console.log(i); } outputNumber(3);// 0 1 2 3