前言: 在这篇文章里,我将对那些在各种有关闭包的资料中频繁出现,但却又千篇一律,且暧昧模糊得让人难以理解的表述,做一次自己的解读。或者说是对“红宝书”的《函数表达式/闭包》的那一章节所写的简洁短小的描述,做一些自己的注解,仅供抛砖引玉
 
好,看到文章标题,你就应该知道我下文的画风是怎样的了,嘿嘿嘿...
 

闭包的概念

首先要搞懂的就是闭包的概念: 闭包是能够访问另一个函数作用域中变量的函数(这个“另外一个函数”,通常指的是包含闭包函数的外部函数), 例如:
function outerFunction () {
var a =
return function () {
console.log(a);
}
}
 
var innerFunction = outerFunction();
innerFunction();
在这个例子里:负责打印a的匿名函数被包裹在外部函数outerFunction里面,且访问了外部函数outerFunction作用域里的变量a,所以从定义上说,它是一个闭包。
 
我在标题上说过我要讲故事的对吧,但...  在听故事前,你需要先看以完下两个方面的知识:
 
1. 谈谈函数执行环境,作用域链以及变量对象
2. 闭包和函数柯里化
 

谈谈函数执行环境,作用域链以及变量对象

(作用域和执行环境其实是同一个概念,我下面的介绍主要会以后者为名)
 
首先我想让大家理解的是:  函数执行环境,作用域链以及变量对象的相互关系以及各自作用
 
先引用一下《javaScript高级语言程序》中的两段原话:
 
1. "当某个函数被调用时,会创建一个执行环境 (execution context)及相应的作用域链(scope Chain)"   — —第178页    7.2  闭包
2. "每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中"   — — 第73页   4.2  执行环境及其作用域
 
这是我在“红宝书”上所能找到的最关键的一句话,但看完后,我。。。。一脸懵逼!!!! 现在我知道了函数被调用的时候就会连带产生和这个函数息息相关的三个东东:
 
执行环境(execution context),作用域链(scope Chain)以及变量对象(variable object),但这三者们具体是什么关系呢?
 
后来我看了汤姆大叔的文章,顿时豁然开朗: (文末有相关链接)
 
下面贴出他写的伪代码:
 
ExecutionContext = {
    variableObject: { .... },
    this: thisValue,
    Scope: [ // Scope chain
      // 所有变量对象的列表
    ]
};
 
所以说,关于三者,更准确的描述或许是这样的: 在函数调用的时候,会创建一个函数的执行环境,这个执行环境有一个与之对应的变量对象和作用域链。
 
嗯,这下三者的关系应该就比较明朗了吧(虽然好像也并没有什么卵用。。)
所以说,下面我要介绍的是变量对象和作用域链的作用。
 
变量对象的作用:
 
每个函数的变量对象保存了它所拥有的数据,以供函数内部访问和调用,这些数据包括:(位于执行环境内部的)
 
1.声明变量
2.声明函数
3.接收参数
 
虽然我们编写的代码无法访问到这个对象,但解析器还处理数据的时候会在后台使用它
 
例如:
function foo (arg) {
    var variable = ’我是变量‘;
    function innerFoo () {
alert("我是彭湖湾")
    }
}
foo('我是参数');
这个时候执行环境对应的变量对象就变成了这样:
ExecutionContext = {
    variableObject: {
      variable:’我是变量‘
      innerFoo: [对函数声明innerFoo的引用]
      arg: '我是参数'
    },
    this: thisValue,
    Scope: [ // Scope chain
      // 所有变量对象的列表
    ]
};
 
作用域链的作用
 
通过作用域链,函数能够访问来自它上层作用域(执行环境)中的变量
 
先看一个例子
function foo () {
    var a = ;
    function innerFoo () {
console.log(a)
    }
    innerFoo();
}
foo(); // 打印  1
在这里,变量a并不是innerFoo作用域(执行环境)内声明的变量呀,为什么能够取到它外部函数foo作用域内的变量呢? 这就是作用域链的作用啦,现在的执行环境用汤姆大叔的伪代码描述是这样的:
 
