更改記錄:
19年 11月27日,修改!
上一節(jié)說(shuō)了執(zhí)行上下文,這節(jié)咱們就乘勝追擊來(lái)搞搞閉包!頭疼的東西讓你不再頭疼!
名詞解釋:
變量對(duì)象
變量對(duì)象是根據(jù)(Variable Object) 來(lái)翻譯過(guò)來(lái)的,也可以翻譯成可變對(duì)象, 就是保存變量的對(duì)象,活動(dòng)對(duì)象,閉包對(duì)象都保存著變量,因此也可以稱為變量對(duì)象。
注:這里解釋下,是因?yàn)楦鱾€(gè)書中對(duì)這幾個(gè)名詞的使用,搞的好遠(yuǎn)。
執(zhí)行上下文
是根據(jù)(Execution Context)翻譯過(guò)來(lái)的,也可譯為執(zhí)行環(huán)境。
在函數(shù)執(zhí)行時(shí),就會(huì)首先創(chuàng)建執(zhí)行上下文來(lái)運(yùn)行代碼。
活動(dòng)對(duì)象
是根據(jù)(Activation Object)翻譯過(guò)來(lái)的,也可譯為激活的對(duì)象。
在函數(shù)執(zhí)行時(shí),創(chuàng)建的變量對(duì)象,不僅含有變量,還有特殊的this,arguments。
閉包
是根據(jù)(closure) 翻譯過(guò)來(lái)的,也可譯為 閉合,使結(jié)束等,我認(rèn)為可以理解為 封閉的環(huán)境。
把當(dāng)前作用域外的環(huán)境封閉起來(lái),以備 其他作用域環(huán)境使用。
總結(jié):好多名詞都是英譯過(guò)來(lái)的,看原著或根據(jù)上下文來(lái)理解才是這些單詞真正的意思,只可意會(huì)不可言傳。。。
一、函數(shù)也是引用類型的。
function f(){ console.log("not change") };
var ff = f;
function f(){ console.log("changed") };
ff();
//"changed"
//ff 保存著函數(shù) f 的引用,改變f 的值, ff也變了
//來(lái)個(gè)對(duì)比,估計(jì)你就明白了。
var f = "not change";
var ff = f;
f = "changed";
console.log(ff);
//"not change"
//ff 保存著跟 f 一樣的值,改變f 的值, ff 不會(huì)變
其實(shí),就是引用類型 和 基本類型的 區(qū)別。
二、函數(shù)創(chuàng)建一個(gè)參數(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í),變量保存的是數(shù)據(jù),引用類型時(shí),變量保存的是內(nèi)存地址。參數(shù)傳遞,就是把變量保存的值 復(fù)制給 參數(shù)。
var o = { a: 5 };
function f(arg){
arg.a = 6;
}
f(o);
console.log(o.a);
//6
四、垃圾收集機(jī)制
JavaScript 具有自動(dòng)垃圾收集機(jī)制,執(zhí)行環(huán)境會(huì)負(fù)責(zé)管理代碼執(zhí)行過(guò)程中使用的內(nèi)存。函數(shù)中,正常的局部變量和函數(shù)聲明只在函數(shù)執(zhí)行的過(guò)程中存在,當(dāng)函數(shù)執(zhí)行結(jié)束后,就會(huì)釋放它們所占的內(nèi)存(銷毀變量和函數(shù))。
而js 中 主要有兩種收集方式:
- 標(biāo)記清除(常見) //給變量標(biāo)記為“進(jìn)入環(huán)境” 和 “離開環(huán)境”,回收標(biāo)記為“離開環(huán)境”的變量。
- 引用計(jì)數(shù) // 一個(gè)引用類型值,被賦值給一個(gè)變量,引用次數(shù)加1,通過(guò)變量取得引用類型值,則減1,回收為次數(shù)為0 的引用類型值。
知道個(gè)大概情況就可以了,《JavaScript高級(jí)程序設(shè)計(jì) 第三版》 4.3節(jié) 有詳解,有興趣,可以看下。.
五、作用域
在 JavaScript 中, 作用域(scope,或譯有效范圍)顧名思義就是變量和函數(shù)的作用范圍(可訪問(wèn)范圍)。
作用域可以實(shí)體化為一個(gè) 可變對(duì)象(Variable Object 變量對(duì)象)
JavaScript中的作用域有:全局作用域和局部作用域(函數(shù)作用域)。ES6 新增了塊級(jí)作用域
全局作用域(Global Scope)
(1)不在任何函數(shù)內(nèi)定義的變量就具有全局作用域。(非嚴(yán)格模式下)
(2)實(shí)際上,JavaScript默認(rèn)有一個(gè)全局對(duì)象window,全局作用域的變量實(shí)際上被綁定到window的一個(gè)屬性。
局部作用域(Local Scope)
(1)JavaScript的作用域是通過(guò)函數(shù)來(lái)定義的,在一個(gè)函數(shù)中定義的變量只對(duì)這個(gè)函數(shù)內(nèi)部可見,稱為函數(shù)(局部)作用域。
塊級(jí)作用域
塊級(jí)作用域指在If語(yǔ)句,switch語(yǔ)句,循環(huán)語(yǔ)句等語(yǔ)句塊中定義變量,這意味著變量不能在語(yǔ)句塊之外被訪問(wèn)。
六、函數(shù)跟作用域鏈間的關(guān)系
每個(gè)函數(shù)都有一個(gè)[[scope]] 的內(nèi)部屬性(可以通過(guò)console.dir(fn),來(lái)查看),它保存著作用域鏈(一個(gè)對(duì)象數(shù)組),而作用域鏈中是一個(gè)個(gè)可變對(duì)象(Variable Object 變量對(duì)象)(一個(gè)保存當(dāng)前作用域中用到的變量,函數(shù)等的對(duì)象)。當(dāng)函數(shù)創(chuàng)建時(shí),一個(gè)代表全局環(huán)境的可變對(duì)象會(huì)被插入到作用域的第一個(gè)位置。該全局可變對(duì)象保存著window,navigator,document 等。
例如如下 聲明一個(gè)全局函數(shù):
function add(num1, num2) {
return num1 + num2
}
當(dāng)函數(shù)執(zhí)行時(shí),會(huì)創(chuàng)建執(zhí)行上下文(執(zhí)行環(huán)境),隨后創(chuàng)建一個(gè)執(zhí)行上下文對(duì)象,它有自己的作用鏈。剛開始,它會(huì)用函數(shù)自身的 [[scope]] 中的作用域鏈初始化自己(也就是復(fù)制)。
隨后一個(gè)活動(dòng)對(duì)象被創(chuàng)建(也可以說(shuō)是變量對(duì)象,可變對(duì)象),它保存著當(dāng)前函數(shù)作用域里的變量,arguments,this 等。最后,該活動(dòng)對(duì)象會(huì)被推到執(zhí)行上下文的作用域鏈的最前端。
注:執(zhí)行上下文(執(zhí)行環(huán)境)在函數(shù)執(zhí)行完畢后就會(huì)被銷毀,里面的作用域鏈,變量,函數(shù),活動(dòng)對(duì)象,this 等也會(huì)一同銷毀。
七、作用域鏈查找
在函數(shù)執(zhí)行過(guò)程中,每遇到一個(gè)變量,都會(huì)經(jīng)歷一次標(biāo)識(shí)符解析過(guò)程以決定從那里獲取存儲(chǔ)數(shù)據(jù)。該過(guò)程搜索執(zhí)行環(huán)境的作用域鏈,查找同名的標(biāo)識(shí)符。搜索過(guò)程從作用域鏈頭部開始也就是當(dāng)前運(yùn)行的作用域。如果找到,就使用這個(gè)標(biāo)識(shí)符對(duì)應(yīng)的變量;如果沒(méi)找到,繼續(xù)搜索作用域鏈中的下一個(gè)對(duì)象。搜索過(guò)程會(huì)持續(xù)進(jìn)行,直到找到標(biāo)識(shí)符,若無(wú)法搜索到匹配的對(duì)象,那么標(biāo)識(shí)符將被視為未定義的。
八、閉包函數(shù) 與 閉包對(duì)象
當(dāng)函數(shù)嵌套時(shí),例如有一個(gè)A函數(shù),內(nèi)部有個(gè)v1 的變量,有一個(gè)B函數(shù),B 中使用了v1 變量。這時(shí),為了讓 B 執(zhí)行時(shí),能訪問(wèn) v1(其實(shí)就是為了形成作用域鏈),會(huì)有以下兩個(gè)變化:
- 形成一個(gè)閉包函數(shù),生成一個(gè)閉包對(duì)象 A,包含了 B 中用到 v1 變量
- 在B 閉包函數(shù)的[[scope]] 屬性中 推入 閉包對(duì)象A。
function A(){
var v1 = 666
function B() {
return v1
}
console.dir(B)
B()
}
A()
執(zhí)行結(jié)果,看函數(shù)的[[scope]] 屬性:
注:可以通過(guò) debugger 來(lái)在谷歌瀏覽器控制臺(tái)里看。具體怎么用,可以自行百度。
現(xiàn)在來(lái)分析一下過(guò)程:
1、首先,A 函數(shù)執(zhí)行,一開始 它的[[scope]] 內(nèi)的作用域鏈中只有全局的可變對(duì)象,然后 創(chuàng)建一個(gè)執(zhí)行上下文對(duì)象,有一個(gè)作用域鏈,根據(jù) [[scope]] 復(fù)制來(lái) 來(lái)初始化自己。
2、創(chuàng)建 A 函數(shù)的活動(dòng)對(duì)象,并推到 執(zhí)行上下文對(duì)象的作用域鏈中。
3、當(dāng)發(fā)現(xiàn) B 中用到 v1 時(shí),B 就會(huì)變成一個(gè)封閉的函數(shù)(閉包函數(shù)),然后,生成一個(gè)關(guān)于A 函數(shù)的封閉對(duì)象(閉包對(duì)象),保存著 v1(因?yàn)樗嬖谟贏,在B中使用)。隨后,把這個(gè)封閉的對(duì)象推到 B 函數(shù) 的[[scope]] 作用域鏈中。
注:這時(shí),B 函數(shù)還沒(méi)有執(zhí)行。至于什么機(jī)制導(dǎo)致js 能夠發(fā)現(xiàn)未執(zhí)行的函數(shù)內(nèi)使用了 A 函數(shù)內(nèi)的變量,目前的知識(shí)還得不到答案。
4、當(dāng)B 函數(shù)執(zhí)行時(shí),創(chuàng)建執(zhí)行上下文,創(chuàng)建執(zhí)行上下文對(duì)象,初始化執(zhí)行上下文對(duì)象的作用域鏈(復(fù)制B 函數(shù)的[[scope]] 屬性)。
5、隨后創(chuàng)建一個(gè)活動(dòng)對(duì)象,并推到 執(zhí)行上下文對(duì)象的作用域鏈中。
這樣,B 在執(zhí)行時(shí),就可以訪問(wèn) v1 了,因?yàn)樵谝粋€(gè)作用域鏈中。
下面來(lái)總結(jié)下作用鏈的變化過(guò)程:
- 全局下的 A函數(shù)執(zhí)行時(shí),內(nèi)部的[[scope]] 保存的作用域鏈只有一個(gè)全局的變量對(duì)象。創(chuàng)建A 函數(shù)的執(zhí)行上下文對(duì)象,根據(jù) [[scope]] 復(fù)制初始化 A函數(shù)的 執(zhí)行上下文對(duì)象的作用域鏈。
- 創(chuàng)建 A 函數(shù)的活動(dòng)對(duì)象,推到 A函數(shù)執(zhí)行上下文對(duì)象的作用域鏈前端。
- 當(dāng)發(fā)現(xiàn) A 函數(shù)內(nèi)部(不管層級(jí)多深)有 一個(gè)函數(shù)使用了 A函數(shù)內(nèi)的 變量或函數(shù)。
- 則 A 內(nèi)(不管層級(jí)多深)所有函數(shù) 都會(huì)形成 閉包函數(shù)。
- 然后創(chuàng)建一個(gè)關(guān)于 A 的閉包對(duì)象,對(duì)象內(nèi)含有被使用的變量或函數(shù)(通過(guò)復(fù)制)。
- 最后把該閉包對(duì)象 推到 所有閉包函數(shù)的 [[scope]] 內(nèi)。
可以得出以下結(jié)論:
- 有兩個(gè)作用域鏈,一個(gè)存與函數(shù)的[[scope]] 中,用來(lái)保存作用域,以備執(zhí)行上下文對(duì)象初始化自身作用域鏈。
- 執(zhí)行上下文對(duì)象中的作用域鏈,會(huì)添加活動(dòng)函數(shù),作用域鏈的查找,查的就是這條作用域鏈。(一般我們說(shuō)的作用域鏈就是指這條)
- 活動(dòng)函數(shù)只會(huì)存在于執(zhí)行上下文對(duì)象的作用域鏈中。
- 有閉包函數(shù)和閉包對(duì)象,閉包函數(shù)的[[scope]] 保存閉包對(duì)象,而閉包對(duì)象,封閉的是 父或祖級(jí)函數(shù)作用域中的變量或?qū)ο?/strong>。
- 閉包函數(shù)的存在是因?yàn)?執(zhí)行上下文環(huán)境 會(huì)在執(zhí)行完后銷毀,而其中的作用域鏈,活動(dòng)對(duì)象,變量等等就丟失了,通過(guò)閉包函數(shù) 就可以保存著作用域鏈,而鏈中的變量對(duì)象又保存著變量,函數(shù)等。
來(lái)一個(gè)難一點(diǎn)的例子,大家可以先自己分析分析。
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ù)上面分析,可以得出各個(gè)函數(shù)的[[scope]]:
- A 只有一個(gè)全局變量對(duì)象
- B 和 E 有兩個(gè)變量對(duì)象,關(guān)于 A 的閉包對(duì)象,全局變量對(duì)象。
- C 和 D 有三個(gè)變量對(duì)象,關(guān)于 B 的閉包對(duì)象,關(guān)于 A 的閉包對(duì)象,全局變量對(duì)象。
控制臺(tái):
通過(guò) debugger 來(lái)單步調(diào)試,無(wú)非就是能看到每個(gè)執(zhí)行環(huán)境內(nèi)的作用域鏈中 含有 活動(dòng)對(duì)象。
有興趣的可以試試。
至于閉包的內(nèi)存泄漏,這里面牽扯到 js 的垃圾回收機(jī)制。不過(guò)可以看到,[[scope]] 中保存著 變量,如果 該變量 占的內(nèi)存不被釋放,一旦這樣的情況過(guò)多,內(nèi)存占用過(guò)大,就會(huì)造成內(nèi)存泄漏 和 性能問(wèn)題。
九、閉包的概念
一般說(shuō)的閉包指的都是閉包函數(shù)。
引用高程(《JavaScript高級(jí)程序設(shè)計(jì)》)中關(guān)于閉包說(shuō)法:
閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中變量的函數(shù)
通過(guò)上面說(shuō)的那么多,你品,你細(xì)品。。。
十、閉包的本質(zhì)
我認(rèn)為就是為了形成作用域鏈。你品,你細(xì)品。。。
再來(lái)個(gè)有趣經(jīng)典的例子:
function timer () {
for (var i=1; i<=5; i++) {
setTimeout(function(){
console.log(i);
},i*1000);
}
}
timer()
//每隔一秒輸出一個(gè)6,共5個(gè)。
是不是跟你想的不一樣?其實(shí),這個(gè)例子重點(diǎn)就在setTimeout函數(shù)上,這個(gè)函數(shù)的第一個(gè)參數(shù)接受一個(gè)函數(shù)作為回調(diào)函數(shù),這個(gè)回調(diào)函數(shù)并不會(huì)立即執(zhí)行,它會(huì)在當(dāng)前代碼執(zhí)行完,并在給定的時(shí)間后執(zhí)行。這樣就導(dǎo)致了上面情況的發(fā)生。
注:這里用一個(gè)函數(shù)包裹起來(lái)了,這樣,你可以通過(guò) debugger,會(huì)發(fā)現(xiàn),這里也形成閉包了。閉包函數(shù)是每一個(gè)匿名函數(shù),閉包對(duì)象是是關(guān)于timer 的,保存著變量 i
。
可以下面對(duì)這個(gè)例子進(jìn)行變形,可以有助于你的理解把:
function timer () {
var i = 1;
while(i <= 5){
setTimeout(function(){
console.log(i);
},i*1000)
i = i+1;
}
}
timer()
正因?yàn)椋?code>setTimeout里的第一個(gè)函數(shù)不會(huì)立即執(zhí)行,當(dāng)這段代碼執(zhí)行完之后,i
已經(jīng) 被賦值為6
了(等于5
時(shí),進(jìn)入循環(huán),最后又加了1
),所以 這時(shí)再執(zhí)行setTimeout
的回調(diào)函數(shù),讀取 i
的值,回調(diào)函數(shù)作用域內(nèi)沒(méi)有i,向上讀取,上級(jí)作用域內(nèi)i
的值就是6
了。但是 i * 1000
,是立即執(zhí)行的,所以,每次讀的 i
值 都是對(duì)的。
這時(shí)候,就需要再用個(gè)閉包函數(shù)來(lái)保存每個(gè)循環(huán)時(shí) i
不同的值。
function makeClosures(i){ // 這個(gè)函數(shù)使用了 上級(jí)作用域中的 `i`,形成閉包函數(shù)。
var i = i; //這步是不需要的,為了讓看客們看的輕松點(diǎn)
return function(){
console.log(i); //匿名沒(méi)有執(zhí)行,它可以訪問(wèn)i 的值,保存著這個(gè)i 的值。
}
}
function timer() {
for (var i=1; i<=5; i++) {
setTimeout(makeClosures(i),i*1000);
//這里簡(jiǎn)單說(shuō)下,這里makeClosures(i), 是函數(shù)執(zhí)行,并不是傳參,不是一個(gè)概念
//每次循環(huán)時(shí),都執(zhí)行了makeClosures函數(shù),形成一個(gè)閉包函數(shù),保存含有 `i` 的閉包對(duì)象(這個(gè)例子就是 5個(gè) 閉包函數(shù)保存各自的閉包對(duì)象)。
//然后每次都返回了一個(gè)沒(méi)有被執(zhí)行的匿名函數(shù),(這里就是返回了5個(gè)匿名函數(shù))。
//每個(gè)匿名函數(shù)都是一個(gè)局部作用域,它的上級(jí)作用域就是 makeClosures 閉包函數(shù)。
//因此,每個(gè)匿名函數(shù)執(zhí)行時(shí),讀取`i`值,都是上級(jí)作用域內(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);
}
這個(gè)例子不僅利用了閉包,而且還利用了立即執(zhí)行函數(shù) 來(lái)模擬 函數(shù)作用域 來(lái)解決的。
做下變形,你再看看:
for (var i=1; i<=5; i++) {
function f(i){
setTimeout(function(){
console.log(i);
},i*1000);
};
f(i);
}
附錄:
其實(shí)這道題,知道ES6
的 let
關(guān)鍵詞,估計(jì)也想到了另一個(gè)解法:
for (let i=1; i<=5; i++) { //這里的關(guān)鍵就是使用的let 關(guān)鍵詞,來(lái)形成塊級(jí)作用域
setTimeout(function(){
console.log(i);
},i*1000);
}
我不知道,大家有沒(méi)有疑惑啊,為啥使用了塊級(jí)作用域就可以了呢。反正我當(dāng)初就糾結(jié)了半天。
18年 11月 2日修正:
這個(gè)答案的關(guān)鍵就在于 塊級(jí)作用域的規(guī)則了。它讓let
聲明的變量只在{}
內(nèi)有效,外部是訪問(wèn)不了的。
做下變形,這個(gè)是為了方便理解的,事實(shí)并非如此:
for (var i=1; i<=5; i++) {
let j = i;
setTimeout(function(){
console.log(j);
},j*1000);
}
當(dāng)for 的()
內(nèi)使用 let
時(shí),for 循環(huán)就存在兩個(gè)作用域,()
括號(hào)里的父作用域,和 {}
中括號(hào)里的 子作用域。
每次循環(huán)都會(huì)創(chuàng)建一個(gè) 子作用域。保存著父作用域傳來(lái)的值,這樣,每個(gè)子作用域內(nèi)的值都是不同的。當(dāng)setTimeout 的匿名函數(shù)執(zhí)行時(shí),自己的作用域沒(méi)有i
的值,向上讀取到了該 子作用域 的 i
值。因此每次的值才會(huì)不一樣。
你要是喜歡折騰,你會(huì)發(fā)現(xiàn),塊級(jí)作用域的表現(xiàn)跟函數(shù)作用域一樣,子作用域中使用它的變量,它也會(huì)形成一個(gè)塊級(jí)對(duì)象,被寫入到 函數(shù)的 [[scope]] 中。