理解闭包,首先要知道作用域链是个什么东西。
1.什么是作用域链
在 JavaScript 的最顶层代码中(也就是不包含在任何函数定义内的代码),作用域链有一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。
当定义一个函数时,它实际上保存一个作用域链。
当调用这个函数时,它创建一个新的对象来储存它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。
对于嵌套函数来说,每次调用外部函数时,内部函数又会重新定义一次,因为每次调用外部函数的时候,作用域链都是不同的。
内部函数在每次定义的时候都有微妙的差别,即每次调用外部函数的时候,内部函数的代码都是相同的,而关联这段代码的作用域链却是不相同的
2.什么是闭包
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”——是指函数变量可以被隐藏于作用域之内,因此看起来是函数将变量“包裹”起来了
例1
var scope = 'global scope';
function checkscope() {
var scope = "local scope";
function f() {
console.log(scope);
}
return f();
}
checkscope();
执行函数之后打印
checkscope()函数声明了一个局部变量,并定义一个函数 f(), 函数 f() 返回了这个变量的值,最后将函数 f() 的执行结果返回。这个结果是显而易见的返回函数内局部变量,然后我们改动一下函数再看看。。。
var scope = 'global scope';
function checkscope() {
var scope = "local scope";
function f() {
console.log(scope);
}
return f;
}
checkscope()();
上面例子也可以改为这样
var scope = 'global scope';
function checkscope() {
var scope = "local scope";
function f() {
console.log(scope);
}
return f;
}
var f = checkscope();
f();
结果并没有改变,这是什么原因呢?
会看一下作用域的基本规则:javascript 函数执行用到了作用域链,这个作用域链是函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域里,其中的变量scope一定是局部变量,不管在何时何地执行函数f(),这种绑定在执行f()时依然有效。简言之,闭包的这个特性就是:它们可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义的外部函数。
3.利用闭包
例2
function counter() {
var n = 0;
return {
count: function() {
return n++;
},
reset: function() {
n = 0;
}
};
}
var c = counter(), d = counter();
console.log(c.count());
console.log(d.count());
c.reset();
console.log(c.count());
console.log(d.count());
每次调用couter()都会创建一个新的作用域和一个新的私有变量。因此,变量c重置了n=0,所以第二次c,d执行count()的结果不一样。
例3
function counter(n) {
return {
get count() {return n++;},
set count(m) {
if(m>=n) {
n = m;
}else {
throw Error("count can only be set to a larger value")
}
}
}
}
var c = counter(1000);
console.log(c.count);
console.log(c.count);
console.log(c.count = 2000);
console.log(c.count);
console.log(c.count = 2000);
这个版本的counter()函数并未申明局部变量,而只是使用参数n来保存私有状态,属性存储器方法可以访问n,这样的话,调用counter()的函数就可以指定私有变量的初始值了。
例4(利用闭包实现的私有属性存储器方法)
function addPrivateProperty(o, name, predicate) {
var value;
o["get" + name] = function() { return value; };
o["set" + name] = function(v) {
if(predicate && !predicate(v)){
throw Error("set" + name + ": invalid value "+ v);
}else {
value = v;
}
}
}
var o = {};
addPrivateProperty(o, "Name", function(x) { return typeof x == "string"});
o.setName("Frank");
console.log(o.getName());
o.setName(o); //将抛出异常
这里除了调用o对象的setName()方法,且只能传入字符串对变量value进行赋值,同样只能通过o对象的geiName()方法才能访问到变量value的值。
4.闭包的危害
例5
function constfunc(v) {return function() {return v;};}
var funcs = [];
for(var i = 0 ; i< 10; i++) {
funcs[i] = constfunc(i);
}
console.log(funcs[5]());
function constfuncs() {
var funcs = [];
for(var i = 0; i < 10; i++){
funcs[i] = function() {
return i;
}
}
return funcs;
}
var funcs2 = constfuncs();
console.log(funcs2[5]());
第一个函数显然使我们需要的结果,可以第二次为什么会调用结果显示为10呢?其实数组funcs2储存的所有函数执行结果都是10。
第二个函数中创建了是个闭包,并将它们存储在一个数组中,这些闭包都是在同一个函数调用中定义的,因此它们可以共享变量i。当constfuncs()返回时,变量i的值是10,所有的闭包都共享这一个值,因此数组中的函数的返回值都是同一个值。
故而我们需要像第一个函数那样,不是直接将变量i返回而是通过传入参数的方法,实现变量私有化的方法来实现想要的结果。