1、什麼是閉包
通俗的來說,閉包就是在函數內部的一個函數,使得能在函數外部訪問函數內的變量。舉個例子:例1
function outerFn() {
var count = 0;
var innerFn = function() {
console.log(++count)
}
return innerFn
}
var fn = outerFn();
fn(); //0
2、爲什麼要使用閉包
從上面例1來看,好像並沒有必要使用外包來實現。咱們再來看另外一種情況:如果我要實現一個計數器,在全局任何地方都可以調用
當然,可以這樣來實現:例2
var count = 0;//局部變量
function countFn() {
console.log(++count)
}
//在其他地方調用
countFn();//1
countFn();//2
但是這樣會存在安全隱患,因爲count
是一個全局變量,在任何地方都可以訪問,即使不調用countFn
,很可能產生意想不到的影響。
當然也可以在函數內部創建這個變量:例3
function countFn() {
var count = 0;//局部變量
console.log(++count)
}
//在其他地方調用
countFn();//1
countFn();//1
由於count
是局部變量,因此在外部無法訪問不會例2中存在的問題,但卻也導致計數器函數失效
但如果使用閉包就可以很好的解決這個矛盾:例4
function countFn() {
var count = 0;//局部變量
var innerCount = function() {
console.log(++count)
}
}
首先定義一個內部函數innerCount
,如果能在外部調用innerCount
就可以實現計數器。
function countFn() {
var count = 0;//局部變量
var innerCount = function() {
console.log(++count)
}
return innerCount
}
var outerCount = countFn();
//在其他地方調用
outerCount();//1
outerCount();//2
或者通過函數自調用
var countFn = (function() {
var count = 0;//局部變量
return function() {
console.log(++count)
}
})()
//在其他地方調用
countFn();//1
countFn();//2
再看另外一種使用情況:對象按照特定屬性排序。
function sortFn(key) {
return function(a, b) {
return a[key] - b[key]
}
}
var arr = [{id: 2, age: 20},{id: 1, age: 23},{id: 3, age: 18}];
console.log(arr.sort(sortFn('id')));//[{id: 1, age: 23},{id: 2, age: 20},{id: 3, age: 18}]
console.log(arr.sort(sortFn('age')));//[{id: 3, age: 18},{id: 2, age: 20},{id: 1, age: 23}]
3、閉包的原理
我們知道,函數內的變量不能在外部訪問,那爲什麼閉包能做到呢?這就要從作用域鏈來分析了。
每一個函數都有自己對應的作用域鏈,那麼作用域鏈又是什麼呢?
首先,在每個函數對應的執行環境都有一個對應的變量對象,裏面存放了這個執行環境所有的變量和方法。當代碼在這個執行環境中執行時,會創建這個變量對象的一個作用域鏈。如果這個環境是函數,則變量對象首先(作用域鏈的前端)是函數的活動對象,而活動對象則是函數的arguments對象和其他在函數內部定義的變量。下一個變量對象(作用域鏈的下一部分)是這個函數的外部環境,再下一個變量對象(作用域鏈的再下一部分)是函數的外部環境的包含環境(也就是外部環境的外部環境),一直延續到全局環境。
可能這個描述不太直觀,舉個例子:例4
var a = 'global';
function outerFn() {
var b = 'outer'
var innerFn = function(A, B) {
var c = 'inner';
c += A + B;
console.log(a + b + c);//可以訪問a b c
}
innerFn(a, b);//可以訪問a b
}
outerFn();//只可以訪問a
下圖可以很好的展示例4的作用域鏈
一般情況下,在函數執行過程中,可以在作用域鏈上查詢和訪問變量,因此作用域鏈只是指向變量對象的一個列表,當函數執行完之後,這個函數內的局部變量就會被銷燬。但閉包的情況就不同了。
在函數內定義的函數會將外部函數的活動對象添加到自己的作用域鏈上,即使外部函數執行完後,外部函數的作用域鏈被銷燬,這個活動對象也仍然存在。
4、閉包的隱患
先來看一個例子:例5
function outerFn() {
var arr = [];
for (var i=0; i < 10; i++){
arr[i] = function() {
console.log(i);
};
}
return arr;
}
var a = outerFn();
a.forEach(item => item());
執行結果輸出都是10,不是我們期待的結果。這是因爲a
中的每一個函數的作用域鏈中都只是保存着變量i
的引用而已。如果想要解決這個問題,可以這樣做:例6
function outerFn() {
var arr = [];
for (var i=0; i < 10; i++){
arr[i] = (function(num) {
return function(){
console.log(num);
}
})(i)
}
return arr;
}
var a = outerFn();
a.forEach(item => item());
我們創建了一個自調用的匿名函數,因爲函數傳參是值傳遞的,因此會將變量i
複製給num
,那麼a
中的每一個函數的作用域鏈上都會有各自對應的num
。
5、其中的this對象
在匿名函數的執行環境具有全局性,因此他的的this
指向的是window
。看個例子:例7
var age = 10;
var obj = {
age: 20,
sayAgeFn: function() {
return function() {
console.log(this.age)
}
}
}
obj.sayAgeFn()();//10
爲什麼匿名函數的this
沒有指向他的外部函數呢,這是因爲匿名函數執行時,他的this
和arguments
對象最多搜索到函數本身的活動對象,而不會再向外搜索,如果沒有搜索到則爲window
var age = 10;
var obj = {
age: 20,
sayAgeFn: function() {
return function() {
age = 30
console.log(this.age)
}
}
}
obj.sayAgeFn()();//30
那麼如果不想通過這種方式解決時,還有什麼方法呢?那就是將this
放在閉包能夠訪問的變量裏:例8
var age = 10;
var obj = {
age: 20,
sayAgeFn: function() {
var that = this
return function() {
console.log(that.age)
}
}
}
obj.sayAgeFn()();//20
6、內存泄漏
什麼是內存泄漏?簡單來說,就是因爲閉包使得在函數外部也可以訪問函數內的變量,因此在函數執行完之後,活動對象還不能被銷燬,一直佔用着內存。
那麼怎麼來解決這個問題呢?主要有兩個方法;
- 使用完後手動解除引用
function outerFn() {
var count = 0;
return function() {
console.log(count);
}
}
var a = outerFn();
a();//調用匿名函數
a = null;//解除對匿名函數的引用
- 模仿塊級作用域
比如上面的例5可以改爲:
function outerFn() {
var arr = [];
(function() {
for (var i=0; i < 10; i++){
arr[i] = function() {
console.log(i);
};
}
})();//自執行完後會銷燬 i
console.log(i);//會出錯
return arr;
}
var a = outerFn();
a.forEach(item => item());