前言
有很多人搞不清匿名函數和閉包這兩個概念,經?;煊?。閉包是指有權訪問另一個函數作用域中的變量的函數。匿名函數就是沒有實際名字的函數。
閉包
概念
閉包,其實是一種語言特性,它是指的是程序設計語言中,允許將函數看作對象,然后能像在對象中的操作搬在函數中定義實例(局部)變量,而這些變量能在函數中保存到函數的實例對象銷毀為止,其它代碼塊能通過某種方式獲取這些實例(局部)變量的值并進行應用擴展。
條件
閉包是允許函數訪問局部作用域之外的數據。即使外部函數已經退出,外部函數的變量仍可以被內部函數訪問到。
因此閉包的實現需要三個條件:
內部函數實用了外部函數的變量
外部函數已經退出
內部函數可以訪問
function a() {
var x = 0;
return function(y) {
x = x + y;
// return x;
console.log(x);
}
}
var b = a();
b(1); //1
b(1); //2
上述代碼在執行的時候,b得到的是閉包對象的引用,雖然a執行完畢后,但是a的活動對象由于閉包的存在并沒有被銷毀,在執行b(1)的時候,仍然訪問到了x變量,并將其加1,若再執行b(1),則x是2,因為閉包的引用b并沒有消除。(后面會解釋,閉包返回了函數,函數可以創建獨立的作用域)
閉包,其實就是指程序語言中能讓代碼調用已運行的函數中所定義的局部變量。
但是你只需要知道應用的兩種情況即可——函數作為返回值,函數作為參數傳遞。
function fn() {
var max = 10;
return function bar(x) {
if (x > max) {
console.log(x);
}
};
}
var f1 = fn();
f1(15);
如上代碼,bar函數作為返回值,賦值給f1變量。執行f1(15)時,用到了fn作用域下的max變量的值。至于如何跨作用域取值,可以參考上一篇文章。
var max = 10,
fn = function(x) {
if (x > max) {
console.log(x); //15
}
};
(function(f) {
var max = 100;
f(15);
})(fn);
如上代碼中,fn函數作為一個參數被傳遞進入另一個函數,賦值給f參數。執行f(15)時,max變量的取值是10,而不是100。
上一篇講到自由變量跨作用域取值時,曾經強調過:要去創建這個函數的作用域取值,而不是“父作用域”。理解了這一點,以上兩端代碼中,自由變量如何取值應該比較簡單.
另外,講到閉包,除了結合著作用域之外,還需要結合著執行上下文棧來說一下。
在前面講執行上下文棧時,我們提到當一個函數被調用完成之后,其執行上下文環境將被銷毀,其中的變量也會被同時銷毀。
有些情況下,函數調用完成之后,其執行上下文環境不會接著被銷毀。這就是需要理解閉包的核心內容。
可以拿本文的之前代碼(只做注釋修改)來分析一下。
1//全局作用域
2 function fn() {
3 var max = 10;
4 // fn作用域
5 return function bar(x) {
6 if (x > max) {
7 console.log(x);
8 }
9 }; //bar作用域
10 }
11 var f1 = fn();
12 f1(15);
全局作用域為:代碼1-12行;fn作用域為:代碼2-10行;bar作用域為:代碼5-9行。
舉例
第一步,代碼執行前生成全局上下文環境,并在執行時對其中的變量進行賦值。此時全局上下文環境是活動狀態。
第二步,執行第17行代碼時,調用fn(),產生fn()執行上下文環境,壓棧,并設置為活動狀態。
第三步,執行完第17行,fn()調用完成。按理說應該銷毀掉fn()的執行上下文環境,但是這里不能這么做。注意,重點來了:
因為執行fn()時,返回的是一個函數。函數的特別之處在于可以創建一個獨立的作用域。而正巧合的是,返回的這個函數體中,還有一個自由變量max要引用fn作用域下的fn()上下文環境中的max。因此,這個max不能被銷毀,銷毀了之后bar函數中的max就找不到值了。
因此,這里的fn()上下文環境不能被銷毀,還依然存在與執行上下文棧中。
——即,執行到第18行時,全局上下文環境將變為活動狀態,但是fn()上下文環境依然會在執行上下文棧中。另外,執行完第18行,全局上下文環境中的max被賦值為100。如下圖:
第四步,執行到第20行,執行f1(15),即執行bar(15),創建bar(15)上下文環境,并將其設置為活動狀態。
執行bar(15)時,max是自由變量,需要向創建bar函數的作用域中查找,找到了max的值為10。這個過程在作用域鏈一節已經講過。
這里的重點就在于,創建bar函數是在執行fn()時創建的。fn()早就執行結束了,但是fn()執行上下文環境還存在與棧中,因此bar(15)時,max可以查找到。如果fn()上下文環境銷毀了,那么max就找不到了。
總結:使用閉包會增加內容開銷
第五步,執行完20行就是上下文環境的銷毀過程,這里就不再贅述了。
閉包與變量
概念
閉包只能取得包含函數中任何變量的最后一個值,閉包所保存的是整個變量對象,而不是某個特殊變量。
例子
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i;
};
}
return result;
}
var funcs = createFunctions();
//每個函數都輸出10
for (var i = 0; i < funcs.length; i++) {
document.write(funcs[i]() + "<br />");
}
總結:每個函數的作用域鏈中都保存著createFunctions()函數的活動對象,所以它們引用的都是同一個變量i。當createFunctions()函數返回后,變量i的值為10。
我們可以通過創建另一個匿名函數強制讓閉包的行為符合預期。
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function(x) {
return function() {
return x;
};
}(i);
}
return result;
}
var funcs = createFunctions();
//循環輸出0-10
for (var i = 0; i < funcs.length; i++) {
document.write(funcs[i]() + "<br />");
}
總結:沒有直接把閉包賦值給數組,而是定義了一個匿名函數,并通過立即執行該匿名函數的結果賦值給數組,并帶了for循環的參數i進去,讓x能找到傳入的參數值為0-10,這就解釋了函數參數是按值傳遞的,所以會將變量i的當前值復制給參數x。而這個匿名函數內部又創建并返回了一個訪問x的閉包。這樣以來result數組中的每個函數都有自己x變量的一個副本,所以會符合我們的預期輸出不同的值。
函數按值傳遞
函數傳參就兩個類型,基本類型和引用類型,大家糾結的都是引用類型的傳遞。
引用類型作為參數傳入函數,傳的是個地址值,或者指針值,不是那個引用類型本身,它還好好的呆在堆內存呢。賦值給argument的同樣是地址值或者指針。所以說是value值傳遞一點沒錯,傳的是個地址值。通過兩個例子看懂就行了。
例子1:
function setName(obj) {
obj.name = 'aaa';
var obj = new Object(); // 如果是按引用傳遞的,此處傳參進來obj應該被重新引用新的內存單元
obj.name = 'ccc';
return obj;
}
var person = new Object();
person.name = 'bbb';
var newPerson = setName(person);
console.log(person.name + ' | ' + newPerson.name); // aaa | ccc
從結果看,并沒有顯示兩個'ccc'。這里是函數內部重寫了obj,重寫的obj是一個局部對象。當函數執行完后,立即被銷毀。
引用值:對象變量它里面的值是這個對象在堆內存中的內存地址。因此如果按引用傳遞,它傳遞的值也就是這個內存地址。那么var obj = new Object();會重新給obj分配一個地址,比如是0x321了,那么它就不在指向有name = 'aaa';屬性的內存單元了。相當于把實參obj和形參obj的地址都改了,那么最終就是輸出兩個ccc了。
例子2
var a = {
num:'1'
};
var b = {
num:'2'
};
function change(obj){
obj.num = '3';
obj = b;
return obj.num;
}
var result = change(a);
console.log(result + ' | ' + a.num); // 2 | 3
首先把a的值傳到change函數內,obj.num = '3';后a.name被修改為3;
a的地址被換成b的地址;
返回此時的a中a.num。
閉包中使用this對象
概念
this對象是在運行時基于函數的執行環境綁定的:全局函數中,this等于window;當函數被作用某個對象的方法調用時,this等于那個對象。
但在匿名函數中,由于匿名函數的執行環境具有全局性,因此this對象通常指向window(在通過call或apply函數改變函數執行環境的情況下,會指向其他對象)。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"
通過修改把作用域中的this對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了。如下代碼:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //"MyObject"
變量聲明提前
var scope="global";
function scopeTest() {
console.log(scope);
var scope="local";
}
scopeTest(); //undefined
此處的輸出是undefined,并沒有報錯,這是因為在前面我們提到的函數內的聲明在函數體內始終可見,上面的函數等效于:
var scope="global";
function scopeTest() {
var scope;
console.log(scope);
scope="local";
}
scopeTest(); //undefined
注意,如果忘記var,那么變量就被聲明為全局變量了。結果就是global
沒有塊級作用域
和其他我們常用的語言不同,在Javascript中沒有塊級作用域:
function scopeTest() {
var scope = {};
if (scope instanceof Object) {
var j = 1;
for (var i = 0; i < 10; i++) {
console.log(i); //輸出0-9
}
console.log(i); //輸出10
}
console.log(j); //輸出1
}
scopeTest();
在javascript中變量的作用范圍是函數級的,即在函數中所有的變量在整個函數中都有定義,這也帶來了一些我們稍不注意就會碰到的“潛規則”:
var scope = "hello";
function scopeTest() {
console.log(scope);//①
var scope = "no";
console.log(scope);//②
}
在①處輸出的值竟然是undefined,簡直喪心病狂啊,我們已經定義了全局變量的值啊,這地方不應該為hello嗎?其實,上面的代碼等效于:
var scope = "hello";
function scopeTest() {
var scope;
console.log(scope);//①
scope = "no";
console.log(scope);//②
}
聲明提前、全局變量優先級低于局部變量,根據這兩條規則就不難理解為什么輸出undefined了。