闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术。下来对其进行一个小小的总结

什么是闭包?

官方说法:

  闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量------《javascript高级程序设计第三版》

下面就是一个简单的闭包:

function A(){
    var text="hello world";
    function B(){
       console.log(text);
    }
    return B;
}
var c=A();
c(); // hello world 

按照字面量的意思是:函数B有权访问函数A作用域中的变量(text),通过另一个函数C来访问这个函数的局部变量text。因此函数B形成了一个闭包。也可以说C是一个闭包,因为C执行的实际是函数B。

这个需要注意的是,直接执行A();是没有任何反应的。因为return B没有执行,除非是return B();

闭包的特性

闭包有三个特性:

 1.函数嵌套函数
2.函数内部可以引用外部的参数和变量
3.参数和变量不会被垃圾回收机制回收 解释一下第3点,为什么闭包的参数和变量不会被垃圾回收机制回收呢? 首先我们先了解一下javascript的垃圾回收原理:

(1)、在javascript中,如果一个对象不再被引用,那么这个对象就会被GC(garbage collection)回收;

(2)、如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。

上面的示例代码中A是B的父函数,而B被赋给了一个全局变量C(全局变量的生命周期直至浏览器卸载页面才会结束),这导致B始终在内存中,而B的存在依赖于A,因此A也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

闭包的作用:

其实闭包的作用也是有闭包的特性决定的,根据上面的闭包特性,闭包的作用如下:

1、可以读取函数内部的变量,而不是定义一起全局变量,避免污染环境

2、让这些变量的值始终保持在内存中。

闭包的代码示例

下面主要介绍几种常见的闭包,并进行解析:

demo1 局部变量的累加。

1
2
3
4
5
6
7
8
9
10
11
12
function countFn(){
    var count=1;
    return function(){             //函数嵌套函数
        count++;
        console.log(count);
    }
}
var y = countFn();    //外部函数赋给变量y;
y();   //2      //y函数调用一次,结果为2,相当于countFn()()
y();   //3     //y函数调用第二次,结果为3,因为上一次调用的count还保存在内存中,没有被销毁,所以实现了累加
y=null;  //垃圾回收,释放内存
y();  // y is not a function

由于第一次执行完,变量count还保存在内存中,所以不会被回收,以致于第二次执行的时候可以对上次的值就行累加。当引入y=null时,销毁引用,释放内存

demo2 循环中使用闭包


代码如下(下面的三个代码示例):我们的目的是想在每次循环中调用循环序号:

demo2-1

for (var i = 0; i < 10; i++) {
var a = function(){
console.log(i)
}
a() //依次为0--9
}

这个例子的结果是没有题的,我们依次打印出了0-9

每一层匿名函数和变量i都组成了一个闭包,但是这样在循环中并没有问题,因为函数在循环体中立即被执行了

demo2-2

但是在setTimeout中就不一样了

1
2
3
4
5
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  //10次10
    }, 1000);
}

我们期望的依次是打印出0--10,实际情况是打印出 10次10。即使吧setTimeout的时间改为0,也是打印出10个10。这是为什么呢?

这是因为setTimeout的一种机制,setTimeout是从任务队列结束的时候开始计时的,如果前面有进程没有结束,那么它就等到它结束再开始计时。在这里,任务队列就是它自己所在的循环。

循环结束setTimeout才开始计时,所以无论如何,setTimeout里面的i都是最后一次循环的 i。该代码中,最后的 i 为10,所以打印出了10个10.

这也就是为什么setTimeout的回调不是每次取循环时的值,而取最后一次的值

demo2-3

解决上面的setTimeout不能依次打印出循环的问题

for(var i=0;i<10;i++){
var a=function(e){
return function(){ console.log(e); //依次输入0--9
}
}
setTimeout(a(i),0);
}

因为setTimeout第一个参数需要一个函数,所以返回一个函数给它,返回的同时把 i 作为参数传进去,通过形参 e 缓存了i,也就是说e变量相当于是 i 的一个拷贝 ,并带进返回的函数里面。

