前言
有很多人搞不清匿名函數(shù)和閉包這兩個(gè)概念,經(jīng)常混用锋拖。閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)。匿名函數(shù)就是沒(méi)有實(shí)際名字的函數(shù)祸轮。
閉包
概念
閉包兽埃,其實(shí)是一種語(yǔ)言特性,它是指的是程序設(shè)計(jì)語(yǔ)言中适袜,允許將函數(shù)看作對(duì)象柄错,然后能像在對(duì)象中的操作搬在函數(shù)中定義實(shí)例(局部)變量,而這些變量能在函數(shù)中保存到函數(shù)的實(shí)例對(duì)象銷毀為止苦酱,其它代碼塊能通過(guò)某種方式獲取這些實(shí)例(局部)變量的值并進(jìn)行應(yīng)用擴(kuò)展售貌。
條件
閉包是允許函數(shù)訪問(wèn)局部作用域之外的數(shù)據(jù)。即使外部函數(shù)已經(jīng)退出疫萤,外部函數(shù)的變量仍可以被內(nèi)部函數(shù)訪問(wèn)到颂跨。
因此閉包的實(shí)現(xiàn)需要三個(gè)條件:
內(nèi)部函數(shù)實(shí)用了外部函數(shù)的變量
外部函數(shù)已經(jīng)退出
內(nèi)部函數(shù)可以訪問(wèn)
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
上述代碼在執(zhí)行的時(shí)候,b得到的是閉包對(duì)象的引用扯饶,雖然a執(zhí)行完畢后恒削,但是a的活動(dòng)對(duì)象由于閉包的存在并沒(méi)有被銷毀池颈,在執(zhí)行b(1)的時(shí)候,仍然訪問(wèn)到了x變量钓丰,并將其加1躯砰,若再執(zhí)行b(1),則x是2携丁,因?yàn)殚]包的引用b并沒(méi)有消除琢歇。(后面會(huì)解釋,閉包返回了函數(shù)则北,函數(shù)可以創(chuàng)建獨(dú)立的作用域)
閉包矿微,其實(shí)就是指程序語(yǔ)言中能讓代碼調(diào)用已運(yùn)行的函數(shù)中所定義的局部變量。
但是你只需要知道應(yīng)用的兩種情況即可——函數(shù)作為返回值尚揣,函數(shù)作為參數(shù)傳遞涌矢。
function fn() {
var max = 10;
return function bar(x) {
if (x > max) {
console.log(x);
}
};
}
var f1 = fn();
f1(15);
如上代碼,bar函數(shù)作為返回值快骗,賦值給f1變量娜庇。執(zhí)行f1(15)時(shí),用到了fn作用域下的max變量的值方篮。至于如何跨作用域取值名秀,可以參考上一篇文章。
var max = 10,
fn = function(x) {
if (x > max) {
console.log(x); //15
}
};
(function(f) {
var max = 100;
f(15);
})(fn);
如上代碼中藕溅,fn函數(shù)作為一個(gè)參數(shù)被傳遞進(jìn)入另一個(gè)函數(shù)匕得,賦值給f參數(shù)。執(zhí)行f(15)時(shí)巾表,max變量的取值是10汁掠,而不是100。
上一篇講到自由變量跨作用域取值時(shí)集币,曾經(jīng)強(qiáng)調(diào)過(guò):要去創(chuàng)建這個(gè)函數(shù)的作用域取值考阱,而不是“父作用域”。理解了這一點(diǎn)鞠苟,以上兩端代碼中乞榨,自由變量如何取值應(yīng)該比較簡(jiǎn)單.
另外,講到閉包当娱,除了結(jié)合著作用域之外吃既,還需要結(jié)合著執(zhí)行上下文棧來(lái)說(shuō)一下。
在前面講執(zhí)行上下文棧時(shí)趾访,我們提到當(dāng)一個(gè)函數(shù)被調(diào)用完成之后态秧,其執(zhí)行上下文環(huán)境將被銷毀,其中的變量也會(huì)被同時(shí)銷毀扼鞋。
有些情況下申鱼,函數(shù)調(diào)用完成之后,其執(zhí)行上下文環(huán)境不會(huì)接著被銷毀云头。這就是需要理解閉包的核心內(nèi)容捐友。
可以拿本文的之前代碼(只做注釋修改)來(lái)分析一下。
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);
全局作用域?yàn)椋捍a1-12行溃槐;fn作用域?yàn)椋捍a2-10行匣砖;bar作用域?yàn)椋捍a5-9行。
舉例
第一步昏滴,代碼執(zhí)行前生成全局上下文環(huán)境猴鲫,并在執(zhí)行時(shí)對(duì)其中的變量進(jìn)行賦值。此時(shí)全局上下文環(huán)境是活動(dòng)狀態(tài)谣殊。
第二步拂共,執(zhí)行第17行代碼時(shí),調(diào)用fn()姻几,產(chǎn)生fn()執(zhí)行上下文環(huán)境宜狐,壓棧,并設(shè)置為活動(dòng)狀態(tài)蛇捌。
第三步抚恒,執(zhí)行完第17行,fn()調(diào)用完成络拌。按理說(shuō)應(yīng)該銷毀掉fn()的執(zhí)行上下文環(huán)境俭驮,但是這里不能這么做。注意春贸,重點(diǎn)來(lái)了:
因?yàn)閳?zhí)行fn()時(shí)混萝,返回的是一個(gè)函數(shù)。函數(shù)的特別之處在于可以創(chuàng)建一個(gè)獨(dú)立的作用域祥诽。而正巧合的是譬圣,返回的這個(gè)函數(shù)體中,還有一個(gè)自由變量max要引用fn作用域下的fn()上下文環(huán)境中的max雄坪。因此厘熟,這個(gè)max不能被銷毀,銷毀了之后bar函數(shù)中的max就找不到值了维哈。
因此绳姨,這里的fn()上下文環(huán)境不能被銷毀,還依然存在與執(zhí)行上下文棧中阔挠。
——即飘庄,執(zhí)行到第18行時(shí),全局上下文環(huán)境將變?yōu)榛顒?dòng)狀態(tài)购撼,但是fn()上下文環(huán)境依然會(huì)在執(zhí)行上下文棧中跪削。另外谴仙,執(zhí)行完第18行,全局上下文環(huán)境中的max被賦值為100碾盐。如下圖:
第四步晃跺,執(zhí)行到第20行,執(zhí)行f1(15)毫玖,即執(zhí)行bar(15)掀虎,創(chuàng)建bar(15)上下文環(huán)境,并將其設(shè)置為活動(dòng)狀態(tài)付枫。
執(zhí)行bar(15)時(shí)烹玉,max是自由變量,需要向創(chuàng)建bar函數(shù)的作用域中查找阐滩,找到了max的值為10二打。這個(gè)過(guò)程在作用域鏈一節(jié)已經(jīng)講過(guò)。
這里的重點(diǎn)就在于叶眉,創(chuàng)建bar函數(shù)是在執(zhí)行fn()時(shí)創(chuàng)建的址儒。fn()早就執(zhí)行結(jié)束了,但是fn()執(zhí)行上下文環(huán)境還存在與棧中衅疙,因此bar(15)時(shí)莲趣,max可以查找到。如果fn()上下文環(huán)境銷毀了饱溢,那么max就找不到了喧伞。
總結(jié):使用閉包會(huì)增加內(nèi)容開(kāi)銷
第五步,執(zhí)行完20行就是上下文環(huán)境的銷毀過(guò)程绩郎,這里就不再贅述了潘鲫。
閉包與變量
概念
閉包只能取得包含函數(shù)中任何變量的最后一個(gè)值,閉包所保存的是整個(gè)變量對(duì)象,而不是某個(gè)特殊變量肋杖。
例子
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i;
};
}
return result;
}
var funcs = createFunctions();
//每個(gè)函數(shù)都輸出10
for (var i = 0; i < funcs.length; i++) {
document.write(funcs[i]() + "<br />");
}
總結(jié):每個(gè)函數(shù)的作用域鏈中都保存著createFunctions()函數(shù)的活動(dòng)對(duì)象溉仑,所以它們引用的都是同一個(gè)變量i。當(dāng)createFunctions()函數(shù)返回后状植,變量i的值為10浊竟。
我們可以通過(guò)創(chuàng)建另一個(gè)匿名函數(shù)強(qiáng)制讓閉包的行為符合預(yù)期。
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();
//循環(huán)輸出0-10
for (var i = 0; i < funcs.length; i++) {
document.write(funcs[i]() + "<br />");
}
總結(jié):沒(méi)有直接把閉包賦值給數(shù)組津畸,而是定義了一個(gè)匿名函數(shù)振定,并通過(guò)立即執(zhí)行該匿名函數(shù)的結(jié)果賦值給數(shù)組,并帶了for循環(huán)的參數(shù)i進(jìn)去肉拓,讓x能找到傳入的參數(shù)值為0-10后频,這就解釋了函數(shù)參數(shù)是按值傳遞的,所以會(huì)將變量i的當(dāng)前值復(fù)制給參數(shù)x暖途。而這個(gè)匿名函數(shù)內(nèi)部又創(chuàng)建并返回了一個(gè)訪問(wèn)x的閉包卑惜。這樣以來(lái)result數(shù)組中的每個(gè)函數(shù)都有自己x變量的一個(gè)副本膏执,所以會(huì)符合我們的預(yù)期輸出不同的值。
函數(shù)按值傳遞
函數(shù)傳參就兩個(gè)類型残揉,基本類型和引用類型胧后,大家糾結(jié)的都是引用類型的傳遞芋浮。
引用類型作為參數(shù)傳入函數(shù)抱环,傳的是個(gè)地址值,或者指針值纸巷,不是那個(gè)引用類型本身镇草,它還好好的呆在堆內(nèi)存呢。賦值給argument的同樣是地址值或者指針瘤旨。所以說(shuō)是value值傳遞一點(diǎn)沒(méi)錯(cuò)梯啤,傳的是個(gè)地址值。通過(guò)兩個(gè)例子看懂就行了存哲。
例子1:
function setName(obj) {
obj.name = 'aaa';
var obj = new Object(); // 如果是按引用傳遞的,此處傳參進(jìn)來(lái)obj應(yīng)該被重新引用新的內(nèi)存單元
obj.name = 'ccc';
return obj;
}
var person = new Object();
person.name = 'bbb';
var newPerson = setName(person);
console.log(person.name + ' | ' + newPerson.name); // aaa | ccc
從結(jié)果看因宇,并沒(méi)有顯示兩個(gè)'ccc'。這里是函數(shù)內(nèi)部重寫(xiě)了obj祟偷,重寫(xiě)的obj是一個(gè)局部對(duì)象察滑。當(dāng)函數(shù)執(zhí)行完后,立即被銷毀修肠。
引用值:對(duì)象變量它里面的值是這個(gè)對(duì)象在堆內(nèi)存中的內(nèi)存地址贺辰。因此如果按引用傳遞,它傳遞的值也就是這個(gè)內(nèi)存地址嵌施。那么var obj = new Object();會(huì)重新給obj分配一個(gè)地址饲化,比如是0x321了,那么它就不在指向有name = 'aaa';屬性的內(nèi)存單元了吗伤。相當(dāng)于把實(shí)參obj和形參obj的地址都改了吃靠,那么最終就是輸出兩個(gè)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(bǔ)的值傳到change函數(shù)內(nèi)足淆,obj.num = '3';后a.name被修改為3;
a的地址被換成b的地址;
返回此時(shí)的a中a.num巢块。
閉包中使用this對(duì)象
概念
this對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的:全局函數(shù)中,this等于window;當(dāng)函數(shù)被作用某個(gè)對(duì)象的方法調(diào)用時(shí)缸浦,this等于那個(gè)對(duì)象夕冲。
但在匿名函數(shù)中,由于匿名函數(shù)的執(zhí)行環(huán)境具有全局性裂逐,因此this對(duì)象通常指向window(在通過(guò)call或apply函數(shù)改變函數(shù)執(zhí)行環(huán)境的情況下歹鱼,會(huì)指向其他對(duì)象)。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"
通過(guò)修改把作用域中的this對(duì)象保存在一個(gè)閉包能夠訪問(wèn)到的變量里卜高,就可以讓閉包訪問(wèn)該對(duì)象了弥姻。如下代碼:
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南片,并沒(méi)有報(bào)錯(cuò),這是因?yàn)樵谇懊嫖覀兲岬降暮瘮?shù)內(nèi)的聲明在函數(shù)體內(nèi)始終可見(jiàn)庭敦,上面的函數(shù)等效于:
var scope="global";
function scopeTest() {
var scope;
console.log(scope);
scope="local";
}
scopeTest(); //undefined
注意疼进,如果忘記var,那么變量就被聲明為全局變量了秧廉。結(jié)果就是global
沒(méi)有塊級(jí)作用域
和其他我們常用的語(yǔ)言不同伞广,在Javascript中沒(méi)有塊級(jí)作用域:
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中變量的作用范圍是函數(shù)級(jí)的,即在函數(shù)中所有的變量在整個(gè)函數(shù)中都有定義疼电,這也帶來(lái)了一些我們稍不注意就會(huì)碰到的“潛規(guī)則”:
var scope = "hello";
function scopeTest() {
console.log(scope);//①
var scope = "no";
console.log(scope);//②
}
在①處輸出的值竟然是undefined嚼锄,簡(jiǎn)直喪心病狂啊,我們已經(jīng)定義了全局變量的值啊蔽豺,這地方不應(yīng)該為hello嗎区丑?其實(shí),上面的代碼等效于:
var scope = "hello";
function scopeTest() {
var scope;
console.log(scope);//①
scope = "no";
console.log(scope);//②
}
聲明提前修陡、全局變量?jī)?yōu)先級(jí)低于局部變量沧侥,根據(jù)這兩條規(guī)則就不難理解為什么輸出undefined了。