InnerFoo函数的执行环境:
InnerFooExecutionContext = {
    variableObject: {
    },
    this: thisValue,
    Scope: [ // Scope chain
       innerFooExecutionContext. variableObject,  // innerFoo的变量对象
       FooExecutionContext.variableObject,  // Foo的变量对象
       globalContext.variableObject   // 全局执行环境window的变量对象
    ]
};
Foo函数的执行环境:
FooExecutionContext = {
    variableObject: {
       a:
    },
    this: thisValue,
    Scope: [ // Scope chain
         FooExecutionContext.variableObject,  // Foo的变量对象
         globalContext.variableObject   // 全局执行环境window的变量对象
    ]
};
你可以看到,作用域链其实就是个从当前函数的变量对象开始,从里到外取出所有变量对象,组成的一个列表。通过这个作用域链列表,就可以实现对上层作用域的访问。
 
innerFoo在自己的执行环境的变量对象中没有找到 a 的变量声明, 它感到很苦恼,但转念一想: 诶! 我可以向上层函数执行环境的变量对象(variableObject)中找嘛! 于是乎沿着作用域链( Scope chain)攀爬,往上找变量a,幸运的是,在父函数Foo的变量对象,它找到了自己需要的变量a
“啊! 找到a了! 它的值是1”
 
如果今天innerFoo恰逢水逆,没有在Foo的变量对象中找到a呢? 那么它会沿着作用域链继续向上“攀爬',直到它到达全局执行环境window(global)
 
 

 
 
 

闭包和函数柯里化

闭包和函数柯里化在定义一个函数的时候,可能会使用到多层嵌套的闭包,这种用法,叫做“柯里化”。 而闭包柯里化有两大作用:参数累加和延迟调用
例子:
function foo (a) {
     return function (b) {
   return function (c) {
console.log(a + b + c);
   }
     }
}
foo('我')('叫')('彭湖湾'); // 打印 我叫彭湖湾
 
 
从这里,我们可以很直观地看出闭包柯里化的时候参数累加的作用
我们把上面那个例子改变一下:
function foo (a) {
    return function (b) {
return function (c) {
console.log(a + b + c);
}
    }
}
 
var foo1 = foo('我');
var foo2 = foo1('叫');
foo2('彭湖湾'); // 打印 我叫彭湖湾
 
可以看到,最内层的闭包在外层函数foo和foo1调用的时候都没有调用,直到最后得到foo2并调用foo2()的时候,这个最内层的闭包才得到执行, 这也是闭包的一大特性——延迟执行
 

 
好,如果你看完了以上两个方面的内容,那接下来就可以听我将故事啦。
 

闭包造成的额外的内存占用  (注意我说的不是“内存泄漏”!)

函数的变量对象一般在函数调用结束后被销毁(它的“任务”已经完成了,可以被垃圾回收了)
 
但闭包的情况却不同
function foo (a) {
    return function () {
console.log(a)
    }
}
 
var foo1 = foo();
var foo2 = foo();
var foo3 = foo();
foo1();  // 输出1
foo2();  // 输出2
foo3();  // 输出3
 
实际上,foo函数调用结束后, foo函数的变量对象并不会被立即销毁,而是只有当取得foo函数闭包的值的foo1, foo2, foo3调用结束, 这三个函数的变量对象和作用域链被销毁后, foo函数才算“完成任务”,这时,它才能被销毁。
 

 
所以说,闭包会造成额外的内存占用(注意这种内存占用是有必要的,和内存泄漏不同!!)
 
如果你不是很明白。看看我下面这个故事:
 
故事: 有这么一个差异化明显的班级,班级成员由一个学霸和一堆学渣组成,在某次监管很宽松的测验中(老师不在) , 为了其他人能够不去教导处喝茶,非常老好人的学霸用10分钟做完了试卷后,把卷子给全班同学抄, 弘扬了中华民族一贯以来的团结和谐,共同奋斗的精神。。。。
 
这个外层函数,就是那个学霸; 
里面的闭包,就是那些学渣;
闭包所引用的外层函数的变量,就是学霸递给学渣们的试卷!!!!!
 
问:
 
学霸10分钟就做完了试卷,那为什么他一整节课都忙的满头大汗???(为什么外层函数的变量对象在外层函数调用完毕之后没有立即销毁???)
 
答案
 
