许多书上闭包过于复杂讲解难懂,自己理解了一下并总结啦~
讲闭包之前,需要先明白以下几个概念。
总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
1、执行上下文(execution context)
每创建一个函数同时就会创建一个执行环境,也就是执行上下文。全局执行上下文就是global环境,一个函数内部的当前执行环境就是当前执行上下文。
执行上下文定义了变量或函数有权访问其他数据,决定了他们各自的行为 。
2、执行上下文堆栈
活动的执行上下文在逻辑上组成一个堆栈。堆栈底部永远都是全局上下文(globalContext),而顶部就是当前(活动的)执行上下文。
当一个函数被创建且被调用时,在函数内部,当前的函数执行上下文被压入栈,若内部还有函数,则继续压入栈顶。栈底部永远是全局执行上下文。
3、变量对象
每个执行上下文中有会有与之关联的变量对象,在上下文中定义的所有变量和函数都会放在这里面。如果在函数中,我们称之为活动对象。
可以说变量对象是与执行上下文相关的数据作用域(scope of data) 。它是与执行上下文关联的特殊对象,用于存储被定义在执行上下文中的变量(variables)、函数声明(function declarations) 。
4、作用域链
作用域链是用来指向变量对象的,作用域的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域的最前端,始终都是当前执行的代码所在环境的变量对象。
子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
var f1 = function() {
function f2() {
function f3() {}
}
}
//f3中 作用域链就是 f3活动对象 -> f2活动对象 -> f1活动对象 -> global活动对象
可以开始讲闭包啦
定义:闭包是指有权访问另一个函数作用域中的变量的函数。
是不是有点过于抽象了?举个例子
var outter = function() {
var a = 1;
function inner() {
return a;
}
return inner; //返回里面这个函数
}
var result = outter(); //外部得到了返回的里面那个函数
console.log(result()); //1
由JavaScript的作用域链特性可知,在函数里面可以访问到外部的变量,但反过来是不行的。但上面这个例子做到了,这就是闭包。
既然inner可以读取outter中的局部变量,那么只要把inner作为返回值,我们不就可以在outter外部读取它的内部变量了吗!
上一节代码中的inner函数,就是闭包。
各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。
由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
使用闭包时常出现的一些问题
1、闭包只能取得包含函数中任何变量的最后一个值
好像有点抽象。不要忘记,闭包所保存的是整个变量对象,而不是某个特殊的变量。其实这里可以把它想象成类似于一个引用。
function createArray() {
var result = new Array();
for(var i = 0;i<10;i++) {
result[i] = function() {
return i;
};
}
return result;
}
var r = createArray();
console.log(r);
for(var i=0;i<10;++i)
console.log(r[i]()); //10个10
原因在于闭包保存的是整个变量对象,因此每个函数中都保存一样的变量对象。它们引用的都是同一个变量i。所以在执行闭包时,i已经执行到10,返回的i自然便都是10。
2、关于this对象
我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数作为某个对象的方法调用时,this等于那个对象。
不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window
var name = "The Window";
var object = {
name: "My object",
getNameFunc: function() {
return function() {
return this.name;
};
}
}
console.log(object.getNameFunc()()); //The Window
为什么最后的结果是"The Window"而不是object里面的name"My object"呢?
首先,要理解函数作为函数调用和函数作为方法调用。
我们把最后的一句拆成两个步骤执行:
var first = object.getNameFunc();
var second = first
其中
- 第一步,获得的first为返回的匿名函数,此时的getNameFunc()作为object的方法调用,如果在getNameFunc()中使用this,此时的this指向的是object对象。
- 第二步,调用first函数,可以很清楚的发现,此时调用first函数,first函数没有在对象中调用,因此是作为函数调用的,是在全局作用域下,因此first函数中的this指向的是window。
为什么匿名函数没有取得其包含作用域(外部作用域)的this对象呢?
每个函数被调用时,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
那么,怎么获得外部作用变量中的this呢?
把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。
var name = "The Window";
var object = {
name: "My object",
getNameFunc: function() {
var name = 19;
var that = this;
return function() {
return that.name;
};
}
}
console.log(object.getNameFunc()()); //My object
此时调用,getNameFunc执行时的活动变量有哪些?name that function。在执行匿名函数时,同时引用了getNameFunc()中的活动对象,因此可以获取that和age的值。但是由于是在全局环境中调用的匿名函数,因此匿名函数内部的this还是指向window。
闭包用途总结
1、可以读取函数内部的变量,建立函数内部与外部的桥梁;
2、让一些变量的值始终保持在内存中。
使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
《JavaScript高等程序设计第3版》