深入理解js闭包

一、什么是闭包

  在JS高程上,是这样介绍闭包的:闭包是指有权访问另一个函数作用域中的变量的函数。比如下面这个最简单的例子:

function returnf() {
	var a = 1;
	return function f() {
		console.log(a);
	}
}

  在f中可以访问returnf函数中的a变量,所以这样就创建了一个闭包。当我们用变量接到f函数的时候,就可以在returnf函数外部去访问a变量了。下面,我们来仔细探究一下他的原理,在此之前,我们来了解几个概念。

二、执行环境、变量对象、活动对象

  执行环境定义了变量或函数有权访问的其他数据。每个执行环境都会有一个与之关联的变量对象。变量对象存储的是环境中定义的所有变量和函数。比如:

function f() {
	var a,b,c;
	funtion f2() {}
}

  在这个函数的执行环境中,变量对象应该包含a, b, c和f2。还有一个与之相似的名词叫活动对象。根据字面意思来理解,活动,即活跃的对象。我的理解是,如果你定义了一个函数,然后又调用了这个函数,那么这个函数里面的变量就会变成活动对象,因为它在当前是活跃的。比如说:

function f1() {}
function f2() {}
f1();

  我们定义了两个函数,他们都属于变量对象,但是调用f1()时,f1内部的各种变量以及传入的参数就会变成活动对象。当f1调用完成调用f2时,f2内部的变量又会变成活动对象。所以活动对象是在执行过程中产生的,当前活跃的对象。
  最后,我们回到执行环境。JS中有一个环境栈,栈顶是当前的执行环境。当执行流进入一个函数时,会创建当前函数的执行环境然后压栈。当函数执行完以后,会把执行环境出栈并销毁该执行环境,将控制权交给栈中的另一个环境。

三、作用域链

作用域链决定着可以访问的变量,每个执行环境都有一个作用域链。作用域链产生的过程是这样的:

  • 当一个函数被声明时,会在函数的[[Scope]]属性中保存当前环境的作用域链,当被调用的时候,会复制[[Scope]]属性中的对象,构建执行环境的作用域链。同时,会产生一个活动对象,将这个活动对象加到作用域链的顶端,就生成了该执行环境的作用域链。
  • 每次访问变量时,都会从作用域的顶端去查找变量,一层层往上找,直到找到变量或者将作用域链走完。

我们重点分析一下上面两句话:

  • 当一个函数被声明时,会在函数的[[Scope]]属性中保存当前环境的作用域链,在被调用时,将活动对象加入作用域顶端生成该执行环境的作用域链。重点是函数被声明时。如果不理解我们来看下面这个例子:
var msg = "window";

function getMsg() {
  console.log(msg);
}

function f() {
  var msg = "f";

  getMsg();
}

f();
//window

  一眼看去是不是感觉会输出f中的msg,因为f中的msg离getMsg比较近。那么我们来手动模拟一下作用域链的创建过程:

  1. getMsg函数被声明的时候,保存当前的作用域链到[[Scope]]属性
    在这里插入图片描述

  当前的环境是全局环境,所以保存的就是全局环境的作用域链,此时作用域链里面只有一个结点。
3. getMsg被调用的时将活动对象加到顶端在这里插入图片描述
  因为当前函数里面没有声明变量,所以活动对象内就是一个arguments。 查找作用域链的时候会从0往上找。
  好了,我们生成作用域链之后来看看我们调用getMsg会发生什么。我们访问了变量msg,所以会去作用域上查找,首先在自己的活动对象中查找,没有。就会往上,去全局对象里面查找,发现存在,所以就输出window。可以说getMsg函数根本访问不到f函数中的变量,就是因为他是在定义的时候保存的是当前环境的作用域链。所以最后是输出"window"而不是"f"。

四、闭包

  好了,终于到了我们的重点,闭包。前面我们说过,当执行环境出栈之后会销毁。对于普通函数来说是这样的,但是对于闭包又有所不同。下面我们从整个流程来对闭包进行解释。在此之前,我想说:我们应该理解闭包而不是背闭包的定义,我们应该觉得闭包和其他函数没有什么区别,不需要刻意去记,因为他内部就是那样执行的,没什么好背的,这样才是对闭包真正理解了
  想想我们的闭包:有权访问另一个函数作用域中的变量的函数。这意味着我们应该将函数定义在某个函数内部,才能访问到函数内部的变量。我们就使用最开始创建闭包时候使用的代码:

function returnf() {
	var a = 1;
	return function f() {
		console.log(a);
	}
}

var f = returnf();
f();
//1

  我们在函数内部定义了f,所以f的[[Scope]]属性上保存的作用域链应该是:
在这里插入图片描述
  随后我们将f返回,给了全局变量中的f,最后我们调用了f,此时f的作用域链就会变成下面这样:
在这里插入图片描述
  所以,看似我们已经退出了returnf的执行环境,但是还可以访问到a。从作用域链来看就很明显了,f的作用域链仍然包含了returnf的活动对象,很显然我们是可以访问到变量a的。
  这也是闭包和普通函数不同的地方。普通函数执行环境出栈之后会被销毁。但是对于闭包来说,函数返回之后,即returnf返回以后,其执行环境的作用域链会销毁,但是活动对象还保存在内存中。因为f函数的作用域链还在引用着returnf的活动对象,f还可以访问到returnf中的变量。这就是闭包。
  还有一个小问题就是,我们说过活动对象是在函数被调用的时候产生的。但是f函数被定义的时候returnf函数的活动对象肯定还没有产生,那么我们是如何保存returnf的活动对象的呢?对此,我的理解是,[[Scope]]函数中只是保存的作用域链的引用,所以后面当returnf被真正调用的时候,[[Scope]]中保存的作用域链也会跟着改变。但是f被调用的时候,是将[[Scope]]中的作用域了真正复制了一份,这样才能保证returnf的执行环境被销毁,作用域被销毁以后,自身的作用域链不受影响。

五、结语

  我觉得,至此,我们就可以抛弃闭包的概念了,因为这只不过是前人给他取的一个名字,我可以叫他开包,闭闭包。但是,我们必须知道这种现象,我们知道,可以通过一种手段在函数外部访问函数内部的变量,可以让函数内的变量在函数执行完毕后还留在内存中。而产生这种现象的原因是js的执行机制就是这样的,并没有为什么,也不需要我们去背什么内容。说实话,我现在也不明白是那个函数是闭包,还是这种现象叫闭包,又或者是哪一部分被成为闭包。但是,我觉得这都是无所谓的,不是吗?不过,根据MDN上的解释:闭包是函数和声明该函数的词法环境的组合,或许,整个returnf函数才被称做闭包。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值