因为他要忙着给其他同学们传递他做好的试卷,又因为他是个老好人,所以只有最后一个同学做完试卷后,这位善良“负责”的学霸才能休息 呀!!!!!!!(因为闭包通过作用域链还保留着对这个外部函数的变量对象的引用,所以外部函数并不能立即得到销毁)
 

 

闭包只能取得包含函数的最后一个值

让我们来看看《红宝书》闭包那一章节中的一个典型例子:
 
function createArray() {
   var arr = new Array();
   for (var i = ; i < ; i++) {
      arr[i] = function () {
         return i;
      }
    }
    return arr;
}
var funcs = createArray();
for (var i = ; i < funcs.length; i++) {
     document.write(funcs[i]() + "<br />");
}
 
实际上,最后输出的不是1,2,3,4,5,6,7 。。10,而是全部都是10,为什么? 因为:
 
1. 这几个函数都保留着对同一个外部函数的变量对象的引用
2. 因为闭包函数“延迟调用”的特性,而关键变量值i的获取是在闭包函数调用(f也即uncs[i]())的时候才从外部函数的变量对象中获取,而这个时候,外部函数早就完成for循环使 i =10了 !!!
 
 

 
还不太理解的话看我接下来的这个故事:
 
改完卷子后, 老师把除了学霸以外的所有同学叫到办公室:为什么你们的答案TM都是一样的???
 
在这之前我再附加一个现实场景: 学霸虽然学力无穷,但对一些比较难的题目,也不是一下子就能答对的,比如下面这道选择题:
 
请问中国最富盛名的博客社区是以下哪个?
A: 博客园   B: CSDN  C:51CTO  D: JB之家 
 
但学霸不知道哪根筋断了选了B, 后来变本加厉改为C, 最后无可救药的改为D,但最后学霸发现做这道题的时候自己犯了万分之一的脑子进水的概率,于是把前面的答案都涂掉了,重新选为A !!
 
问题: 学霸做这道题的时候,他先后选了A—>B—>C—>D—>A; 那么!!!为什么全班人都不是:“有的选A有的选B有的选C有的选D”, 而是全部选了A呢? 
答案:
因为!! 学霸把试卷全班传阅的时候
 
1.其他人参考的只有学霸那唯一一张试卷(唯一一章是重点,划起来呀!!)
2.其他人抄的时候,学霸已经做完了!做完了!做完了!(重要的事情说三遍)所以那道选择题其他人只能看到他最后选的A,而不是B,C,D!!
 

 
参考书籍或文章:
 
1.《javaScrpt高级语言程序设计》
2. 深入理解JavaScript系列(12):变量对象(Variable Object) ——汤姆大叔  http://www.cnblogs.com/TomXu/archive/2012/01/16/2309728.html
3.深入理解JavaScript系列(14):作用域链(Scope Chain)   http://www.cnblogs.com/TomXu/archive/2012/01/18/2312463.html
 
 

