作用域
先來(lái)說(shuō)下什么是作用域五续,簡(jiǎn)單的說(shuō)迂苛,作用域就是變量與函數(shù)的可訪問(wèn)范圍,即作用域控制著變量與函數(shù)的可見性和生命周期讹蘑。他減少了名稱沖突怀樟,并且提供了自動(dòng)內(nèi)存管理功偿。
在JavaScript中,變量的作用域有全局作用域和局部作用域兩種往堡。
全局作用域
var num1 = 1;
function fun1 (){
num2 = 2;
}
以上三個(gè)對(duì)象 num1
, num2
和 fun1
均是全局作用域械荷,這里要注意的是 末定義直接賦值的變量自動(dòng)聲明為擁有全局作用域;
局部作用域
function wrap(){
var obj = "我被wrap包裹起來(lái)了虑灰,wrap外部無(wú)法直接訪問(wèn)到我";
function innerFun(){
//外部無(wú)法訪問(wèn)我
}
}
作用域鏈
當(dāng)代碼在一個(gè)環(huán)境中執(zhí)行時(shí)吨瞎,會(huì)創(chuàng)建變量對(duì)象的一個(gè)作用域鏈。
[{當(dāng)前環(huán)境的變量對(duì)象}穆咐,{外層變量對(duì)象}关拒,{外層的外層的變量對(duì)象}, {window全局變量對(duì)象}] 每個(gè)數(shù)組單元就是作用域鏈的一塊,這個(gè)塊就是我們的變量對(duì)象庸娱。
作用于鏈的前端,始終都是當(dāng)前執(zhí)行的代碼所在環(huán)境的變量對(duì)象谐算。全局執(zhí)行環(huán)境的變量對(duì)象也始終都是鏈的最后一個(gè)對(duì)象熟尉。
function foo(){
var a = 12;
fun(a);
function fun(a){
var b = 8;
console.log(a + b);
}
}
foo();
再來(lái)看上面這個(gè)簡(jiǎn)單的例子,我們可以先思考一下洲脂,每個(gè)執(zhí)行環(huán)境下的變量對(duì)象都是什么斤儿? 這兩個(gè)函數(shù)它們的變量對(duì)象分別都是什么剧包?
我們以fun為例,當(dāng)我們調(diào)用它時(shí)往果,會(huì)創(chuàng)建一個(gè)包含 arguments
疆液,a
,b
的 ** 活動(dòng)對(duì)象 **陕贮,對(duì)于函數(shù)而言堕油,在執(zhí)行的最開始階段它的活動(dòng)對(duì)象里只包含一個(gè)變量,即arguments
(當(dāng)執(zhí)行流進(jìn)入肮之,再創(chuàng)建其他的活動(dòng)對(duì)象)掉缺。
在活動(dòng)對(duì)象中,它依然表示當(dāng)前參數(shù)集合戈擒。對(duì)于函數(shù)的活動(dòng)對(duì)象眶明,我們可以想象成兩部分,一個(gè)是固定的arguments
對(duì)象筐高,另一部分是函數(shù)中的局部變量搜囱。而在此例中,a和b都被算入是局部變量中柑土,即便a已經(jīng)包含在了arguments中
蜀肘,但他還是屬于。
有沒有發(fā)現(xiàn)在環(huán)境棧中冰单,所有的執(zhí)行環(huán)境都可以組成相對(duì)應(yīng)的作用域鏈幌缝。我們可以在環(huán)境棧中非常直觀的拼接成一個(gè)相對(duì)作用域鏈。
下面我們大致說(shuō)下這段代碼的執(zhí)行流程:
- 在創(chuàng)建
foo
的時(shí)候诫欠,作用域鏈已經(jīng)預(yù)先包含了一個(gè)全局對(duì)象涵卵,并保存在內(nèi)部屬性[[ Scope ]]
當(dāng)中。 - 執(zhí)行
foo
函數(shù)荒叼,創(chuàng)建執(zhí)行環(huán)境與活動(dòng)對(duì)象后轿偎,取出函數(shù)的內(nèi)部屬性[[Scope]]
構(gòu)建當(dāng)前環(huán)境的作用域鏈(取出后,只有全局變量對(duì)象被廓,然后此時(shí)追加了一個(gè)它自己的活動(dòng)對(duì)象)坏晦。 - 執(zhí)行過(guò)程中遇到了
fun
,從而繼續(xù)對(duì)fun
使用上一步的操作嫁乘。 -
fun
執(zhí)行結(jié)束昆婿,移出環(huán)境棧。foo
因此也執(zhí)行完畢蜓斧,繼續(xù)移出仓蛆。 - javscript 監(jiān)聽到
foo
沒有被任何變量所引用,開始實(shí)施垃圾回收機(jī)制挎春,清空占用內(nèi)存看疙。
作用域鏈其實(shí)就是引用了當(dāng)前執(zhí)行環(huán)境的變量對(duì)象的指針列表豆拨,它只是引用,但不是包含能庆,因?yàn)樗男螤钕矜湕l施禾,它的執(zhí)行過(guò)程也非常符合,所以我們都稱之為 ** 作用域鏈 **搁胆,而當(dāng)我們弄懂了這其中的奧秘弥搞,就可以拋開這種形式上的束縛,從原理上出發(fā)丰涉。
閉包
閉包拓巧,官方對(duì)閉包的解釋是:一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式(通常是一個(gè)函數(shù)),因而這些變量也是該表達(dá)式的一部分一死。
閉包的特點(diǎn):
- 作為一個(gè)函數(shù)變量的一個(gè)引用肛度,當(dāng)函數(shù)返回時(shí),其處于激活狀態(tài)投慈。
- 一個(gè)閉包就是當(dāng)一個(gè)函數(shù)返回時(shí)承耿,一個(gè)沒有釋放資源的棧區(qū)。
其實(shí)就是 ** 有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù) 伪煤。簡(jiǎn)單說(shuō)就是加袋,假設(shè)函數(shù)a是定義在函數(shù)b中的函數(shù),那么函數(shù)a就是一個(gè)閉包抱既。正常情況下职烧,在函數(shù)的外部訪問(wèn)不到函數(shù)內(nèi)部的變量,但有了閉包就可以間接的實(shí)現(xiàn)訪問(wèn)內(nèi)部變量的需要防泵。也就是說(shuō)蚀之, 閉包是連接函數(shù)內(nèi)部和外部的橋梁 **。
閉包的作用
- 訪問(wèn)函數(shù)內(nèi)部的變量捷泞。
- 讓被引用的變量值始終保持在內(nèi)存中足删。
function fn1(){
var a = 1;
return function(){
console.log(++a);
}
}
var fn2 = fn1();
fn2(); //輸出2
fn2(); //輸出3
在這段代碼中,fn1中的閉包函數(shù)被當(dāng)作結(jié)果返回锁右,在閉包中的引用的變量a因?yàn)楸灰枚鴽]有被清除失受,一直保存在內(nèi)存當(dāng)中,所以執(zhí)行fn2的時(shí)候會(huì)輸出不斷增加的結(jié)果:2和3咏瑟。
當(dāng)閉包中引用了函數(shù)中的變量時(shí)拂到,那么,這個(gè)變量就會(huì)保存在內(nèi)存中码泞。也就是上面提到的閉包的第二個(gè)作用兄旬。之所以為這樣,是因?yàn)镴avaScript的回收機(jī)制浦夷。
基本所有瀏覽器都是使用“標(biāo)記清除”的方式回收內(nèi)存辖试。也就是說(shuō),當(dāng)變量進(jìn)入執(zhí)行環(huán)境的時(shí)候(在函數(shù)中聲明一個(gè)變量)劈狐,就給變量添加標(biāo)記罐孝,而當(dāng)函數(shù)執(zhí)行完的,變量不再被引用的時(shí)候肥缔,再添加刪除的標(biāo)記莲兢,垃圾收集器就會(huì)自動(dòng)清楚這個(gè)變量占有的內(nèi)存。但在閉包中引用了函數(shù)中的變量续膳,而閉包又被當(dāng)作結(jié)果返回時(shí)改艇,閉包中的因?yàn)楸灰镁筒粫?huì)被清除
閉包的用途
- 匿名自執(zhí)行函數(shù)
我們知道所有的變量,如果不加上var關(guān)鍵字坟岔,則默認(rèn)的會(huì)添加到全局對(duì)象的屬性上去谒兄,這樣的臨時(shí)變量加入全局對(duì)象有很多壞處,
比如:別的函數(shù)可能誤用這些變量社付;造成全局對(duì)象過(guò)于龐大承疲,影響訪問(wèn)速度(因?yàn)樽兞康娜≈凳切枰獜脑玩溕媳闅v的)。
除了每次使用變量都是用var
關(guān)鍵字外鸥咖,我們?cè)趯?shí)際情況下經(jīng)常遇到這樣一種情況燕鸽,即有的函數(shù)只需要執(zhí)行一次,其內(nèi)部變量無(wú)需維護(hù)啼辣,
比如UI的初始化啊研,那么我們可以使用閉包:
var data= {
table : [],
tree : {}
};
(function(dm){
for(var i = 0; i < dm.table.rows; i++){
var row = dm.table.rows[i];
for(var j = 0; j < row.cells; i++){
drawCell(i, j);
}
}
})(data);
我們創(chuàng)建了一個(gè)匿名的函數(shù),并立即執(zhí)行它鸥拧,由于外部無(wú)法引用它內(nèi)部的變量党远,因此在函數(shù)執(zhí)行完后會(huì)立刻釋放資源,關(guān)鍵是不污染全局對(duì)象住涉。
- 結(jié)果緩存
我們開發(fā)中會(huì)碰到很多情況麸锉,設(shè)想我們有一個(gè)處理過(guò)程很耗時(shí)的函數(shù)對(duì)象,每次調(diào)用都會(huì)花費(fèi)很長(zhǎng)時(shí)間舆声,
那么我們就需要將計(jì)算出來(lái)的值存儲(chǔ)起來(lái)花沉,當(dāng)調(diào)用這個(gè)函數(shù)的時(shí)候,首先在緩存中查找媳握,如果找不到碱屁,則進(jìn)行計(jì)算,然后更新緩存并返回值蛾找,如果找到了娩脾,直接返回查找到的值即可。閉包正是可以做到這一點(diǎn)打毛,因?yàn)樗粫?huì)釋放外部的引用柿赊,從而函數(shù)內(nèi)部的值可以得以保留俩功。
var CachedSearchBox = (function(){
var cache = {},
count = [];
return {
attachSearchBox : function(dsid){
if(dsid in cache){ //如果結(jié)果在緩存中
return cache[dsid]; //直接返回緩存中的對(duì)象
}
var fsb = new uikit.webctrl.SearchBox(dsid); //新建
cache[dsid] = fsb; //更新緩存
if(count.length > 100){ //保正緩存的大小<=100
delete cache[count.shift()];
}
return fsb;
},
clearSearchBox : function(dsid){
if(dsid in cache){
cache[dsid].clearSelection();
}
}
};
})();
CachedSearchBox.attachSearchBox("input");
這樣我們?cè)诘诙握{(diào)用的時(shí)候,就會(huì)從緩存中讀取到該對(duì)象碰声。
- 封裝
var person = function(){
//變量作用域?yàn)楹瘮?shù)內(nèi)部诡蜓,外部無(wú)法訪問(wèn)
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
}();
print(person.name);//直接訪問(wèn),結(jié)果為undefined
print(person.getName());
person.setName("abruzzi");
print(person.getName());
// 得到結(jié)果如下:
// undefined
// default
// abruzzi
- 實(shí)現(xiàn)類和繼承
function Person(){
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
};
var p = new Person();
p.setName("Tom");
alert(p.getName());
var Jack = function(){};
//繼承自Person
Jack.prototype = new Person();
//添加私有方法
Jack.prototype.Say = function(){
alert("Hello,my name is Jack");
};
var j = new Jack();
j.setName("Jack");
j.Say();
alert(j.getName());
我們定義了Person
胰挑,它就像一個(gè)類蔓罚,我們new
一個(gè)Person
對(duì)象,訪問(wèn)它的方法瞻颂。
下面我們定義了Jack
豺谈,繼承Person
,并添加自己的方法贡这。