JavaScript之闭包
Posted 张松任
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript之闭包相关的知识,希望对你有一定的参考价值。
第一部分:基本概念
我们知道根据作用域链的规则,一个函数是不能访问到在与他同一个作用域内的函数内的内部变量的,如下所示:
function foo() { var a = 5; console.log(a); } foo(); function bar() { var b = a + 5; console.log(b); } bar();
在执行到第二个函数时,由于找不到a,所以会报错: a is not defined。
但是有的同学说了,我们可以将a作为全局变量啊,没错,这样一定可以做到, 但是全局变量是由污染的,一般我们最好定义为局部变量, 废话不多说,看看闭包怎么实现:
function foo() { var a = 5; console.log(a); function closure() { return a; } return closure; } var foo_bar = foo(); function bar() { var a = foo_bar() var b = a + 5; console.log(b); // 10 } bar();
这段代码同样执行了相同的任务,并且还成功的拿到了foo内部的局部变量。 虽然foo()函数之前已经执行结束了,但是因为闭包closure还没有执行,它保留着对a的引用,所以在foo函数执行完了之后a并不会被销毁。
这就是闭包。
在javascript中,闭包恐怕是很多人不能理解的一个概念了,甚至很多人也会把闭包和匿名函数混淆。
闭包是有权访问另一个函数作用域中的变量的函数。首先要明白的就是,闭包是函数。由于要求它可以访问另一个函数的作用于中的变量,所以我们往往是在一个函数的内部创建另一个函数,而“另一个函数”就是闭包。
比如之前提到过的作为比较函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function createComparisonFunction(propertyName){ return function (object1,object2){ var value1=object1[propertyName]; var value2=object2[propertyName]; if (value1<value2){ return -1; } else if (value1>value2){ return 1; } else { return 0; } }; } |
在这个函数中,由于return的函数它访问了包含函数(外部函数)的变量propertyName,所以我们认为这个函数即为闭包。即使这个闭包被返回了,而且是在其他地方调用了,但是它仍然可以访问propertyName,之所以还能够访问到propertyName这个变量,是因为内部函数(闭包)的作用域链中包含着createComparisonFunction函数的作用域。因此,要彻底搞清楚闭包,就需要彻底搞清楚函数被调用时发生了什么以及作用域链的有关知识。
当某个函数被调用时,会创建一个执行环境(函数一旦被调用,则进入函数执行环境)和相应的作用域链(作用域链是随着执行环境的不同而动态变化的)。(对于函数而言)之后使用arguments和其他命名参数的值来初始化函数的活动对象(每个执行环境都有一个变量对象,对于函数成为活动对象)。对于有闭包的函数而言,在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象始终处于第三位。。。直至作为作用域链终点的全局执行环境。
下面撇开闭包不谈,先通过一个简单的例子来理解作用域链以及变量对象和活动对象。
1
2
3
4
5
6
7
8
9
|
function compare(value1,value2){ if (value1<value2){ return -1; } else if (value1>value2){ return 1; } else { return 0; } }<br> var result=compare(5,10); |
以上代码首先定义了compare()函数,然后又在全局作用域中调用了它。当调用compare函数时,首先创建一个函数执行环境,每个执行环境又对应这一个变量对象,也就是说作用域链和函数执行环境是同时创建的,其中作用域链的前端即为compare函数的活动对象(在函数中,变量对象又称为活动对象)。在compare活动对象中包含了arguments、value1、value2(关键:尽管arguments数组对象包含value1和value2,但是我们还是要分开列举,而不是仅仅认为只有arguments包含于compare的活动对象,因为value1和value2也包含于compare的活动对象)。
对于上述代码而言,全局执行环境的变量对象(再次声明:每一个执行环境都存在相应的变量对象)中包含result和compare,该变量对象在compare()执行环境的作用域链的第二位。
当我们创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在compare函数内部的[[scope]]属性中,当调用compare函数时,会为函数创建一个执行环境,然后通过复制函数的[[scope]]属性中的对象构建起执行环境的作用域链。如下:
作用域链的本质就是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。无论什么时候在函数中访问一个变量,就会从作用域链的前端沿着作用域链搜索具有相应名字的变量。我们知道,全局环境的变量对象始终存在,而局部环境(如compare()函数执行环境)的变量对象只在函数执行的时候存在,一旦执行完毕,局部变量对象(活动对象)就会被销毁。但在闭包中,却与此不同。
把博文开始的代码复制如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function createComparisonFunction(propertyName){ return function (object1,object2){ var value1=object1[propertyName]; var value2=object2[propertyName]; if (value1<value2){ return -1; } else if (value1>value2){ return 1; } else { return 0; } }; } |
由于在一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction函数内部定义的匿名函数的作用域中实际包含着外部函数的活动对象。如果我们执行如下代码:
1
2
|
var compare=createComparisonFunction( "name" ); var result=compare({name: "zzw" },{name: "ht" }); |
这时候匿名函数的作用域链将引用着外部函数的活动对象。因为匿名函数从外部函数中被返回后,它的作用域链被初始化为包含外部函数的活动对象和全局变量对象。这样,匿名函数就可以访问外部函数中定义的所有变量。更为重要的是,即使外部函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparison()函数返回后,其执行环境的作用域链会被销毁,但是她的活动对象仍然保存在内存中。等到倪敏函数被销毁后,外部函数的活动对象才会被销毁。
由于闭包会携带者包含他的函数的作用域,因此会比其他函数占用更多的内存。过度的使用闭包可能会导致内存占用过多,我们建议只在绝对必要的时候再考虑使用闭包。
模仿块级作用域
1
2
3
4
5
6
|
( function (){ var now= new Date(); if (now.getMonth()==0&&now.getDate()==1){ alert( "happy new year" ); } })(); |
这就是模仿块级作用域,即定义并立即调用了一个匿名函数。
如下为演示其作用:
1
2
3
4
5
6
7
8
9
|
function outputNumbers(count){ ( function (){ for ( var i=0;i<count;i++){ console.log(i); } })(); console.log(i); } outputNumbers(5); |
这是在模仿块级作用域之外的console.log(i)就会导致错误,因为i未被定义。说明在执行了模仿块级作用域之后,内部的变量就被销毁了。
第二部分:深入JavaScript闭包
闭包经典例子
Example 1
function numberGenerator() { var num = 1; function checkNumber() { console.log(num); } num++; return checkNumber; } var number = numberGenerator(); number(); // 2
这就是一个闭包,因为在numberGenerator中的函数checkNumber()保留了对num的引用,且在numberGenerator执行完了之后num不会由此被释放,因为checkNumber还在保存着,所以不会消失。
他的好处就是我们可以很方便的取到在局部作用域的值, 当然如果你说我取到他的值只要设置成全局变量就行了啊,也不是不可以,但是最佳实践是尽量减少局部变量的使用,如果仅仅是为了取到,还是使用闭包的好。
Example 2
function sayHello() { var say = function () { console.log(hello); }; var hello = "hello world"; return say; } var sayHelloClosure = sayHello(); sayHelloClosure();
在这个例子中我们可以看到闭包可以访问得到外部封闭函数的所有变量,因为 hello 是在匿名函数之后定义的,但是仍然可以被访问得到。
通过上面两个例子,我们可以归纳为一句话:
定义在封闭函数中的变量,即使在该封闭函数已经执行完了之后,仍然能被访问到。
执行上下文如下所示:
执行上下文是用来记录代码运行的环境的, 可以使全局上下文,也可以是进入某个函数内的上下文(注意:全局上下文不包括内部函数内部的上下文)
值得注意的是,程序自始至终只能进入一个执行上下文,这也就是为什么说JavaScript是单线程的原因,即每次只能有一个命令在执行。
通常浏览器使用 栈 来维护执行上下文。 栈遵守后入先出的原则, 所以对应的执行上下文也是这样。另外,程序并不需要执行完当前的执行上下文中的所有代码再进入另一个,经常是当前执行上下文A执行到了一半就暂停,又进入了另一个B,当他在B中完全执行完了之后,又会回到A中,从它暂停的地方继续执行。每次一个上下文被另一个上下文所替代时, 这个新的上下文就入栈成为栈顶,即当前的上下文。
说明: 为什么很多函数喜欢用 for、bar、foobar来命名呢?
下面的一个例子,让我们感受一下js执行过程中的栈的概念:
var a = 20; var b = 50; (function () { c = 30; c = a + 50; console.log(this); function goog () { var d = 20; d = d + 50; function bar() { var b = d + 15; console.log(b); } console.log(this); console.log(d); return bar; } var barCloure = goog(); barCloure(); console.log("444"); })(); var m = 50; console.log(m);
他在chrome中的断点如下所示:
打了这么多,是因为我希望进入一个执行环境就停下,我们可以仔细观察每一个细节:
1.进入第一个语句执行时如下:
可以看到 Call Stack 和 Scope ,其中Call Stack 最直白的理解就是“叫它进栈”,且Call Stack中的第一个表示的就是当前的执行上下文,而后面的9表示现在运行的行数,程序执行到第10行就会为10.
注: 在当前执行环境下会执行直到12行,浏览器 认为第11行也是全局的, 12行就会进入一个匿名函数。
2. 程序执行到12行
可以看到执行到12行时, 我们显然又进入了一个执行上下文,所以 Call Stack 让这个新的执行上下文入栈,且当前在这个新的执行上下文执行,第13行, 而在栈底就是那个全局的执行上下文。
值得注意的是:在从全局的执行上下文进入到匿名函数的执行上下文中时,他会记住在执行完这个匿名函数后(匿名函数退栈后)应该接着从哪里执行, 即第30行。 另外,浏览器认为在 <script>标签下的js即我们认为的全局环境也是一个匿名函数的执行上下文。
另外,可以看到这时的Scope,即作用域是在一个Local作用域中,而他还引用者全局作用域,代表着他可以访问全局作用域的所有变量和Local作用域中的所有变量和方法。 或更为概括的说:
在当前的执行上下文情况下, 我们可以访问的到 Local 和 Global 下的所有属性和方法。
且在Local中也指明了this是指向window的,这对于我们判断有很大的好处。后面会讲到这个例子。
3. 程序执行到26行后,由于调用了goog()函数,所以进入了第16行,进入了goog()个执行环境。
可以看到,我们已经进入了goog的执行环境,所以Call Stack已经让goog执行上下文入栈,同时因为goog所在的匿名函数的执行上下文还没有执行完,所以把它压在了下面,并保留了暂停的位置,以便于goog退栈后记住重新开始的位置。
而在Scope中可以看到当前的作用域同样是局部作用域(在js中作用域只有全局作用域和局部作用域的区别。)
直接看下面这张图吧:
这里由于bar在执行时对goog中的d引用了,所以在goog()执行完了之后d仍然没有 被清除。
所以在作用域中显示了 Local 对goog中的d的引用,而goog中的d还引用了外层的匿名函数的m, 所以这里 bar 就是一个闭包。
Closures are functions that refer to independent (free) variables (variables that are used locally, but defined in an enclosing scope). In other words, these functions \'remember\' the environment in which they were created.
所以,bar记住的环境是 goog函数内的环境,而并没有因为在goog执行结束之后就把环境丢弃了。
上述问题的代码如下:
看官网讲解会更好。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
即下面这种方式也是我们常用的:
function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
大师牛人,宁有种乎?
以上是关于JavaScript之闭包的主要内容,如果未能解决你的问题,请参考以下文章