思維導(dǎo)圖
閉包
了解閉包前先來了解一下上級(jí)作用域和堆棧內(nèi)存釋放問題。
上級(jí)作用域的概念
- 函數(shù)的上級(jí)作用域在哪里創(chuàng)建創(chuàng)建的,上級(jí)作用域就是誰
var a = 10
function foo(){
console.log(a)
}
function sum() {
var a = 20
foo()
}
sum()
/* 輸出
10
/
復(fù)制代碼
函數(shù)
foo()
是在全局下創(chuàng)建的廷粒,所以a
的上級(jí)作用域就是window
扑毡,輸出就是10
思考題
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
f()
return f
}
var x = fn()
x()
x()
console.log(n)
/* 輸出
* 21
22
23
10
/
復(fù)制代碼
思路:
fn
的返回值是什么變量x
就是什么,這里fn
的返回值是函數(shù)名f
也就是f
的堆內(nèi)存地址宴霸,x()
也就是執(zhí)行的是函數(shù)f()
姆坚,而不是fn()
澳泵,輸出的結(jié)果顯而易見
- 關(guān)于如何查找上級(jí)作用域
JS 堆棧內(nèi)存釋放
堆內(nèi)存:存儲(chǔ)引用類型值,對(duì)象類型就是鍵值對(duì)兼呵,函數(shù)就是代碼字符串兔辅。
堆內(nèi)存釋放:將引用類型的空間地址變量賦值成
null
,或沒有變量占用堆內(nèi)存了瀏覽器就會(huì)釋放掉這個(gè)地址棧內(nèi)存:提供代碼執(zhí)行的環(huán)境和存儲(chǔ)基本類型值击喂。
棧內(nèi)存釋放:一般當(dāng)函數(shù)執(zhí)行完后函數(shù)的私有作用域就會(huì)被釋放掉维苔。
但棧內(nèi)存的釋放也有特殊情況:① 函數(shù)執(zhí)行完,但是函數(shù)的私有作用域內(nèi)有內(nèi)容被棧外的變量還在使用的懂昂,棧內(nèi)存就不能釋放里面的基本值也就不會(huì)被釋放蕉鸳。② 全局下的棧內(nèi)存只有頁面被關(guān)閉的時(shí)候才會(huì)被釋放
閉包是什么
在 JS 忍者秘籍(P90)中對(duì)閉包的定義:閉包允許函數(shù)訪問并操作函數(shù)外部的變量。紅寶書上對(duì)于閉包的定義:閉包是指有權(quán)訪問另外一個(gè)函數(shù)作用域中的變量的函數(shù)忍法。 MDN 對(duì)閉包的定義為:閉包是指那些能夠訪問自由變量的函數(shù)潮尝。這里的自由變量是外部函數(shù)作用域中的變量。
概述上面的話饿序,閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中變量的函數(shù)
形成閉包的原因
內(nèi)部的函數(shù)存在外部作用域的引用就會(huì)導(dǎo)致閉包勉失。從上面介紹的上級(jí)作用域的概念中其實(shí)就有閉包的例子
return f
就是一個(gè)表現(xiàn)形式。
var a = 0
function foo(){
var b =14
function fo(){
console.log(a, b)
}
fo()
}
foo()
復(fù)制代碼
這里的子函數(shù)
fo
內(nèi)存就存在外部作用域的引用a, b
原探,所以這就會(huì)產(chǎn)生閉包
閉包的作用
- 保護(hù)函數(shù)的私有變量不受外部的干擾乱凿。形成不銷毀的棧內(nèi)存。
- 保存咽弦,把一些函數(shù)內(nèi)的值保存下來徒蟆。閉包可以實(shí)現(xiàn)方法和屬性的私有化
閉包經(jīng)典使用場(chǎng)景
-
return
回一個(gè)函數(shù)
-
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
return f
}
var x = fn()
x() // 21
復(fù)制代碼
這里的 return
f
,f()
就是一個(gè)閉包,存在上級(jí)作用域的引用型型。
- 函數(shù)作為參數(shù)
var a = '林一一'
function foo(){
var a = 'foo'
function fo(){
console.log(a)
}
return fo
}
function f(p){
var a = 'f'
p()
}
f(foo())
/* 輸出
* foo
/
復(fù)制代碼
使用 return
fo
返回回來段审,fo()
就是閉包,f(foo())
執(zhí)行的參數(shù)就是函數(shù)fo
闹蒜,因?yàn)?fo() 中的 a
的上級(jí)作用域就是函數(shù)foo()
寺枉,所以輸出就是foo
- IIFE(自執(zhí)行函數(shù))
var n = '林一一';
(function p(){
console.log(n)
})()
/* 輸出
* 林一一
/
復(fù)制代碼
同樣也是產(chǎn)生了閉包
p()
,存在window
下的引用n
绷落。
- 循環(huán)賦值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
復(fù)制代碼
因?yàn)榇嬖陂]包的原因上面能依次輸出1~10姥闪,閉包形成了10個(gè)互不干擾的私有作用域。將外層的自執(zhí)行函數(shù)去掉后就不存在外部作用域的引用了砌烁,輸出的結(jié)果就是連續(xù)的 10筐喳。為什么會(huì)連續(xù)輸出10,因?yàn)?JS 是單線程的遇到異步的代碼不會(huì)先執(zhí)行(會(huì)入棧)函喉,等到同步的代碼執(zhí)行完
i++
到 10時(shí)避归,異步代碼才開始執(zhí)行此時(shí)的i=10
輸出的都是 10。
- 使用回調(diào)函數(shù)就是在使用閉包
window.name = '林一一'
setTimeout(function timeHandler(){
console.log(window.name);
}, 100)
復(fù)制代碼
使用閉包需要注意什么
容易導(dǎo)致內(nèi)存泄漏函似。閉包會(huì)攜帶包含其它的函數(shù)作用域槐脏,因此會(huì)比其他函數(shù)占用更多的內(nèi)存。過度使用閉包會(huì)導(dǎo)致內(nèi)存占用過多撇寞,所以要謹(jǐn)慎使用閉包顿天。
經(jīng)典面試題
- for 循環(huán)和閉包(號(hào)稱必刷題)
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
/* 輸出
3
3
3
/
復(fù)制代碼
這里的
i
是全局下的i
,共用一個(gè)作用域蔑担,當(dāng)函數(shù)被執(zhí)行的時(shí)候這時(shí)的i=3
牌废,導(dǎo)致輸出的結(jié)構(gòu)都是3。
- 使用閉包改善上面的寫法達(dá)到預(yù)期效果啤握,寫法1:自執(zhí)行函數(shù)和閉包
var data = [];
for (var i = 0; i < 3; i++) {
(function(j){
setTimeout( data[j] = function () {
console.log(j);
}, 0)
})(i)
}
data[0]();
data[1]();
data[2]()
復(fù)制代碼
- 寫法2:使用
let
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
復(fù)制代碼
let
具有塊級(jí)作用域鸟缕,形成的3個(gè)私有作用域都是互不干擾的。