闭包
“官方”的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
简单来说:
- 函数执行时返回内部私有函数, 或者通过其他方式将内部私有函数保留在外(比如说通过将其内部私有函数的引用赋值外部变量),
从而阻止该函数内部作用域等被执行引擎回收。
- 在函数外部通过访问暴露在外的函数内部私有函数, 从而具有访问函数内部私有作用域的效果, 就是闭包。
ES6之前, 通常我们实现的模块就是利用了闭包. 闭包依赖的结构有个鲜明的特点, 即: 一个函数在词法作用域之外执行。
如下, f2是闭包的关键, 它的词法作用域便是函数f的内部私有作用域, 且它在f的作用域外部执行。
var h = 1;
function f(){
var i = 2;
return function f2(){
var j = 3 + i + h;
console.log(j);
}
}
var ff = f();
ff();//6
由于定义时 f2 处于 f 的内部, 因此 f2 内可以访问到 f 的内部私有作用域, 这样通过返回 f2 就能保证在 f 函数外部也能访问到 i 变量.
当f2执行时, 变量 j 处于scope chain的 index0的位置上, 变量 i 和变量 h 分别处于 scope chain 的
index1 index2 的位置上. 因此 j 的赋值过程其实就是沿着 scope chain 第二层 第三层 依次找到 i 和 h 的值,
然后将它们和3一起求和, 最终赋值给 j .
浏览器沿着 scope chain 寻找变量总是需要耗费CPU时间, 越是 scope chain 的 外层(或者离f2越远的变量), 浏览器查找起来越是需要时间, 因为 scope chain 需要历经更多次遍历. 因此全局变量(window)总是需要最多的访问时间。
---------------------如果有兴趣的可以往下看-------------------------
(以下内容并非原创,纯属来源于网上的朋友)
如果要更加深入的了解闭包以及函数 f 和嵌套函数 f2 的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。
- 当定义函数 f 的时候, js解释器会将函数a的作用域链(scope chain)设置为定义 f 时 a 所在的”环境”, 如果 f 是一个全局函数,则scope chain中只有window对象。
- 当执行函数 f 的时候, f 会进入相应的执行环境(excution context).
- 在创建执行环境的过程中, 首先会为 f 添加一个scope属性, 即a的作用域, 其值就为第1步中的scope chain. 即a.scope=f 的作用域链.
- 然后执行环境会创建一个活动对象(call object). 活动对象也是一个拥有属性的对象, 但它不具有原型而且不能通过JavaScript代码直接访问. 创建完活动对象后, 把活动对象添加到 f 的作用域链的最顶端. 此时a的作用域链包含了两个对象: f 的活动对象和window对象.
- 下一步是在活动对象上添加一个arguments属性, 它保存着调用函数 f 时所传递的参数.
- 最后把所有函数 f 的形参和内部的函数 f2 的引用也添加到 f 的活动对象上. 在这一步中, 完成了函数 f2 的定义, 因此如同第3步, 函数 f2 的作用域链被设置为 f2 所被定义的环境, 即 f 的作用域.
到此, 整个函数 f 从定义到执行的步骤就完成了. 此时 f 返回函数 f2 的引用给 ff, 又函数 f2 的作用域链包含了对函数 f
的活动对象的引用, 也就是说 f2 可以访问到 f 中定义的所有变量和函数. 函数 f2 被 ff 引用, 函数 f2又依赖函数 f ,
因此函数 f 在返回后不会被GC回收.
当函数 f2 执行的时候亦会像以上步骤一样. 因此, 执行时 f2 的作用域链包含了3个对象: f2 的活动对象、f 的活动对象和window对象, 如下图所示:
如图所示, 当在函数 f2 中访问一个变量的时候, 搜索顺序是:
- 先搜索自身的活动对象, 如果存在则返回, 如果不存在将继续搜索函数 f 的活动对象, 依次查找, 直到找到为止.
- 如果函数 f2 存在prototype原型对象, 则在查找完自身的活动对象后先查找自身的原型对象, 再继续查找. 这就是Javascript中的变量查找机制.
- 如果整个作用域链上都无法找到, 则返回undefined。