让你能看懂的 JavaScript 闭包
没有废话,直入主题,先看一段代码:
var counter = (function() {
var x = 1;
return function() {
return x++;
};
}());
counter(); // => 1
counter(); // => 2
上面的代码,每执行一次 counter()
,返回自增的计数。
这段代码用了匿名函数表达式,格式为 (function() {}())
,括号内的匿名函数会自动执行,并以返回值作为表达式的值。上面的匿名函数,返回嵌套的函数:function() { return x++; }
。关于匿名函数,这里不赘述。上面的匿名函数,也可以采用传参的形式:
var counter = (function(x) {
return function() {
return x++;
}
}(1));
下面来分析这段代码:
匿名函数返回嵌套的函数。在这个嵌套的函数中,有变量 x
,在 x
所在的作用域(JavaScript 以 function
块为作用域,即“词法作用域”)中找不到这个变量,所以会向上级作用域查找,直到找到为止。这段代码在匿名函数所在的作用域中找到了变量 x
后,返回嵌套的函数。如果变量 x
不再被使用,就会被运行环境回收,以节约资源,但在上面的代码中,x
却被嵌套的函数“记住”了,所以不会被运行环境回收,并且,这个变量不能被直接访问,只能通过定义这个变量所在的作用域内的代码操作。 这种 用函数作用域包裹并“记住”住变量 的技术,就是“闭包”。
闭包的作用域
先简单说下 JavaScript 的作用域。JavaScript 用的是词法作用域,以 function(){}
块为词法,一个块就是一个作用域,最外层为全局作用域。ES6 标准支持了块级作用域,但这不是这篇文章的重点。JavaScript 首先在当前作用域查找变量,如果找不到,就向上一级作用域查找,一直到全局作用域,直到查到为止。
现在,我们把闭包“打开”,把变量定义到全局作用域:
var x = 1;
var counter = function() {
return x++;
};
counter(); // => 1
counter(); // => 2
上面的代码,当访问 counter()
时,也返回一个自增的计数。与闭包函数不同的是,x
被定义到了全局作用域。当访问函数时,当前作用域找不到 x
,代码向一级作用域(这儿为全局作用域)查找,找到后返回函数。这儿的 x
定义在全局作用域中,可以被直接访问,而在闭包函数中,x
定义在匿名函数的作用域中,不能被直接访问。
把变量放到全局作用域,这样做会污染全局变量。JavaScript 没有包的概念,大家都是在全局作用域上操作,污染全局变量很可能导致引用的库冲突。上面的代码就定义了两个全局变量,而闭包的写法只需定义一个。最少的定义全局变量,是写 JavaScript 的原则。
我们把这段代码修改下,把计数的变量定义在函数的属性,就能做到少污染全局变量:
var counter = function() {
return counter.x++;
};
counter.x = 1;
但上面的代码和把变量放到全局作用域,都有一个局限,变量可以被直接访问甚至是修改。
用闭包函数把变量封闭起来,只让定义变量的作用域内的代码操作变量
闭包可以减少全局变量污染,并且让变量只可以被定义变量的作用域内的代码操作。这两个问题,可以被闭包一举解决。
下面,我们把上面的计数器改为可重置的:
var counter = (function() {
var x = 1;
return {
inc: function() {
return x++;
},
reset: function() {
x = 1;
},
};
}());
counter.inc(); // => 1;
counter.inc(); // => 2;
counter.inc(); // => 3;
counter.reset();
counter.inc(); // => 1;
inc
和 reset
两个方法操作的都是定义在匿名函数的作用域中的变量 x
,所以 x
可以被递增和重置。
上面的例子中,操作闭包变量的代码所都在闭包函数的作用域内。
再看一个常见的陷阱,这个陷阱的本质就是把操作闭包变量的代码放到了闭包函数的作用域外:
var fs = (function() {
var functions = [];
for (var i = 0; i < 10; i++) {
functions[i] = function() {
return i;
};
}
return functions;
}());
fs[0](); // => 10
fs[1](); // => 10
fs[2](); // => 10
上面的代码中, i
定义在匿名函数所在的作用域,随着循环,i
不断地被修改,当循环结束时,i = 10
,所以会有上面的结果。
下面,我再演示下怎么把代码改成返回 1
, 2
, 3
...
要返回 1
, 2
, 3
...的话,就需要在匿名函数和嵌套的函数之间,增加一层作用域,在这层作用域中,定义一个变量 _i = i
,再让嵌套的函数返回 _i
即可。这样,当代码执行到中间层的作用域时,_i
被赋值,并且在嵌套的函数作用域中保存下来。代码如下:
var fs = (function() {
var functions = [];
for (var i = 0; i < 10; i++) {
functions[i] = (function() {
var _i = i;
return function() {
return _i;
};
}());
}
return functions;
}());
fs[0](); // => 0
fs[1](); // => 1
fs[2](); // => 2
用 ES6 的 let
语法也可以实现。let
赋值会让变量产生块级作用域:
var fs = (function() {
var functions = [];
for (let i = 0; i < 10; i++) {
functions[i] = function() {
return i;
};
}
return functions;
}());
fs[0](); // => 0
fs[1](); // => 1
fs[2](); // => 2
在上面的代码中,let
包裹在 for
循环控制语句里,相当于 for
循环中添加了一层作用域,本质上和前一种方法是一样的。
闭包函数是非闭包函数相比:一、减少了全局变量污染;二、让变量不能被随意修改。
最后,再分亨一个函数,这个函数可以用于给 dom
生成 ID
。这个函数的本质就是一个闭包的增量,不过带了一个前缀。
var generateID = (function(prefix, x) {
return function() {
return prefix + x++;
};
}("id-", 1));
generateID(); // => "id-1";
generateID(); // => "id-2";
generateID(); // => "id-3";
generateID(); // => "id-4";