1、闭包基础
1.1 作用域和作用域链
(1)作用域(Scope):作用域就是变量与函数的可访问范围。在JavaScript中,变量的作用域有全局作用域(全局变量)和局部作用(局部变量)域两种。
以下一个例子说明函数的作用域:
function outFun() {
var outName = "outName";
var outNum = "outNum";
function innerFun() {
var innerName = "innerName";
var innerNum = "innerNum";
alert(outName); // outName
alert(outNum); // outNum
alert(innerName); // innerName
alert(innerNum); // innerNum
}
innerFun();
alert(outName); // outName
alert(outNum); // outNum
alert(innerName); // undefined
alert(innerNum); // undefined
}
outFun();
说明:内部函数(子级函数)innerFun()可以访问外部函数(父级函数)outFun的变量outName、outNum,父级函数不能访问子级函数的变量innerName、innerNum。作用域关乎函数、对象、变量的可访问范围。
全局作用域(Global Scope)(全局变量):任何地方都能访问的变量或者对象具有全局作用域。例如:
1)父级函数和父级函数外面定义的变量拥有全局作用域
var a= 1;
function f1() {
var b= 2;
function f2() {
var c= 3;
return a+b+c;
}
alert(a); // 1
alert(b); // 2
alert(c); // 3
alert(f2()) ; // 6
}
f1();
说明:父级函数f1可以访问其内部定义的c变量和其外部定义的a、b变量,子级函数f2则可以访问所有变量。其中 a是全局变量,在web页面中全局变量属于 window 对象,全局变量可应用于页面上的所有脚本。所以a变量可以通过window.a获取,f1也可以通过window.f1()获取,一般情况下window对象可以省略不写。
2)变量声明如果不使用var关键字,那么它就是一个全局变量,即便它在函数内定义
function f1() {
var a = "a" ;
n = " n ";
function f2() {
alert(a)
}
return f2;
}
f1(); // "a"
alert(n); // "n" 外部可以直接访问
说明:变量n没有用var关键字定义,那么它就是个全局变量,在函数外可以引用。
3)所有window对象的属性拥有全局作用域
一般情况下,全局变量属于 window 对象,全局变量可应用于页面上的所有脚本。window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等。
局部作用域(Local Scope)(局部变量):在固定的代码片段内访问,一般在函数内部,也称为函数作用域。
例如,上面的第一个例子中,在函数f1内部定义的变量b,只能在函数内部使用,在函数外部或脚本代码是不可用的。
全局变量和局部变量即便名称相同,它们也是两个不同的变量。修改其中一个,不会影响另一个的值。
变量生命周期:全局变量的作用域是全局性的,即在整个JavaScript程序中,全局变量处处都在,不会自动在内存中清除。而函数内部生命的局部变量的作用域是局部性的,只有在函数内部起作用,当函数运行过程中对局部变量引用结束之后,局部变量就会在内存中清掉。
(2)作用域链(Scope Chain):创建函数的同时,它的作用域也被创建了。作用域中包含可访问的数据对象的集合,称为作用域链,它决定了哪些数据可以被函数访问。
function Add(num1, num2) {
var sum = num1 + num2;
return sum;
}
在函数Add创建时,它的作用域链中会填入一个全局对象,该全局对象包含了所有全局变量。函数Add的作用域在运行时用到:
var total = Add(5, 7);
执行此函数时会创建一个称为“运行期上下文( execution context )”的内部对象,它定义了函数执行时的环境,并被初始化为当前运行函数的[[Scope]]所包含的对象。
根据局部变量、命名参数、参数集合以及this等在函数中的出现顺序,它们被复制到“运行期上下文”的作用域链中,它们共同组成了一个新的对象,叫“活动对象( activation object )”:
在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,即从活动对象开始搜索,查找同名的标识符,若找到就使用这个标识符对应的变量,若没找到继续搜索作用域链中的下一个对象。如果搜索完所有对象都未找到,则认为该标识符未定义。函数执行过程中,每个标识符都要经历这样的搜索过程。
(3)作用域链和代码优化
从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。如上图所示,因为全局变量总是存在于运行期上下文作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码的时候应尽量少使用全局变量,尽可能使用局部变量。
如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量里再使用, 例如:
function changeColor() {
document.getElementById("button").onclick = function () {
document.getElementById("button").style.backgroundColor = "red";
}
}
两次全局变量document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。
function changeColor() {
var doc = document;
doc.getElementById("button").onclick = function () {
doc.getElementById("button").style.backgroundColor = "red";
}
}
这段代码比较简单,重写后不会显示出巨大的性能提升,但如果程序中有大量的全局变量被从反复访问,那么重写后的代码性能会有显著改善.
1.2 匿名函数
(1)匿名函数:没有给函数命名的函数。
(2)函数的定义有三种:
1)最常见的一种
function f1(x){
return x;
}
2)使用了Function构造函数,把参数列表和函数体都作为字符串。不建议使用:
var f1 = new Function() { 'x', 'return x;' } ;
3 ) 通过匿名函数赋值,不推荐通过匿名函数赋值
var f1 = function(x) { return x; }
(3)匿名函数的创建有两重方法:
1)没有函数名:
function(){};
2)通过两个括号实现:
( function (x, y) {
return x+y;
})(2, 3);
其中,第一个括号是对匿名函数的定义;第二个括号是对匿名函数的调用。
(4)匿名函数的作用
1)创建闭包,构建命名空间,以减少全局变量的使用
var allObj = { };
(function(){
var addEvent = function() {
function removeEvent() {
allObj.addEvent = addEvent ;
allObj.removeEvent= removeEvent ;
}
}
})( );
在这段代码中, 匿名函数中的函数对象addEvent和匿名函数对象removeEvent都是局部变量,但我们可以通过全局变量allObj使用它,这就大大减少了全局变量的使用,增强了网页的安全性。
2)通过两个括号的匿名函数赋值,比较实用
var addFun = (function(x, y){
return x+y;
})(2, 3);
创建了一个addFun函数,并通过匿名函数将其初始化为5。
3)递增效果,闭包能使函数的内部变量保存在内存中。
var outer = null ;
(function(){
var a = 1;
function inner() {
a +=1;
alert( a );
}
outer = inner;
});
outer(); //2
outer(); //3
outer(); //4
我们要想使用此段代码:oEvent.addEvent(document.getElementById('box') , 'click' , function(){});
1.3 this理解与应用
在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。以下列出this出现的集中场景:
(1)有对象就指向调用对象
var object = {
value: 123,
getValue: function(){
return this.value; // this指向object
}
}
object.getValue(); // 调用getValue的对象为object.
(2)没有调用对象就指向全局对象
var myObj = {
value: 123,
getValue: function() {
var foo = function(){
console.log( this.value ); // undefined 指向window
}
foo(); //没有调用对象
return this.value; // this指向object
}
}
console.log(myObj.getValue()); //123 调用getValue的对象为object.
(3)用new构造就指向新对象
js 中,我们通过 new 关键词来调用构造函数,此时 this 会绑定在该新对象上。
var SomeClass =function() {
this.value = 100;
}
var myCreate =new SomeClass();
console.log(myCreate.value); // 输出100
(4) 通过 apply 或 call 或 bind 来改变 this 的所指
// apply 和 call 调用以及 bind 绑定: 指向绑定的对象
// apply 方法接受两个参数:第一个是函数运行的作用域, 另外一个是一个参数数组(arguments)。
// call 方法第一个参数的意义与 apply() 方法相同, 只是其他的参数需要一个个列举出来。
// 简单来说, call 的方式更接近我们平时调用函数, 而 apply 需要我们传递 Array 形式的数组给它。 它们是可以互相转换的。
var myObject = { value: 100 };
var foo =function() {
console.log(this);
};
foo();// 全局变量 this指向window
foo.apply(myObject);// { value: 100 }
foo.call(myObject);// { value: 100 }
var newFoo = foo.bind(myObject);
newFoo();// { value: 100 }
1.4 闭包(closure)定义
(1)闭包:简而言之,闭包就是函数中的函数。
function f1(){
var a = 666;
function f2() {
alert(a);
}
}
(2)闭包特点:
1)子级f2()可以向上访问父级f1()的所有变量,而父级f1()不能向下访问子级f2()的变量,即外部不能访问内部变量,内部可以访问外部变量,这就是链式作用域。
2)子级函数可以引用父级函数的定义的变量,但该变量是最终的结果。
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
var lists = document.getElementsByTagName('li');
for(var i = 0 , len = lists.length ; i < len ; i++){
lists[ i ].onmouseover = function(){
alert(i);
};
}
说明:当鼠标移过每一个非自执行的匿名函数function(){ alert(i); })时,会首先在内部查找是否定义了i,结果是没有定义;因此它会进一步向上查找,查找结果是已经定义了,并且i的值是4(父级循环结束后的i值,因为匿名函数没有自执行);所以,最终每次弹出的都是4。
(3)匿名函数本身是个闭包,可以在函数外部对函数内部的变量进行操作。
2、闭包作用
(1)通过闭包,可以读取函数内部的变量,并且闭包能让函数的内部变量(局部变量)始终保存在内存中,即使是在父级函数关闭(运行结束)的情况下。
function f1(){
var n=666;
add = function( ) { n+=1 } ; //全局变量
function f2( ) {
alert (n);
}
return f2;
}
var result = f1( );
result( ); // 666 读取函数内部变量
add(); // 读取函数内部变量
result(); // 667 函数的内部变量始终保存在内存中
上面函数中,result实际上就是闭包f2函数,它一共运行了两次,第一次是666,第二次是667。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
3、闭包经典例子
例1:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return funtion(){
return this.name; // this指向window
}
}
alert(object.getNameFunc()()); // The Window
例2:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this; // this指向object
return function(){
return that.name; // that指向object
};
}
};
alert(object.getNameFunc()()); //My Object
4、闭包应用
4.1、循环与闭包
var k = document.getElementsByTagName("li");
for(var i = 0; i < k.length; i ++) {
(function(i){
k[i].onclick = function() {
alert(i);
}
})(i)
}
5、闭包缺点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
参考资料
JavaScript中的匿名函数及函数的闭包 http://www.cnblogs.com/rainman/archive/2009/05/04/1448899.html#m0
JavaScript开发进阶:理解JavaScript作用域和作用域链 http://www.cnblogs.com/lhb25/archive/2011/09/06/javascript-scope-chain.html
javascript中局部变量和全局变量的区别详解 http://www.jb51.net/article/61442.htm
hJavaScript中作用域、闭包与this指针 http://blog.csdn.net/junbo_song/article/details/52261247
hJavaScript闭包详情 http://www.cnblogs.com/gbin1/p/4092427.html
理解Javascript的闭包 http://coolshell.cn/articles/6731.html
学习javascript的闭包 http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
JavaScript闭包 http://www.runoob.com/js/js-function-closures.html
JavaScript内存泄露 http://www.cnblogs.com/rainman/archive/2009/03/07/1405624.html
在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。