JavaScript的闭包问题
JS的闭包真的是一个老生常谈的知识点了,无奈它并不是那么好掌握,但是它又是那么重要,很多高级应用的开发都会用到闭包去解决相关问题。希望你能从这篇文章了解基本的闭包知识点,后期会不定期更新使之更加完善。
在了解闭包知识以前,先说下函数作用域问题。在一些类似C语言的编程语言中,它们是具有块级作用域(block scope)的,但是在JavaScript中却没有块级作用域,取而代之的是函数作用域(function scope):“变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的”。(引用自《JavaScript权威指南》)
例如如下代码:
function test(o) {
var i = 0; // i在整个函数体中都是有定义
if(typeof o == "object") {
var j = 0; // j在函数体中是有定义的,不仅仅是在这段代码内
for(var k = 0; k < 10; k++) { // k在函数体内是有定义的,不仅仅是在循环内
console.log(k); // 输出数字0-9
}
console.log(k); // k已经定义了,输出数字10
}
console.log(j); // j已经定义了,但是可能没有初始化
}
上面这段代码中,如果我们传入不同的参数,j的打印结果是不一样的。比如:
var o = {name: "qin", old: 23};
test(o);
当传入参数为object时,会执行if判断语句中的代码块,这个时候j的值打印出来为0;
var o = "qin";
test(o);
当传入参数不为object的时候,就不会执行if判断语句中的代码块,这个时候j的值打印为undefined。这个时候的变量j就属于定义了未初始化。
JavaScript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。而且JavaScript函数里声明的所有变量都会被提前至函数顶部,这叫做声明提前(hoisting)。
在JavaScript中每一个全局代码或函数所包含的代码块中,都有一个与之关联的作用域链(scope chain),只有弄懂这个作用域链才能更好的去理解闭包问题。
作用域链是一个对象列表或者链表,当执行JavaScript代码需要查找变量y的值的时候(这个过程叫做“变量解析”),它会从链表中的第一个对象开始查找,如果这个对象中有一个名为y的属性,则会直接使用这个值,如果没有,就会继续查找下一个对象,以此类推。当整个链表的对象中都没有y这个属性的话,就会抛出一个引用异常(ReferenceError)的错误。
简单理解就是:子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
那么这个作用域链表对象创建规则是怎样的呢?大致分三种情况:
1.整个代码不包含任何函数:
这时的作用域链对象是由一个全局对象组成。比如:
var n = "qin";
if(typeof n == "string") {
for(var i = 0; i < 5; i++) {
console.log(i);
}
}
console.log(i);
上面这段代码中的作用域链为{n: "qin", i: 5}。
2.整个代码包含函数,但不包括嵌套函数:
这时的作用域链有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。比如:
var x = "qin";
function test() {
var k = 3;
for(var i = 0; i < 3; i++) {
k += 1;
}
console.log(k); // 打印结果为6
}
test();
console.log(x); // 打印结果为qin
console.log(i); // 会抛出ReferenceError错误
上面这段代码中存在两个作用域链,第一个是函数局部变量的作用域链{i: 3, k: 6, x: "qin"},第二个是全局对象的作用域链{x: "qin"}。当我们在函数外部打印i的值的时候,JavaScript会去全局对象的作用域链查找属性为i的值,但是全局对象的作用域链并不存在i这个属性,因此就会抛出引用异常错误(ReferenceError)。
3.代码中存在函数,且有嵌套函数:
这时的作用域链至少有三个对象(因嵌套函数的数量增加而增加),第一个是全局对象的作用域链,第二个是最外层函数的参数和局部变量的作用域链,第三个是嵌套函数的参数和局部变量的作用域链。比如:
var x = 0;
function test() {
var y = 2;
x += 1;
function foo() {
var a = 3;
console.log(y); // 结果为2
}
foo();
console.log(a); // 会抛出ReferenceError错误
}
test();
console.log(x); // 结果为1
上面这段代码中,函数test中包含一个嵌套函数foo,这段代码含三个作用域链,第一个是全局对象作用域链{x: 1},第二个是函数test的作用域链{y: 2, x: 1},第三个是嵌套函数foo的作用域链{a: 3,y: 2, x: 1}。
当我们定义一个函数的时候,其实它就已经保存了一个作用域链。当我们调用这个函数,它会创建新的对象来存储它的局部变量,并将这个对象添加到保存的那个作用域链上,同时还会创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来说,每次调用外部函数的时候,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都会有微妙的差别——在每次调用外部函数时,内函数的代码都是相同的,而且关联这段代码的作用域链也不相同。(引用自《JavaScript权威指南》)
关于变量作用域及函数作用域可参考这篇文章:什么是变量作用域和函数作用域?(坑未填)
说完函数作用域和作用域链,接下来就要开始理解什么是闭包了。
首先在上面包含嵌套函数的例子中,我们如何在外层函数test中访问到嵌套函数foo中的变量a的值呢?
其实很简单,我们只需要把foo中a变量返回就可以了,如下:
var x = 0;
function test() {
var y = 2;
x += 1;
function foo() {
var a = 3;
console.log(y); // 结果为2
return a;
}
var res = foo();
console.log(res);// 结果为3
}
test();
console.log(x); // 结果为1
其实上面的代码就是典型的闭包,闭包函数为foo。
个人理解闭包的概念就是:
有权访问另一个函数作用域内变量的函数就是闭包。
本质上闭包就是将函数内部与外部联系起来的一座桥梁。
闭包的用途:
闭包的最大用处有两个:
第一是上面所说的可以读取到函数内部的变量。
第二则是让这些变量值能一直保存在内存中。
来看一个比较有趣的例子:
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
上面的例子中,我们定义了函数makeAdder,它接受一个参数x,并返回一个新的函数。它返回的函数使用一个参数y,并返回x+y的值。
add5和add10都是两个闭包函数,我们为他们传入不同的参数x,一个为5,一个为10。实际上这两个函数有着各自独立的作用域链,并不相互影响。
使用闭包应该注意的点:
1.由于闭包会使得函数中的变量都会被保存在内存中,内存消耗很大,因此不能滥用闭包,否则会导致网页性能问题,在IE中还可能造成内存泄漏。解决办法是在退出函数之前,将不使用的局部变量全部删除。
2.闭包会在父函数外部改变父函数内部的值,所以你在把父函数当做对象使用,把闭包当做它的公用方法,把内部变量当做它的私有属性的时候,一定要小心不要随便修改父函数内部变量的值。
最后放两个思考题:
思考题一:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); // 打印出什么?
思考题二:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); // 打印出什么?
参考文献及资料:
《JavaScript权威指南》
阮一峰博客《学习JavaScript闭包(closure)》
如果你在本文中发现错误或者有异议的地方,可以在评论留言,谢谢!