【javascript】详解javascript闭包 — 大家准备好瓜子,我要开始讲故事啦~~的更多相关文章

  1. js对象详解(JavaScript对象深度剖析,深度理解js对象)

    js对象详解(JavaScript对象深度剖析,深度理解js对象) 这算是酝酿很久的一篇文章了. JavaScript作为一个基于对象(没有类的概念)的语言,从入门到精通到放弃一直会被对象这个问题围绕 ...

  2. 详解javascript中的this对象

    详解javascript中的this对象 前言 Javascript是一门基于对象的动态语言,也就是说,所有东西都是对象,一个很典型的例子就是函数也被视为普通的对象.Javascript可以通过一定的 ...

  3. 详解JavaScript调用栈、尾递归和手动优化

    调用栈(Call Stack) 调用栈(Call Stack)是一个基本的计算机概念,这里引入一个概念:栈帧. 栈帧是指为一个函数调用单独分配的那部分栈空间. 当运行的程序从当前函数调用另外一个函数时 ...

  4. 详解javascript的类

    前言 生活有度,人生添寿. 原文地址:详解javascript的类 博主博客地址:Damonare的个人博客 Javascript从当初的一个"弹窗语言",一步步发展成为现在前后端 ...

  5. 详解Javascript的继承实现(二)

    上文<详解Javascript的继承实现>介绍了一个通用的继承库,基于该库,可以快速构建带继承关系和静态成员的javascript类,好使用也好理解,额外的好处是,如果所有类都用这种库来构 ...

  6. 【转】详解JavaScript中的this

    ref:http://blog.jobbole.com/39305/ 来源:foocoder 详解JavaScript中的this JavaScript中的this总是让人迷惑,应该是js众所周知的坑 ...

  7. Day03 javascript详解

    day03 js 详解 JavaScript的基础 JavaScript的变量 JavaScript的数据类型 JavaScript的语句 JavaScript的数组 JavaScript的函数 Ja ...

  8. 详解 javascript中offsetleft属性的用法(转)

    详解 javascript中offsetleft属性的用法 转载  2015-11-11   投稿:mrr    我要评论 本章节通过代码实例介绍一下offsetleft属性的用法,需要的朋友可以做一 ...

  9. 详解JavaScript的任务、微任务、队列以及代码执行顺序

    摘要: 理解JS的执行顺序. 作者:前端小智 原文:详解JavaScript的任务.微任务.队列以及代码执行顺序 思考下面 JavaScript 代码: console.log("scrip ...

  10. (转载)详解Javascript中prototype属性(推荐)

    在典型的面向对象的语言中,如java,都存在类(class)的概念,类就是对象的模板,对象就是类的实例.但是在Javascript语言体系中,是不存在类(Class)的概念的,javascript中不 ...

随机推荐

  1. git常用基本命令

    一定要以管理员的身份打开,否则有些命令不能用,比如ssh -T git@github.com(查看配置ssh是否成功)@初始化git git config --global user.name ruo ...

  2. 数据结构(C语言版)链表相关操作算法的代码实现

    这次实现的是带头结点的单链表的初始化.遍历.创建.插入.删除.判断链表是否为空.求链表长度函数,编译环境是vs2013. 其中插入和删除函数中循环的条件目前还不太明白. #include<ios ...

  3. CJOJ 1087 【NOIP2010】乌龟棋 / Luogu 1541 乌龟棋(动态规划)

    CJOJ 1087 [NOIP2010]乌龟棋 / Luogu 1541 乌龟棋(动态规划) Description 小明过生日的时候,爸爸送给他一副乌龟棋当作礼物. 乌龟棋的棋盘是一行N个格子,每个 ...

  4. java基础05 集合

    一.集合的由来? 我们学习Java,可以操作很多对象 ,存储 的容器有数组和StringBuffer,StringBuilder; 而数组的长度固定,所以不适合做变化的需求,Java就提供了集合供我们 ...

  5. nyoj_83:迷宫寻宝(二)(计算几何)

    题目链接 枚举所有墙的2n个端点与宝物的位置作为一条线段(墙的端点必定与边界重合), 求出与之相交的最少线段数(判断线段相交时用跨立实验的方法),+1即为结果. #include<bits/st ...

  6. Windows7 下安装 tersorflow

    最近看起深度学习的一些知识,想要学习一个框架.在网上看了别人对这些框架的评比后,决定学习 tersorflow.之前一直以为 tersorflow 只可以在 Linux 下安装,出乎意料的是,Wind ...

  7. STM32使用cube生成的程序后在keil5编译后首次SWD可以下载再次下载不行的解决办法。

    使用cube配置导出工程在keil5编译后首次SWD下载可以再次下载不行的解决办法. 1原因: cube使用的是HAL库,初始化语句里面禁用了调试功能. 在stm32f1xx_hal_msp.c中 _ ...

  8. ionic2+Angular 使用ng2-file-upload 插件上传图片并实现本地预览

    第一步:npm install ng2-file-upload --save 安装 ng2-file-upload 第二步:在需要使用该插件的页面的对应module文件的imports中引入Commo ...

  9. (转)Linux系统安装时分区的选择

    场景:对于Linux系统的分区总是迷迷茫茫的,还是实践少,基础不牢. 以前初识Linux时,对Linux系统安装时分区的选择,一点都不了解,导致几次没法进行下一步安装,因此就静下心来,专门拿出时间研究 ...

  10. Java 枚举7常见种用法(转)

    JDK1.5引入了新的类型——枚举.在 Java 中它虽然算个“小”功能,却给我的开发带来了“大”方便. 用法一:常量 在JDK1.5 之前,我们定义常量都是: public static fianl ...