当 setTimeout 的执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

也可以用下面的写法,和上面类似:

for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e); //依次打印出0-9
}, 0);
})(i);
}

 demo3 循环中添加事件

看下面的一个典型的demo.

我们希望每次点击li的时候,alert出li的索引值,所以用下面的代码:

 <ul id="test">
<li>第一个</li>
<li>第二个</li>
<li>第三个</li>
<li>第四个</li>
</ul> var nodes = document.getElementsByTagName("li");
for(i = 0,len=nodes.length;i<len;i++){
nodes[i].onclick = function(){
alert(i); //值全是4
};
}

事与愿违,无论点击哪一个li,都是alert(4),也就是都是alert循环结束之后的索引值。这是为什么呢?

这是因为循环中为不同的元素绑定事件,事件回调函数里如果调用了跟循环相关的变量,则这个变量取循环的最后一个值。

由于绑定的回调函数是一个匿名函数,所以上面的代码中, 这个匿名函数是一个闭包,携带的作用域为外层作用域(也就是for里面的作用域),当事件触发的时候,作用域中的变量已经随着循环走到最后了。

还有一点就是,事件是需要触发的,而绝大多数情况下,触发的时候循环已经结束了,所以循环相关的变量就是最后一次的取值。

要实现点击li,alert出li的索引值,需要将上面的代码进行以下的修改:

<ul id="test">
<li>第一个</li>
<li>第二个</li>
<li>第三个</li>
<li>第四个</li>
</ul>
var nodes=document.getElementsByTagName("li");
for(var i=0;i<nodes.length;i++){
(function(e){
nodes[i].onclick=function(){
alert(e);
};
})(i)
}

解决思路: 增加若干个对应的闭包域空间(这里采用的是匿名函数),专门用来存储原先需要引用的内容(下标)。

当立即执行函数执行的时候,e 值不会被销毁,因为它的里面有个匿名函数(也可以说是因为闭包的存在,所以变量不会被销毁)。执行后,e 值 与全局变量 i 的联系就切断了,

也就是说,执行的时候,传进的 i 是多少,立即执行函数的 e 就是多少,但是 e 值不会消失,因为匿名函数的存在。

也可以用下面的解法,原理是一样的:

<ul id="test">
<li>第一个</li>
<li>第二个</li>
<li>第三个</li>
<li>第四个</li>
</ul> var nodes=document.getElementsByTagName('li');
for(var i = 0; i<nodes.length;i++){
(function(){
var temp = i;
nodes[i].onclick = function () {
alert(temp);
}
})();
}

注意事项

  1、造成内存泄露

    由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,所以只有在绝对必要时再考虑使用闭包。

  2、在闭包中使用this也可能会导致一些问题。

       代码示例:来源于《js高级程序设计3》;

其实我们的目的是想alert出object里面的name

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

因为在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。

每个函数在被调用时,都会自动取的两个特殊变量:this和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止。也就是说,里面的return function只会搜索

到全局的this就停止继续搜索了。因为它永远不可能直接访问外部函数中的这两个变量。

稍作修改,把外部作用域中的this对象保存在一个闭包能够访问的变量里。这样就可以让闭包访问该对象了。

 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对象赋值给了that变量。定义了闭包之后闭包也可以访问这个变量。因此,即使在函数返回之后,that也仍引用这object,所以调用object.getNameFunc()()就返回 “My Object”了。

  

总结

当在函数内部定义了其他函数,就创建了闭包。闭包有权访问包含函数内部的所有变量。

  闭包的作用域包含着它自己的作用域、包含函数的作用域和全局作用域。

  当函数返回一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止。

  使用闭包必须维护额外的作用域,所有过度使用它们可能会占用大量的内存

有误之处,欢迎指出

首先,我们需要知道 JavaScript 里面的函数会创建内部词法作用域,是的,JavaScript 是词法作用域,也就是说作用域与作用域的层级关系在你书写的时候就已经确定了,而不是调用的时候,调用的时候确定的称为动态作用域,由于不是本篇文章的重点,就不再详细解释了,举两个例子自己领悟:

