一、什么是闭包
在高级程序设计中对闭包定义是这样的:“闭包是指有权限访问另一个函数作用域中的变量的函数。“
那么能读取另一个函数里的变量,无非两种情况:内部函数读取外部函数的;外部函数读取内部函数的。
内部函数读取外部函数的;
这种JavaScript本身就支持。
外部函数读取内部函数的
这个JavaScript本身不支持。
二、如何从外部读取局部变量?
出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。
那就是在函数的内部,再定义一个函数。并且还要把内部函数return回外部函数,或者想办法调用到外部函数,这样外部函数就是内部函数的子函数了。
function f1(){
var n=999;
function f2(){
alert(n); // 999
}
}
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
可以理解f2返回到f1,f1也就是f2的子函数了,有点自己调用自己的,字头咬字尾的感觉。
案例解释
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JavaScript闭包属性详解</title>
<style type="text/css">
p {
background: gold;
}
</style>
<script type="text/javascript">
function init() {
var pAry = document.getElementsByTagName('p');
for(var i=0; i<pAry.length; i++) {
// 每次点击都是5
pAry[i].onclick = function() {
alert(i);
}
}
}
</script>
</head>
<body onload="init();">
<p>产品0</p>
<p>产品1</p>
<p>产品2</p>
<p>产品3</p>
<p>产品4</p>
</body>
</html>
这里每次单击的时候才调用子函数function();调用父函数init()的变量i;单击事件的发生在初始化函数执行之后,才发生,所以i早已运行到i=5,元素长度了。
如何解决这个问题了,在单击function()函数外面加一层函数。让这层函数把i传过来的值保存住,例如保持到j中,然后单击function函数调用外面这一层函数的变量j值。
使用立即执行函数
立即执行函数
很多函数只会用一次,之后一直在等待执行浪费内存空间,而立即执行函数执行完就会把函数立即删除,之后调用也找不到。
主要用于初始化功能的函数。
基本形式:(function (){}())
其它方面与普通函数无任何区别,有执行期上下文,要预编译等等。
如果想打印0-9,的解决方法:
//立刻执行保存值(最常用)
function test() {
var arr = [];
for(var i = 0; i < 10; i++) {
// 实际循环立刻执行函数:立刻执行函数保存i值
(function(j){
// 立即执行函数AO中的 j = 0, 1, 2, 3......
// 每个立刻执行函数中都有一个唯一的j值
arr[j] = function() {
// 访问的是立刻执行函数AO中的j值:0, 1, 2....
document.write(j + ' ');
}
})(i);
}
return arr;
}
var myArr = test();
console.log(myArr); // [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
for(var j = 0; j < 10; j++) {
myArr[j](); //0 1 2 3 4 5 6 7 8 9
}
在调用每个匿名函数时,我们传入了变量i。由于函数参数是按值传递的,所以就会将变量i 的当前值复制给参数j。而在这个匿名函数内部,又创建并返回了一个访问j 的闭包。这样一来,myArr数组中的每个函数都有自己j 变量的一个副本,因此就可以返回各自不同的数值了。
案例解释
点击oli的数字打印出对应的数字值
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JavaScript闭包属性详解</title>
<style type="text/css">
p {
background: gold;
}
</style>
<script type="text/javascript">
function init() {
var pAry = document.getElementsByTagName('p');
for(var i=0; i<pAry.length; i++) {
/* // 每次点击都是5
pAry[i].onclick = function() {
alert(i);
}
*/
// 立刻执行函数包裹解决
(function(j){
pAry[j].onclick = function() {
alert(j + 1);
}
})(i);
}
}
</script>
</head>
<body onload="init();">
<p>产品0</p>
<p>产品1</p>
<p>产品2</p>
<p>产品3</p>
<p>产品4</p>
</body>
</html>
此题运行分析:
此题目的点击某行列表,显示序号。
如果直接在for循环中调用单击事件,由于单击事件发生在网页加载执行完for循环的时候,那时候i早已变成5。
如何解决:在单击事件函数外面加一个父函数,利用单击事件这个子函数的父函数的变量都要保存。让先在内存保存5个父函数作用域链。
同时利用立即执行函数:
补充:立即执行函数
立即执行函数案例
(function(a, b){ // 形参
console.log(a + b); // 3
}(1, 2)); // 实参
此题对应
(function(j){//j是形参
Li[j].onclick = function() {
console.log(j + 1);
}
})(i);//i是实参
所以利用立即执行函数,来实现外部调用内部变量。
还有参数:在调用每个匿名函数时,我们传入了变量i。由于函数参数是按值传递的,所以就会将变量i 的当前值复制给参数j。而在这个匿名函数内部,又创建并返回了一个访问j 的闭包。这样一来,function(j)形成[function(0),function(1),function(2),function(3),function(4)]数组,数组项为函数的数组,数组中的每个函数都有自己j 变量的一个副本,因此就可以返回各自不同的数值了。
父函数的作用域都存在内存中,单击第几个项目列表,对应的父函数的j就会被调出来,子函数调用父函数的变量。
闭包
创建
在Javascript中闭包的创建过程,如以下程序所示
代码
function a(){
var i=0;
function b(){
alert(++i);
}
return b;
}
var c=a();
c();
特点
这段代码有两个特点:
1、函数b嵌套在函数a内部;
2、函数a返回函数b。
这样在执行完var c=a( )后,变量c实际上是指向了函数b,再执行c( )后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,这是因为函数a外的变量c引用了函数a内的函数b。也就是说,当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。
作用
简而言之,闭包的作用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制不会收回a所占用的资源,因为a的内部函数b的执行需要依赖a中的变量。
在上面的例子中,由于闭包的存在使得函数a返回后,a中的i始终存在,这样每次执行c(),i都是自加1后alert出i的值。
那 么我们来想象另一种情况,如果a返回的不是函数b,情况就完全不同了。因为a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引 用,因此函数a和b互相引用但又不被外界打扰(被外界引用),函数a和b就会被回收。
微观世界
如 果要更加深入的了解闭包以及函数a和嵌套函数b的关系,我们需要引入另外几个概念:函数的执行环境(execution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。
1、当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain中只有window对象。
2、当函数a执行的时候,a会进入相应的执行环境(execution context)。
3、在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。
4、然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
5、下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
6、最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:
当在函数b中访问一个变量的时候,搜索顺序是先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。如果整个作用域链上都无法找到,则返回undefined。如果函数b存在prototype原型对象,则在查找完自身的活动对象 后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。