JavaScript闭包
什么是闭包
在JavaScript中,闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包指的就是在一个函数内部创建另一个函数。
闭包的作用
1.可以使外部作用域读取到内部作用域的变量
在js中的作用域一般是指函数,每个作用域相当于一个执行环境,每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
当代码在执行环境中执行时,会创建变量对象的一个作用域链,作用域链能对当前执行环境有权访问的所有变量和函数进行有序的访问(本质上是一个指向变量对象的指针列表)。在JavaScript中,这种顺序是单向的,内部环境可以通过作用域链依次访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。表现在代码中就是,变量会从自身所处的执行环境开始依次向外层执行环境查找他的定义,如果查到全局的执行环境(window)依然没有该变量的定义,则返回undefined。
使用闭包,我们可以借助变通的方式从外部执行环境中拿到内部执行环境的变量。
不使用闭包:
function f1() { var m=0; var n=1; } f1(); console.log(m); //undefined console.log(n); //undefined
使用闭包:
function f1() { var m=0; var n=1; return function(){ console.log(m); console.log(n); } } var result=f1(); result(); //m=0 n=1
2.使函数中的变量在内存中的周期可控
一般上来讲,当函数执行完毕后,局部活动对象就会被销毁。但闭包的情况有所不同,在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象也添加到它的作用域链中。
普通函数执行完后,他的局部活动对象就会被销毁。
function f1() { var n=1; n++; console.log(n) } var result=f1; result(); //n=2 result(); //n=2
这里全局变量nAdd的生成依赖于f1执行,当f1执行完成后它的局部变量即被销毁,nAdd作为一个全局变量它的变量对象并没有被销毁。当f1再次执行时nAdd被重新赋值。(这里的nAdd虽然本质上也是一个闭包,但他依赖的外部函数是一个局部变量)
function f1() { var n=1; nAdd = function() { n++; console.log(n); }; console.log(n); } f1(); //n=1 nAdd(); //n=2 nAdd(); //n=3 f1(); //n=1 nAdd(); //n=2
这里,返回的函数实际上将他和他的外部函数f1的活动对象(当然包括局部活动对象)都赋给全局变量result了,所以这些活动对象会一直保留在内存中。(由于闭包会携带包含他的函数的作用域,因此会比其他函数占用更多的内存)。
function f1() { var n=1; nAdd = function() { n++; console.log(n); }; return function () { console.log(n); } } var result=f1(); result();//n=1 nAdd(); //n=2 nAdd();//n=3 result(); //n=3 nAdd();//n=4
闭包的副作用
由于闭包中的变量值引用于作用域链中的变量对象,这通常是最后一次生成的固定的值。
function f1() { var result=new Array(); for(var i=0;i<10;i++){ result[i]=function () { return i; }; } return result; } var array=f1(); console.log(array[5]());//10
这里如果期望每次返回相应的索引值就出问题了,因为实际上每个函数返回的都是10,因为调用array[n]时闭包引用的是同一个变量对象,而变量对象中的i保存的是最后一次生成的值10。因此,我们应该为每个闭包创建特定的变量对象(即在闭包上一层创建特定的执行环境保存不被改变的索引值)。
function f1() { var result=new Array(); for(var i=0;i<10;i++){ result[i]=function (num) { return function () { return num; }; }(i); } return result; } var array=f1(); console.log(array[5]());//5
以下是一个典型的在开发中碰到的有关闭包的bug:
$(document).ready(function() { for (var i = 1; i < 4; i++) { $('<li>Print ' + i + '</li>') .click(function() { alert(i); }).appendTo('ul'); } });
这里点击每个li时弹出的都是4,原因跟之前一样,调用外层变量对象的i就是最后生成的那个值。解决办法也是创建特定的外层变量对象副本(jQuery中也可以用each代替for)。
$(document).ready(function() { for (var i = 1; i < 4; i++) { (function (num) { $('<li>Print ' + num + '</li>') .click(function() { alert(num); }).appendTo('ul'); })(i) } });
附加
有一道蛮经典的题目
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()()); //The Window
如果单纯以闭包的思维理解这个答案可能想不通,this.name检索到的第一个值应该是object.name。我认为这题主要考察的应该是this的含义。this表示唯一的含义是当前对象所属的执行环境,这是在函数调用时自动获取的。object.getNameFunc()()等价于this.name,而这个this.name所处的位置使他代表的是全局对象window。window.name="The Window"。
但如果我们将this保存在变量里,再通过传递这个变量就可以在其它地方访问这个执行环境了。
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ var that = this; return function(){ return that.name; }; } }; alert(object.getNameFunc()()); //My Object