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

闭包的概念

首先要搞懂的就是闭包的概念: 闭包是能够访问另一个函数作用域中变量的函数(这个“另外一个函数”,通常指的是包含闭包函数的外部函数), 例如:
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. SDP

    SDP语法 一个SDP描述含有会话级信息和媒体级信息.会话级信息应用于整个会话.例如:它能成为会话始发者或者会话的名字.媒体级信息作用于特殊的媒体流.例如:它能作为一个编码器给音频流编码或者是给视频流 ...

  2. (转载)Oracle10g 数据泵导出命令 expdp 使用总结(二)

    原文链接:http://hi.baidu.com/edeed/item/2c454cff5c559f773d198b94 Oracle10g 数据泵导出命令 expdp 使用总结(一) 1.1.2 e ...

  3. [基础架构]PeopleSoft Application Server 重要文件说明

    我们都知道PeopleSoft是由几个不同的服务组成的,他们在PeopleSoft体系结构中扮演着自己的角色.这些服务具有不同的文件结构并包含重要的可执行文件和配置文件. 以下是Peoplesoft体 ...

  4. 奇舞js笔记——第0课——如何写好原生js代码

    摘要 1.好的代码职责要清晰,javscript不要用来操作样式: 2.API要设计的合理:通用性,适度的抽象(数据抽象,过程抽象),可扩展性: 3.效率问题:用好的.合适的算法(前端程序员要把自己当 ...

  5. jsp 按钮颜色

    jsp 按钮颜色 第一种方法 <input style= "color:#FF0000;background-color:#00FF00;" type="butto ...

  6. java volatitle介绍与使用

    关于关键字volatile可以说是Java虚拟机提供的轻量级的同步机制,但是它并不容易完全被正常.完整地理解,以至于许多程序员都不习惯去使用它,遇到需要处理多线程数据竞争问题的时候一律使用Synchr ...

  7. python机器学习模块安装

    环境:RHEL6.5 离线安装 ############################################################################ 一,本地yum ...

  8. iOS 图文并茂的带你了解深拷贝与浅拷贝

    一.概念与总结 1.浅拷贝 浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间,当内存销毁的时候,指向这片内存的几个指针需要重新定义才可以使用,要不然会成为野指针. 浅拷贝就是拷贝指 ...

  9. man rsync翻译(rsync命令中文手册)

    本文为命令rsync的man文档翻译,几乎所有的选项都翻译了,另外关于筛选规则部分只翻译了一部分.由于原文很多地方都比较啰嗦,所以译文中有些内容可能容易让国人疑惑,所以我个人在某些地方加上了注释.若有 ...

  10. electron入门代码

    Electron 提供了一个实时构建桌面应用的纯 JavaScript 环境.Electron 可以获取到你定义在 package.json 中 main 文件内容,然后执行它.通过这个文件(通常我们 ...