JavaScript闭包
前言
每个函数都有自己的执行环境。当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[[Scope]])。然后使用this、arguments和函数参数、内部变量、内部函数引用来初始化函数的活动对象(activation object)。作用域链(堆栈)是指向活动对象的指针列表,该函数的活动对象在栈顶,全局变量对象在栈底。
PS:在 JavaScript 的执行中会一直存在一个Execute Context Stack , 最下面一个是Global Context,创建的execution context会被压入这个栈。
这里面提到几个关键字:
1. 执行环境(execution context)
声明该函数有权访问的变量和函数。
2. 作用域链(scope chain)
作用域链的创建规则是复制上一层环境的作用域链,并将指向本环境变量对象的指针放到链首。本质上是一个指向变量对象的指针列表,它只是引用,实际上不包含变量对象。另外JavaScript是函数作用域的,并没有像Java、C那样有块级作用域。
PS:JS主要是词法作用域(lexical scope,也即是静态作用域),在词法解析阶段既确定了。但是有两个特例,就是eval和with可以构成动态作用域。
3. 活动对象(activation object)
保存该函数arguments和函数参数、内部变量和内部函数引用。
例子:
function compare(value1, value2){ if(value1 < value2){ return -1; } else if(value1 > value2){ return 1; } else{ return 0; } } var result = compare(5, 10);
相应的执行环境图示:
闭包概念
JavaScript高级程序设计里面对闭包的描述是,“闭包是指有权访问另一个函数作用域中的变量的函数”。我觉得严格上讲,这个“另一个”是闭包函数的外部函数。
ECMAScript中,闭包指的是:
1. 从理论角度:所有的函数。因为它们都在执行的时候就将上层上下文的数据保存起来了(体现在作用域链)。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
2. 从实践角度:以下函数才算是闭包:
(1). 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)。
(2). 在代码中引用了自由变量。
PS:自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
闭包使用场景
1. 循环遍历中的延迟使用变量
我们可能在循环中为某些元素注册事件,或setTimeout执行一些代码,这段代码使用到循环部分的变量(我称这个变量为延迟使用变量),可能需要使用闭包保证变量的准确。
例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> function onMyLoad(){ var arr = document.getElementsByTagName("p"); //有问题写法 for(var i = 0; i < arr.length;i++){ arr[i].onclick = function(){ alert(i); } } //闭包写法 for(var i = 0;i<arr.length;i++){ (function (arg) { arr[i].onclick = function () { alert(arg); } })(i); } } </script></head><body onload="onMyLoad()"> <p>产品一</p> <p>产品二</p> <p>产品三</p> <p>产品四</p> <p>产品五</p> </body> </html>
2. 私有变量
JavaScript没有私有变量这东西,只是用闭包模拟而已。
例子:
function MyObject(){ var privateVar = 10; this.getPrivateVar = function(){ return privateVar; } }
3. 模块模式
为单例创建私有变量和特权方法(有权访问私有变量和私有函数的公有方法)就是模块模式。
例子:
var singleton = (function(){ var privateVar = 10; var getPrivateVar = function(){ return privateVar; } return { getPrivateVar : getPrivateVar } })();
4. 块级作用域
可以用来模拟块级作用域。
闭包注意地方
1. 闭包会使函数中的变量常驻内存,所以如果滥用闭包,会导致内存回收不回来,影响脚本性能。
例子:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
上面的代码并未利用到闭包的益处,因此,应该修改为如下常规形式:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
PS:前者每次实例化时候,每个对象里面都存在getName 、getMessage方法,后者则不会。
Q&A
1. 闭包是函数还是一种代码行为?所有函数都是闭包吗?
参考闭包概念
2. 变量查找顺序?
当在函数中访问一个变量的时候,搜索顺序是先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索作用域链上的活动对象,依次查找,直到找到为止。如果整个作用域链上都无法找到,则返回undefined。如果函数存在prototype原型对象,则在查找完自身的活动对象后,再查找自身的原型对象,再继续查找。
参考文献
1. JavaScript高级程序设计 第7章
2. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures (MDN)
3. http://www.cnblogs.com/TomXu/archive/2012/01/31/2330252.html (汤姆大叔)
本文为原创文章,转载请保留原出处,方便溯源,如有错误地方,谢谢指正。