更改記錄:
19年 11月27日昔穴,修改!
上一節(jié)說了執(zhí)行上下文冬念,這節(jié)咱們就乘勝追擊來搞搞閉包!頭疼的東西讓你不再頭疼牧挣!
名詞解釋:
變量對象
變量對象是根據(jù)(Variable Object) 來翻譯過來的急前,也可以翻譯成可變對象, 就是保存變量的對象,活動對象瀑构,閉包對象都保存著變量裆针,因此也可以稱為變量對象。
注:這里解釋下寺晌,是因為各個書中對這幾個名詞的使用世吨,搞的好遠。
執(zhí)行上下文
是根據(jù)(Execution Context)翻譯過來的呻征,也可譯為執(zhí)行環(huán)境耘婚。
在函數(shù)執(zhí)行時,就會首先創(chuàng)建執(zhí)行上下文來運行代碼陆赋。
活動對象
是根據(jù)(Activation Object)翻譯過來的沐祷,也可譯為激活的對象。
在函數(shù)執(zhí)行時攒岛,創(chuàng)建的變量對象赖临,不僅含有變量,還有特殊的this灾锯,arguments兢榨。
閉包
是根據(jù)(closure) 翻譯過來的,也可譯為 閉合顺饮,使結(jié)束等吵聪,我認為可以理解為 封閉的環(huán)境。
把當(dāng)前作用域外的環(huán)境封閉起來领突,以備 其他作用域環(huán)境使用暖璧。
總結(jié):好多名詞都是英譯過來的,看原著或根據(jù)上下文來理解才是這些單詞真正的意思君旦,只可意會不可言傳澎办。嘲碱。。
一局蚀、函數(shù)也是引用類型的麦锯。
function f(){ console.log("not change") };
var ff = f;
function f(){ console.log("changed") };
ff();
//"changed"
//ff 保存著函數(shù) f 的引用,改變f 的值琅绅, ff也變了
//來個對比劫灶,估計你就明白了。
var f = "not change";
var ff = f;
f = "changed";
console.log(ff);
//"not change"
//ff 保存著跟 f 一樣的值请敦,改變f 的值秃嗜, ff 不會變
其實,就是引用類型 和 基本類型的 區(qū)別澎羞。
二髓绽、函數(shù)創(chuàng)建一個參數(shù),就相當(dāng)于在其內(nèi)部聲明了該變量
function f(arg){
console.log(arg)
}
f();
//undefined
function f(arg){
arg = 5;
console.log(arg);
}
f();
//5
三妆绞、參數(shù)傳遞顺呕,就相當(dāng)于變量復(fù)制(值的傳遞)
基本類型時,變量保存的是數(shù)據(jù)括饶,引用類型時株茶,變量保存的是內(nèi)存地址。參數(shù)傳遞图焰,就是把變量保存的值 復(fù)制給 參數(shù)启盛。
var o = { a: 5 };
function f(arg){
arg.a = 6;
}
f(o);
console.log(o.a);
//6
四、垃圾收集機制
JavaScript 具有自動垃圾收集機制楞泼,執(zhí)行環(huán)境會負責(zé)管理代碼執(zhí)行過程中使用的內(nèi)存驰徊。函數(shù)中,正常的局部變量和函數(shù)聲明只在函數(shù)執(zhí)行的過程中存在堕阔,當(dāng)函數(shù)執(zhí)行結(jié)束后棍厂,就會釋放它們所占的內(nèi)存(銷毀變量和函數(shù))。
而js 中 主要有兩種收集方式:
- 標(biāo)記清除(常見) //給變量標(biāo)記為“進入環(huán)境” 和 “離開環(huán)境”超陆,回收標(biāo)記為“離開環(huán)境”的變量牺弹。
- 引用計數(shù) // 一個引用類型值,被賦值給一個變量时呀,引用次數(shù)加1张漂,通過變量取得引用類型值,則減1谨娜,回收為次數(shù)為0 的引用類型值航攒。
知道個大概情況就可以了,《JavaScript高級程序設(shè)計 第三版》 4.3節(jié) 有詳解趴梢,有興趣漠畜,可以看下币他。.
五、作用域
在 JavaScript 中, 作用域(scope憔狞,或譯有效范圍)顧名思義就是變量和函數(shù)的作用范圍(可訪問范圍)蝴悉。
作用域可以實體化為一個 可變對象(Variable Object 變量對象)
JavaScript中的作用域有:全局作用域和局部作用域(函數(shù)作用域)。ES6 新增了塊級作用域
全局作用域(Global Scope)
(1)不在任何函數(shù)內(nèi)定義的變量就具有全局作用域瘾敢。(非嚴(yán)格模式下)
(2)實際上拍冠,JavaScript默認有一個全局對象window,全局作用域的變量實際上被綁定到window的一個屬性簇抵。
局部作用域(Local Scope)
(1)JavaScript的作用域是通過函數(shù)來定義的庆杜,在一個函數(shù)中定義的變量只對這個函數(shù)內(nèi)部可見,稱為函數(shù)(局部)作用域正压。
塊級作用域
塊級作用域指在If語句欣福,switch語句,循環(huán)語句等語句塊中定義變量焦履,這意味著變量不能在語句塊之外被訪問。
六雏逾、函數(shù)跟作用域鏈間的關(guān)系
每個函數(shù)都有一個[[scope]] 的內(nèi)部屬性(可以通過console.dir(fn)嘉裤,來查看),它保存著作用域鏈(一個對象數(shù)組)栖博,而作用域鏈中是一個個可變對象(Variable Object 變量對象)(一個保存當(dāng)前作用域中用到的變量屑宠,函數(shù)等的對象)。當(dāng)函數(shù)創(chuàng)建時仇让,一個代表全局環(huán)境的可變對象會被插入到作用域的第一個位置典奉。該全局可變對象保存著window,navigator丧叽,document 等卫玖。
例如如下 聲明一個全局函數(shù):
function add(num1, num2) {
return num1 + num2
}
當(dāng)函數(shù)執(zhí)行時,會創(chuàng)建執(zhí)行上下文(執(zhí)行環(huán)境)踊淳,隨后創(chuàng)建一個執(zhí)行上下文對象假瞬,它有自己的作用鏈。剛開始迂尝,它會用函數(shù)自身的 [[scope]] 中的作用域鏈初始化自己(也就是復(fù)制)脱茉。
隨后一個活動對象被創(chuàng)建(也可以說是變量對象,可變對象)垄开,它保存著當(dāng)前函數(shù)作用域里的變量琴许,arguments,this 等溉躲。最后榜田,該活動對象會被推到執(zhí)行上下文的作用域鏈的最前端寸认。
注:執(zhí)行上下文(執(zhí)行環(huán)境)在函數(shù)執(zhí)行完畢后就會被銷毀,里面的作用域鏈串慰,變量偏塞,函數(shù),活動對象邦鲫,this 等也會一同銷毀灸叼。
七、作用域鏈查找
在函數(shù)執(zhí)行過程中庆捺,每遇到一個變量古今,都會經(jīng)歷一次標(biāo)識符解析過程以決定從那里獲取存儲數(shù)據(jù)。該過程搜索執(zhí)行環(huán)境的作用域鏈滔以,查找同名的標(biāo)識符捉腥。搜索過程從作用域鏈頭部開始也就是當(dāng)前運行的作用域。如果找到你画,就使用這個標(biāo)識符對應(yīng)的變量抵碟;如果沒找到,繼續(xù)搜索作用域鏈中的下一個對象坏匪。搜索過程會持續(xù)進行拟逮,直到找到標(biāo)識符,若無法搜索到匹配的對象适滓,那么標(biāo)識符將被視為未定義的敦迄。
八、閉包函數(shù) 與 閉包對象
當(dāng)函數(shù)嵌套時凭迹,例如有一個A函數(shù)罚屋,內(nèi)部有個v1 的變量,有一個B函數(shù),B 中使用了v1 變量嗅绸。這時脾猛,為了讓 B 執(zhí)行時,能訪問 v1(其實就是為了形成作用域鏈)朽砰,會有以下兩個變化:
- 形成一個閉包函數(shù)尖滚,生成一個閉包對象 A,包含了 B 中用到 v1 變量
- 在B 閉包函數(shù)的[[scope]] 屬性中 推入 閉包對象A瞧柔。
function A(){
var v1 = 666
function B() {
return v1
}
console.dir(B)
B()
}
A()
執(zhí)行結(jié)果漆弄,看函數(shù)的[[scope]] 屬性:
注:可以通過 debugger 來在谷歌瀏覽器控制臺里看。具體怎么用造锅,可以自行百度撼唾。
現(xiàn)在來分析一下過程:
1、首先哥蔚,A 函數(shù)執(zhí)行倒谷,一開始 它的[[scope]] 內(nèi)的作用域鏈中只有全局的可變對象蛛蒙,然后 創(chuàng)建一個執(zhí)行上下文對象,有一個作用域鏈渤愁,根據(jù) [[scope]] 復(fù)制來 來初始化自己牵祟。
2、創(chuàng)建 A 函數(shù)的活動對象抖格,并推到 執(zhí)行上下文對象的作用域鏈中诺苹。
3、當(dāng)發(fā)現(xiàn) B 中用到 v1 時雹拄,B 就會變成一個封閉的函數(shù)(閉包函數(shù))收奔,然后,生成一個關(guān)于A 函數(shù)的封閉對象(閉包對象)滓玖,保存著 v1(因為它存在于A坪哄,在B中使用)。隨后势篡,把這個封閉的對象推到 B 函數(shù) 的[[scope]] 作用域鏈中翩肌。
注:這時,B 函數(shù)還沒有執(zhí)行殊霞。至于什么機制導(dǎo)致js 能夠發(fā)現(xiàn)未執(zhí)行的函數(shù)內(nèi)使用了 A 函數(shù)內(nèi)的變量摧阅,目前的知識還得不到答案。
4绷蹲、當(dāng)B 函數(shù)執(zhí)行時,創(chuàng)建執(zhí)行上下文顾孽,創(chuàng)建執(zhí)行上下文對象祝钢,初始化執(zhí)行上下文對象的作用域鏈(復(fù)制B 函數(shù)的[[scope]] 屬性)。
5若厚、隨后創(chuàng)建一個活動對象拦英,并推到 執(zhí)行上下文對象的作用域鏈中。
這樣测秸,B 在執(zhí)行時疤估,就可以訪問 v1 了,因為在一個作用域鏈中霎冯。
下面來總結(jié)下作用鏈的變化過程:
- 全局下的 A函數(shù)執(zhí)行時铃拇,內(nèi)部的[[scope]] 保存的作用域鏈只有一個全局的變量對象。創(chuàng)建A 函數(shù)的執(zhí)行上下文對象沈撞,根據(jù) [[scope]] 復(fù)制初始化 A函數(shù)的 執(zhí)行上下文對象的作用域鏈慷荔。
- 創(chuàng)建 A 函數(shù)的活動對象,推到 A函數(shù)執(zhí)行上下文對象的作用域鏈前端缠俺。
- 當(dāng)發(fā)現(xiàn) A 函數(shù)內(nèi)部(不管層級多深)有 一個函數(shù)使用了 A函數(shù)內(nèi)的 變量或函數(shù)显晶。
- 則 A 內(nèi)(不管層級多深)所有函數(shù) 都會形成 閉包函數(shù)贷岸。
- 然后創(chuàng)建一個關(guān)于 A 的閉包對象,對象內(nèi)含有被使用的變量或函數(shù)(通過復(fù)制)磷雇。
- 最后把該閉包對象 推到 所有閉包函數(shù)的 [[scope]] 內(nèi)偿警。
可以得出以下結(jié)論:
- 有兩個作用域鏈,一個存與函數(shù)的[[scope]] 中唯笙,用來保存作用域螟蒸,以備執(zhí)行上下文對象初始化自身作用域鏈。
- 執(zhí)行上下文對象中的作用域鏈睁本,會添加活動函數(shù)尿庐,作用域鏈的查找,查的就是這條作用域鏈呢堰。(一般我們說的作用域鏈就是指這條)
- 活動函數(shù)只會存在于執(zhí)行上下文對象的作用域鏈中抄瑟。
- 有閉包函數(shù)和閉包對象,閉包函數(shù)的[[scope]] 保存閉包對象枉疼,而閉包對象皮假,封閉的是 父或祖級函數(shù)作用域中的變量或?qū)ο?/strong>。
- 閉包函數(shù)的存在是因為 執(zhí)行上下文環(huán)境 會在執(zhí)行完后銷毀骂维,而其中的作用域鏈惹资,活動對象,變量等等就丟失了航闺,通過閉包函數(shù) 就可以保存著作用域鏈褪测,而鏈中的變量對象又保存著變量,函數(shù)等潦刃。
來一個難一點的例子侮措,大家可以先自己分析分析。
function A(){
var va = 'aaa'
function B() {
var vb = 'bbb'
function C() {
var vc1 = 'ccc'
return va
}
function D() {
var vd = 'ddd'
return vb
}
console.dir(C)
console.dir(D)
C()
}
console.dir(B)
function E () {
var vd = 'eee'
}
console.dir(E)
B()
}
console.dir(A)
A()
根據(jù)上面分析乖杠,可以得出各個函數(shù)的[[scope]]:
- A 只有一個全局變量對象
- B 和 E 有兩個變量對象分扎,關(guān)于 A 的閉包對象,全局變量對象胧洒。
- C 和 D 有三個變量對象畏吓,關(guān)于 B 的閉包對象,關(guān)于 A 的閉包對象卫漫,全局變量對象菲饼。
控制臺:
通過 debugger 來單步調(diào)試,無非就是能看到每個執(zhí)行環(huán)境內(nèi)的作用域鏈中 含有 活動對象汛兜。
有興趣的可以試試巴粪。
至于閉包的內(nèi)存泄漏,這里面牽扯到 js 的垃圾回收機制。不過可以看到肛根,[[scope]] 中保存著 變量辫塌,如果 該變量 占的內(nèi)存不被釋放,一旦這樣的情況過多派哲,內(nèi)存占用過大臼氨,就會造成內(nèi)存泄漏 和 性能問題。
九芭届、閉包的概念
一般說的閉包指的都是閉包函數(shù)储矩。
引用高程(《JavaScript高級程序設(shè)計》)中關(guān)于閉包說法:
閉包是指有權(quán)訪問另一個函數(shù)作用域中變量的函數(shù)
通過上面說的那么多,你品褂乍,你細品持隧。。逃片。
十屡拨、閉包的本質(zhì)
我認為就是為了形成作用域鏈。你品褥实,你細品呀狼。。损离。
再來個有趣經(jīng)典的例子:
function timer () {
for (var i=1; i<=5; i++) {
setTimeout(function(){
console.log(i);
},i*1000);
}
}
timer()
//每隔一秒輸出一個6,共5個哥艇。
是不是跟你想的不一樣?其實僻澎,這個例子重點就在setTimeout函數(shù)上貌踏,這個函數(shù)的第一個參數(shù)接受一個函數(shù)作為回調(diào)函數(shù),這個回調(diào)函數(shù)并不會立即執(zhí)行窟勃,它會在當(dāng)前代碼執(zhí)行完哩俭,并在給定的時間后執(zhí)行。這樣就導(dǎo)致了上面情況的發(fā)生拳恋。
注:這里用一個函數(shù)包裹起來了,這樣砸捏,你可以通過 debugger谬运,會發(fā)現(xiàn),這里也形成閉包了垦藏。閉包函數(shù)是每一個匿名函數(shù)梆暖,閉包對象是是關(guān)于timer 的,保存著變量 i
掂骏。
可以下面對這個例子進行變形轰驳,可以有助于你的理解把:
function timer () {
var i = 1;
while(i <= 5){
setTimeout(function(){
console.log(i);
},i*1000)
i = i+1;
}
}
timer()
正因為,setTimeout
里的第一個函數(shù)不會立即執(zhí)行,當(dāng)這段代碼執(zhí)行完之后级解,i
已經(jīng) 被賦值為6
了(等于5
時冒黑,進入循環(huán),最后又加了1
)勤哗,所以 這時再執(zhí)行setTimeout
的回調(diào)函數(shù)抡爹,讀取 i
的值,回調(diào)函數(shù)作用域內(nèi)沒有i芒划,向上讀取冬竟,上級作用域內(nèi)i
的值就是6
了。但是 i * 1000
民逼,是立即執(zhí)行的泵殴,所以,每次讀的 i
值 都是對的拼苍。
這時候笑诅,就需要再用個閉包函數(shù)來保存每個循環(huán)時 i
不同的值。
function makeClosures(i){ // 這個函數(shù)使用了 上級作用域中的 `i`映屋,形成閉包函數(shù)苟鸯。
var i = i; //這步是不需要的,為了讓看客們看的輕松點
return function(){
console.log(i); //匿名沒有執(zhí)行棚点,它可以訪問i 的值早处,保存著這個i 的值。
}
}
function timer() {
for (var i=1; i<=5; i++) {
setTimeout(makeClosures(i),i*1000);
//這里簡單說下瘫析,這里makeClosures(i)砌梆, 是函數(shù)執(zhí)行,并不是傳參贬循,不是一個概念
//每次循環(huán)時咸包,都執(zhí)行了makeClosures函數(shù),形成一個閉包函數(shù)杖虾,保存含有 `i` 的閉包對象(這個例子就是 5個 閉包函數(shù)保存各自的閉包對象)烂瘫。
//然后每次都返回了一個沒有被執(zhí)行的匿名函數(shù),(這里就是返回了5個匿名函數(shù))。
//每個匿名函數(shù)都是一個局部作用域奇适,它的上級作用域就是 makeClosures 閉包函數(shù)坟比。
//因此,每個匿名函數(shù)執(zhí)行時嚷往,讀取`i`值葛账,都是上級作用域內(nèi)保存的值,是不一樣的皮仁。所以籍琳,就得到了想要的結(jié)果
}
}
timer()
//1
//2
//3
//4
//5
你可能在別處菲宴,或者自己想到了下面這種解法:
for (var i=1; i<=5; i++) {
(function(i){
setTimeout(function(){
console.log(i);
},i*1000);
})(i);
}
這個例子不僅利用了閉包,而且還利用了立即執(zhí)行函數(shù) 來模擬 函數(shù)作用域 來解決的趋急。
做下變形喝峦,你再看看:
for (var i=1; i<=5; i++) {
function f(i){
setTimeout(function(){
console.log(i);
},i*1000);
};
f(i);
}
附錄:
其實這道題,知道ES6
的 let
關(guān)鍵詞宣谈,估計也想到了另一個解法:
for (let i=1; i<=5; i++) { //這里的關(guān)鍵就是使用的let 關(guān)鍵詞愈犹,來形成塊級作用域
setTimeout(function(){
console.log(i);
},i*1000);
}
我不知道,大家有沒有疑惑啊闻丑,為啥使用了塊級作用域就可以了呢漩怎。反正我當(dāng)初就糾結(jié)了半天。
18年 11月 2日修正:
這個答案的關(guān)鍵就在于 塊級作用域的規(guī)則了嗦嗡。它讓let
聲明的變量只在{}
內(nèi)有效勋锤,外部是訪問不了的。
做下變形侥祭,這個是為了方便理解的叁执,事實并非如此:
for (var i=1; i<=5; i++) {
let j = i;
setTimeout(function(){
console.log(j);
},j*1000);
}
當(dāng)for 的()
內(nèi)使用 let
時,for 循環(huán)就存在兩個作用域矮冬,()
括號里的父作用域谈宛,和 {}
中括號里的 子作用域。
每次循環(huán)都會創(chuàng)建一個 子作用域胎署。保存著父作用域傳來的值吆录,這樣,每個子作用域內(nèi)的值都是不同的琼牧。當(dāng)setTimeout 的匿名函數(shù)執(zhí)行時恢筝,自己的作用域沒有i
的值,向上讀取到了該 子作用域 的 i
值巨坊。因此每次的值才會不一樣撬槽。
你要是喜歡折騰,你會發(fā)現(xiàn)趾撵,塊級作用域的表現(xiàn)跟函數(shù)作用域一樣侄柔,子作用域中使用它的變量,它也會形成一個塊級對象占调,被寫入到 函數(shù)的 [[scope]] 中勋拟。