理解js中的闭包

207 阅读6分钟

写在前边: 我们知道,当函数执行时,会形成自己的执行期上下文,并把它挂到自己的作用域链上,当函数执行完之后,它的执行期上下文就会被释放。所以,一般情况下,在函数外部访问函数内部的变量需要特殊的方法才能解决,这个特殊的方法就是闭包。

在理解闭包前,我建议你先了解下js的作用域。 理解js中的作用域

闭包的概念

闭包:闭包指的是在函数的外部可以访问函数内部的变量。函数没有被释放,整条作用域链上的局部变量都将得到保留。创建闭包的一般方法是在函数内部返回一个新函数。

通俗的理解: 闭包:顾名思义,是一个封闭的包,但是这个包露出内部的一条线,这条线就是闭包内部返回函数的作用域链,它上面挂载了这个函数以及他的所有父级函数的变量,我们可以通过这条线访问到函数内的变量,这就是闭包。

闭包的形成机制

我们知道: 当一个函数被定义时,它的scope属性会指向他的父级的scope的引用 当一个函数执行时,会形成它自己的执行期上下文(AO),并把它挂载到他的作用域(scope chain)的最顶端,它的父级的scope依次下移。 当函数执行完毕后,他自己的执行期上下文(AO)会被销毁 关于scope、AO详情见理解js中的作用域

这样,当我们在函数内部返回一个函数并在其外部被一个变量接收时,它的作用域链上存的是它的父级的作用域链,只要这个函数存在则它的作用域链就会一直存在,这样它的作用域链上的变量得不到释放,即能在函数外部访问作用域内部的变量。

为了便于理解,我们举个简单的例子:

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执行两次时,结果分别是101、102

闭包的作用

闭包的作用一般有两个: 1.可以在函数外部,使用用函数内部的变量 2.函数内部的变量不会被释放。

闭包的缺陷

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,

关于闭包的常见面试题

第一题:
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指向问题,之后就可以搞明白了。