1
2
3
4
5
6
7
8
9
10
var name = 'fruit'
function apple () {
console.log(name)
}
function orange () {
var name = 'orange'
apple()
}
 
orange() // fruit

由于 JavaScript 是词法作用域,所以 apple 函数的局部作用域的上层作用域是全局作用域,从书写的位置就看出来了。假设 JavaScript 是动态作用域,就要看函数的调用顺序了,由于 apple 是在 orange 中调用的,所以 apple 的上层作用域是 orange 的局部作用域,那样的话会输出 orange!

这样的话,就制定了一套作用域访问的规则,这也是会有闭包的原因之一!

什么是闭包?

函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。

当我看到这句话的时候,泪流满面,国外的作者就是一语道破真相。简单的说,闭包就是引用,对谁的引用呢,对作用域的引用,只不过这种引用是有条件的——首先要记住作用域,然后再访问作用域!

什么叫记住作用域?

首先,我们都知道,在 JavaScript 里面,如果函数被调用过了,并且以后不会被用到,那么垃圾回收机制就会销毁由函数创建的作用域,我们还知道,对象(函数也是对象)的传递属于传引用,也就是类似于C语言里面的指针,并不会把真正的值拷贝给变量,而是把对象所在的位置传递给变量,所以,当函数被传引用到一个还未销毁的作用域的某个变量,由于变量存在,所以函数得存在,又因为函数的存在依赖于函数所在的词法作用域,所以函数所在的词法作用域也得存在,这样一来,就记住了该词法作用域。也就解释了该节的标题!下面举个例子说明一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 没有闭包现象的时候
function apple () {
var count = 0
 
function output () {
console.log(count)
}
 
fruit(output)
}
 
function fruit (arg) {
console.log('fruit')
}
 
apple() // fruit

当我们在调用 apple 的时候,本来 apple 在执行完毕之后 apple 的局部作用域就应该被销毁,但是由于 fruit(output) 将 output 传引用给了 arg,所以在 fruit 执行的这段时间内,arg 肯定是存在的,被引用的函数 output 也得存在,而 output 依赖的 apple 函数产生的局部作用域也得存在,这就是所谓的“记住”,把作用域给记住了!

但是,上面的例子是闭包现象吗?不是,因为函数 output 内部并没有访问记住的词法作用域的变量!在执行 fruit(output) 的过程中,只发生了 arg = output 的传引用赋值,而这个过程,只是把二者关联起来了,并没有去取 arg 引用的对象的值,所以实际上也并没有访问 output 所在的词法作用域!

记住并访问

上面的代码,稍微修改一下就会产生闭包现象了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function apple () {
var count = 0
 
function output () {
console.log(count)
}
 
fruit(output)
}
 
function fruit (arg) {
arg()
}
 
apple() // 0

现在,调用 fruit 时,apple 的局部作用域处于“记住”的状态,这时候, fruit 内部调用了 arg(),因为传引用,实际上访问并执行了 apple 局部作用域的 output,不仅仅是这样,output 内部还访问了 count 变量,这两次对 apple 局部作用域的引用都是闭包!

所以,之所以说所有回调函数的调用都会产生闭包现象,也是因为这个回调函数被传给了另外一个函数的参数,所以在另外一个函数的作用域消失之前,回调函数所在的词法作用域都被记住了,由于回调函数一定会被执行,所以回调函数所在的词法作用域至少被访问了一次,也就是至少访问回调函数本身,而这个对作用域的引用就是闭包。

闭包的作用

根据上面的讲解,估计你自己都能倒背如流了:

  1. 记住了函数所在的词法作用域,使其不被销毁;
  2. 能够访问函数所在词法作用域的变量;
  3. 创建模块(设计私有变量、公有函数等等)

还有很多,就不一一说了,下面就是利用闭包来解决一个常见的问题:

