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.
小结, 本段中提到了两个重要的词语: 函数的定义与执行. 文中提到函数的作用域是在定义函数时候就已经确定, 而不是在执行的时候确定(参看步骤1和3).用一段代码来说明这个问题:
function f(x) { var g = function () { return x; } return g; } var h = f(1); alert(h());
这段代码中变量h指向了f中的那个匿名函数(由g返回).
假设函数h的作用域是在执行alert(h())确定的, 那么此时h的作用域链是: h的活动对象->alert的活动对象->window对象.
假设函数h的作用域是在定义时确定的, 就是说h指向的那个匿名函数在定义的时候就已经确定了作用域. 那么在执行的时候, h的作用域链为: h的活动对象->f的活动对象->window对象.
如果第一种假设成立, 那输出值就是undefined; 如果第二种假设成立, 输出值则为1。
运行结果证明了第2个假设是正确的,说明函数的作用域确实是在定义这个函数的时候就已经确定了.
闭包有可能导致IE浏览器内存泄漏
先看一个栗子:
function f(){ var div = document.createElement("div"); div.onclick = function(){ return false; } }
上述div的click事件就是一个闭包, 由于该闭包的存在使得 f 函数内部的 div 变量对DOM元素的引用将一直存在.