刚开始学习JS时,对闭包的概念模糊不清,看了阮一峰的博客《学习javascript闭包》,稍微理解了一些。在做项目的过程中,可能已经用到了闭包,但自己却浑然不知。为了方便自己多次反复地查看和理解,有必要将它们记录和整理。
(一)变量作用域
1.全局(global)变量和局部变量
全局变量的作用域是全局的,在代码的任何地方都是有定义的。
函数的参数和局部变量只在函数体内有定义,作用域是局部的。
var num = 1; //声明一个全局变量
function func() {
var num = 2; //声明一个局部变量
return num;
}
console.log(func()); //输出:2
2. 块级作用域和函数作用域
块级作用域:像C、Java等编程语言,变量在变量声明的代码段之外是不可见的,我们通常称为块级作用域。
函数作用域:变量在声明它们的函数体以及这个函数体嵌套的任意函数体都是有定义的。
在JavaScript中使用的是函数作用域,没有块级作用域。
function func() {
console.log(num); //输出:undefined,而非报错,因为变量num在整个函数体内都是有定义的
var num = 1; //声明num 在整个函数体func内都有定义
console.log(num); //输出:1
}
func();
(二)闭包
1. 概念:能够访问另一个函数作用域中的变量的函数。简单理解为“定义在一个函数内部的函数”。
闭包也可以认为是函数的嵌套,内部函数可以使用外部函数中的所有变量,即使外部函数已经执行完毕(JavaScript作用域链)。
function f1(){
var n = 10;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 10
注意:当一个函数 f1() 返回了另一个函数 f2() 后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。返回的函数并没有立刻执行,而是直到调用了result() 才执行。
2. 用途:使用闭包代替全局变量,加强了封装性;
函数外或在其他函数中访问某一函数内部的参数;
在dom操作时,为节点循环绑定click事件,在事件函数中使用当次循环的值或节点,而不是最后一次循环的值或节点;
3. 造成问题:会增大内存使用量,容易造成内存泄露。严重时会使浏览器崩溃
4. 立即执行的匿名函数来创建闭包
匿名函数最大的用途是创建闭包(这是JavaScript语言的特性之一),并且还可以构建命名空间,以减少全局变量的使用。
var oEvent = {};
(function(){
var addEvent = function(){ };
function removeEvent(){ }
oEvent.addEvent = addEvent;
oEvent.removeEvent = removeEvent;
})();
5. 闭包用于封装
利用闭包的特性能让我们封装一些复杂的函数逻辑,在这个例子中调用export上的方法(getUserId,getTypeId)间接访问函数里私有变量,但是直接调用export._userId是没法拿到_userId的。
(function() {
var _userId = 23492;
var _typeId = 'item';
var export = {};
function converter(userId) {
return +userId;
}
export.getUserId = function() {
return converter(_userId);
}
export.getTypeId = function() {
return _typeId;
}
window.export = export; //通过此方式输出
}());
export.getUserId(); // 23492
export.getTypeId(); // item
export._userId; // undefined
export._typeId; // undefined
export.converter; // undefined
注意:闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
6. 闭包常见错误
eg. 1.
document.body.innerHTML = "<div id=div1>aaa</div>" + "<div id=div2>bbb</div><div id=div3>ccc</div>";
for (var i = 1; i < 4; i++) {
document.getElementById('div' + i).
addEventListener('click', function() {
alert(i); // all are 4!
});
}
上面代码在页面加载后就会执行,当i的值为4的时候,判断条件不成立,for循环执行完毕,但是因为每个div的onclick方法这时候为内部函数,所以i被闭包引用,内存不能被销毁,i的值会一直保持4,直到程序改变它或者所有的onclick函数销毁(主动把函数赋为null或者页面卸载)时才会被回收。这样每次我们点击div的时候,onclick函数会查找i的值(作用域链是引用方式),一查等于4,然后就alert给我们了。
要达到我们想要的点击aaa输出1,点击bbb输出2,点击ccc输出3,要用到闭包的技巧,在每次循环的时候,用立即执行的匿名函数把它包装起来,这样子做的话,每次alert(i)的值就取自闭包环境中的i,这个i来自每次循环的赋值i。
document.body.innerHTML = "<div id=div1>aaa</div>" + "<div id=div2>bbb</div>" + "<div id=div3>ccc</div>";
for (var i = 1; i < 4; i++) {
!function(i){ //②再用这个参数i,到getElementById()中引用
document.getElementById('div' + i).
addEventListener('click', function() {
alert(i); // 1,2,3
});
}(i); //①把遍历的1,2,3的值传到匿名函数里面
}
eg. 2
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 16
f2(); // 16
f3(); // 16
原因就在于返回的函数引用了变量 i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量 i 已经变成了4,因此最终结果为16。返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量,方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
7. 经典题
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //The Window
分析:Object.getNameFunc()()分两次执行:
var tmp = Object.getNameFunc(); //此时没有执行this.name
var name = tmp();//这个时候才执行,这时候的this上下文为全局,即:window
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //My Object
分析:Object.getNameFunc()()分两次执行:
var tmp = Object.getNameFunc(); //这个时候执行了that = this,这里的this上下文是object,所以that指的是object
var name = Object.getNameFunc(); //这个时候执行了that.name