1
2
3
4
5
6
for (var i = 0; i < 5; i++) {
// 为了方便说明,给函数起名叫 apple
setTimeout(function apple () {
console.log(i) // 5 个 5
}, 0)
}

首先读者们先思考一个问题,这会产生闭包吗?

其实,上面也也会产生闭包,只不过 apple 记住并访问的是全局作用域,为什么呢?因为回调函数被当做 setTimeout 的参数传引用过去了,假设 setTimeout 实现如下

1
2
3
4
var setTimeout = function (callback, delay) {
// 延迟
callback()
}

看到没,因为 setTimeout 属于异步函数,所以会等到 JS 执行完毕之后再调用 callback,所以这段时间 callback 一直存在,所以函数 apple 也一直存在,所以全局作用域并不会等 JavaScript 执行完毕后就销毁(函数 apple 属于全局作用域的),这时候循环早结束了,所以 i 也变成了 5,于是乎,这个时候 apple 对全局作用域的引用称为闭包!

上面也说了回调函数调用都会产生闭包,这里就当举例说明一下!

那么怎么解决以上问题呢,很简单,让回调函数记住不同的作用域就行了!

1
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) {
// 为了方便说明,给函数起名叫 apple
(function baz (i) {
setTimeout(function apple () {
console.log(i)
}, 0)
})(i) // 0 1 2 3 4
}

上面用立即执行函数解决了问题,因为函数有局部作用域,所以调用 5 次函数会产生 5 个局部作用域,每个作用域的 i 由各次循环的 i 传递赋值,而每个作用域内都存在 apple ,都记住了各自的作用域,也就取到了不同的 i

不过通常来说,闭包都是按以下方式产生:

1
2
3
4
5
6
7
8
9
10
11
12
function apple () {
var name = 'apple'
 
var output = function () {
console.log(name)
}
 
return output
}
 
var out = apple()
out() // apple

上述将函数传引用给了全局作用域的变量,显然,闭包(对 apple 作用域的引用)在全局作用域都存在的情况下都可能发生,而且后面也执行了 out()

更常见的写法是下面这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Apple () {
var name = 'apple'
 
var output = function () {
console.log(name)
}
 
var setName = function (arg) {
name = arg
}
 
return {
output: output,
setName: setName
}
}
 
var apple = Apple()
apple.output() // apple
apple.setName('Apple')
apple.output() // Apple

这就是模块的一个例子,name 通常被称为私有变量!

结语

闭包没什么了不起的,这是被人玩的过于玄乎,其实这是人们很自然的想法:我在别的地方调用函数,总得保持函数正常运行吧!“闭包”这种机制很轻松的帮你解决了这个问题,我们不必搞懂闭包是什么也经常在实现它(如果这句话写在前面,会不会很多人都不看了,哈哈),这是语言设计者的过人之处,但是,你不搞懂它,总被人质疑:你不懂闭包吧!实际上,我们都实现了很多次闭包,所以,你把内部机制详细搞清楚了,就不会再害怕别人的质疑了,哈哈!当然,如果你喜欢钻研,更有必要了解其中的机制了,体会到寻找语言设计者设计思路的快感!

最后,再总结一下:函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。
最后的最后,再强调一下:闭包就是引用!

