理解js中的閉包

寫在前邊: 咱們知道,當函數執行時,會造成本身的執行期上下文,並把它掛到本身的做用域鏈上,當函數執行完以後,它的執行期上下文就會被釋放。因此,通常狀況下,在函數外部訪問函數內部的變量須要特殊的方法才能解決,這個特殊的方法就是閉包。es6

在理解閉包前,我建議你先了解下js的做用域。 理解js中的做用域面試

閉包的概念

閉包:閉包指的是在函數的外部能夠訪問函數內部的變量。函數沒有被釋放,整條做用域鏈上的局部變量都將獲得保留。建立閉包的通常方法是在函數內部返回一個新函數。bash

通俗的理解: 閉包:顧名思義,是一個封閉的包,可是這個包露出內部的一條線,這條線就是閉包內部返回函數的做用域鏈,它上面掛載了這個函數以及他的全部父級函數的變量,咱們能夠經過這條線訪問到函數內的變量,這就是閉包。閉包

閉包的造成機制

咱們知道: 當一個函數被定義時,它的scope屬性會指向他的父級的scope的引用 當一個函數執行時,會造成它本身的執行期上下文(AO),並把它掛載到他的做用域(scope chain)的最頂端,它的父級的scope依次下移。 當函數執行完畢後,他本身的執行期上下文(AO)會被銷燬 關於scope、AO詳情見理解js中的做用域異步

這樣,當咱們在函數內部返回一個函數並在其外部被一個變量接收時,它的做用域鏈上存的是它的父級的做用域鏈,只要這個函數存在則它的做用域鏈就會一直存在,這樣它的做用域鏈上的變量得不到釋放,即能在函數外部訪問做用域內部的變量。函數

爲了便於理解,咱們舉個簡單的例子:post

function test(){
	var a = 100
	function b(){
		a++
		console.log(a)
	}
	return b
}
var global = 100
var c = test()
c() // 101
c() // 102 
複製代碼

1.當定義並執行test函數時,它的做用域鏈指向它的AO以及全局的GO 性能

在這裏插入圖片描述
2.當函數b被返回時,b的做用域鏈上掛載這它的父級的做用域鏈,即test執行時的做用域鏈
在這裏插入圖片描述
3.此時,雖然test()函數執行完畢,它的執行期上下文被銷燬( 注意這裏的銷燬指的是指向被銷燬,即圖中箭頭消失)。可是此時返回的函數b的scope上擁有b的父級的全部做用域鏈。此時,又將return 的b 賦值給c。因此,當執行c函數時,它會在b的做用域鏈上尋找所須要的變量,這樣就實現了閉包。因此當c執行兩次時,結果分別是10一、102

閉包的做用

閉包的做用通常有兩個: 1.能夠在函數外部,使用用函數內部的變量 2.函數內部的變量不會被釋放。ui

閉包的缺陷

因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,this

關於閉包的常見面試題

第一題:
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();
複製代碼

輸出結果都是3。爲何?

1.當執行完for循環後,此時的全局執行期上下文爲

GO:{
	data:[...],
	i:3
}
複製代碼

2.當執行data[0]時,它產生它本身的執行期上下文(AO),此時他的做用域鏈爲

scope:[AO,GO]
複製代碼

此時,它的AO上沒有i變量,就向它的上一級的執行期上下文中找,即以上的GO,因此輸出結果爲3

其餘兩個執行結果同理。

當咱們將其修改成閉包時,即以下代碼

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (j) {
        return function(){
            console.log(j);
        }
  })(i)
}

data[0](); // 0
data[1](); // 1
data[2](); // 2
複製代碼

此時,他的執行結果分別爲0,1,2

當執行完for循環時,此時的GO爲

GO:{
	data:[...],
	i:3
}
複製代碼

當data[0]執行時,此時他的做用域鏈爲

scope:[AO,匿名函數的AO,GO]
複製代碼

而此時匿名函數的AO爲

AO:{
	i:0
}
複製代碼

data[0]的AO中沒有變量i,因此它沿着做用域鏈向上尋找,找到匿名函數的AO,即此時i爲0.

執行data[1],data[2]同理

第二題
for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

console.log(i);
複製代碼

以上這道題是在面試中常常問到的問題,那麼它輸出的是什麼呢?相信大多數朋友均可以知道,最後他的結果爲 5,5,5,5,5,5。 只要瞭解了js的運行機制、以及同步異步的問題,咱們橫容易知道第一個5是當即輸出,以後的5在1s後同時輸出。

那麼咱們將它改造爲閉包。

for (var i = 0; i < 5; i++) {
	(function(i){
		setTimeout(function() {
	        console.log(i);
	    }, 1000);
	})(i) 
}
console.log(i);
複製代碼

它的結果爲5,0,1,2,3,4 咱們分析下它的做用域。

首先,先定義了5個當即執行函數,而後執行循環外部的console,此時的GO爲{i:5},因此先輸出5 1s後5個當即執行函數同時執行,此時定時器內部的i爲其外部函數(即當即執行函數)的i,此時i分別爲0,1,2,3,4,因此輸出爲5,0,1,2,3,4

想要那麼有沒有什麼其它方法來改造呢?答案是有的,es6裏提供了一個叫let的東西,他會造成塊級做用域。

for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

console.log(i);  

複製代碼

以上代碼會報錯,由於最後的i是不存在的,由於let造成了塊級做用域,只在for循環內部起做用。

第三題
var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};
alert(object.getNameFunc()());  // The Window

複製代碼
第四題
var name = "The Window";

var object = {
    name : "My Object",
    getNameFunc : function(){
        var that = this;
        return function(){
            return that.name;
        };
    }
};
alert(object.getNameFunc()());  // My Object

複製代碼

上面這兩道題,考察了閉包的用法以及this的指向問題,這裏就很少作解釋了。

相信看過個人這篇文章 關於js中的this指向問題,以後就能夠搞明白了。

相關文章
相關標籤/搜索