【原】理解javascript中的闭包(***********************************************)的更多相关文章

  1. 【原】理解javascript中的闭包

    闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术.下来对其进行一个小小的总结 什么是闭包? 官方说法: 闭包是指有权访问另一个函数作用域中的变量的函数.创建闭包的常见 ...

  2. 深入理解JavaScript中的闭包

    闭包没有想象的那么简单 闭包的概念在JavaScript中占据了十分重要的地位,有不少开发者分不清匿名函数和闭包的概念,把它们混为一谈,我希望借这篇文章能够让大家对闭包有一个清晰的认识. 大家都知道变 ...

  3. 深入理解javascript中的闭包!(转)

    1.闭包的经典错误 假如页面上有若干个div,我们想给它每个绑定一个onclick方法,于是有了下面的代码. function A(){ var divs=document.getElementsBy ...

  4. 全面理解JavaScript中的闭包的含义及用法

    1.什么是闭包 闭包:闭包就是能够读取其他函数内部变量的函数;闭包简单理解成“定义在一个函数内部的函数”. 闭包的形式:即内部函数能够使用它所在级别的外部函数的参数,属性或者内部函数等,并且能在包含它 ...

  5. 理解JavaScript中的闭包

    (这篇文章后面关于onclick事件的解释是错误的,请不要被误导了2016.6.16) 闭包这个概念给JavaScript初学者心中留下了巨大的阴影,网络上关于闭包的文章不可谓不多,但是能让初学者看懂 ...

  6. 理解 JavaScript 中的 this

    前言 理解this是我们要深入理解 JavaScript 中必不可少的一个步骤,同时只有理解了 this,你才能更加清晰地写出与自己预期一致的 JavaScript 代码. 本文是这系列的第三篇,往期 ...

  7. [译]Javascript中的闭包(closures)

    本文翻译youtube上的up主kudvenkat的javascript tutorial播放单 源地址在此: https://www.youtube.com/watch?v=PMsVM7rjupU& ...

  8. JavaScript中的闭包理解

    原创文章,转载请注明:JavaScript中的闭包理解  By Lucio.Yang 1.JavaScript闭包 在小学期开发项目的时候,用node.js开发了服务器,过程中遇到了node.js的第 ...

  9. 深入理解javascript原型和闭包(17)——补this

    本文对<深入理解javascript原型和闭包(10)——this>一篇进行补充,原文链接:http://www.cnblogs.com/wangfupeng1988/p/3988422. ...

随机推荐

  1. Python中如何将数据存储为json格式的文件(续)

    将上一篇中的例子,修改一下,将两个程序合二为一,如果存储了用户喜欢的水果就显示它,否则提示用户输入他喜欢的水果并将其存储到文件中. favorite.py import json filename = ...

  2. 1、初学探讨PYTHON的itchat和wxpy两库

    最近好奇学习了python,觉得简单明了,但是最头疼的就是调整空格和调试吧,的确调试不如C#使用visual studio 方便,都是使用print()来调试.也许因为我是菜鸟,如果大家还有更好的方法 ...

  3. TSOJ--2018 江苏省省赛

    [2018 江苏省大学生程序设计大赛] K. 2018 (测试数据范围有扩大) Problem Given a, b, c, d, find out the number of pairs of int ...

  4. 03007_HttpServlet

    1.创建 new---Servlet package com.gzdlh.servlet; import java.io.IOException; import javax.servlet.Servl ...

  5. Exchange 2010 打补丁的顺序

    升级的顺序: 客户端访问服务器 ——> 集线器传输服务器 ——>统一消息服务器(如果有的话)——> 邮箱服务器 ——> 边缘服务器.

  6. 【02】markdown工具推荐

    [02]信息 Windows 平台 MarkdownPad MarkPad Linux 平台 ReText Mac 平台 Mou 最新版Mac OS下Mou已经无法使用了.这里推荐一个跨平台的编辑器  ...

  7. ctype.h 第2章

    ctype.h ctype.h是c标准函数库中的头文件   定义了一批c语言字符分类函数   (c character classification functions) 用于测试字符是否属于特定的字 ...

  8. NYOJ 745 首尾相连数组的最大子数组和

    首尾相连数组的最大子数组和 时间限制:1000 ms  |  内存限制:65535 KB 难度:4   描述 给定一个由N个整数元素组成的数组arr,数组中有正数也有负数,这个数组不是一般的数组,其首 ...

  9. 九度oj 题目1385:重建二叉树

    题目描述: 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7 ...

  10. 转载:shell中awk printf的用法

    转载:http://www.linuxawk.com/jiaocheng/83.html 6. printf函数   打印输出时,可能需要指定字段间的空格数,从而把列排整齐.在print函数中使用制表 ...