详细理解JS闭包


最近参加面试,错过一个福利很好的公司,很是心痛。我发现很多大型公司都对校招生的项目要求不高,框架基本都没问,但是要求基础扎实,其中就有对闭包的考察,最近仔细钻研了一下,对闭包也有了一些理解,就在这总结一下。

关于变量

要了解闭包就要先了解js中的变量。
js中有全局变量和局部变量。其中可以在任意位置访问和修改的就是全局变量。在函数中用var声明的只能在函数内访问和修改的就是局部变量(let和var在函数中声明的变量作用是一样的,这里就不单列出来了)。

注意

  1. 局部变量必须用var关键字来声明,否则会自动添加到全局对象的属性上去,成为全局变量。
  2. 全局变量是一直存在的,你用或者不用,他就在那里。而局部变量只有在调用函数的时候才会存在,函数调用过后变量就会销毁,下一次使用函数的时候会重新创建。

关于函数

  1. 函数是可以进行多级嵌套的。
  2. 函数里面的子函数可以访问使用它上级定义的变量,不止是上一级,而是像冒泡一样逐层往上查找,找到了就会停止查找,而如果找到全局也没有找到就会报错。
  3. 函数里调用函数外变量不能用var,否则就是声明同名局部变量,对函数外变量不会有影响。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var x = 1;
    function a() {
    var x = 2;
    function b() {
    console.log(x);
    }
    b();
    }
    a();
    console.log(x);

    结果会先输出2,再输出1。这是因为在函数a里用var重新声明了一个局部变量x并赋值为2,此时在函数a中的变量x和全局变量x是没有关系的。所以调用函数a输出的是局部变量,结果为2,而最下面则输出的是全局变量,结果为1.

  4. 函数自调:函数除了上面用a()这样调用外,还可以自调,可以用()把函数包裹起来,后面跟上()执行这个函数。为什么包裹起来呢?因为不包裹那么就是对函数的声明,包裹起来就是一个表达式了,js会直接执行表达式,所以还可以在函数前加”~”、”+”、”-“等符号,函数后跟()让函数自调,但这些符号有很多局限,所以不是很了解的话还是少用。

  5. JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。
    其实这句话就是前面第三条,但是很重要,特别提出来,闭包的实现主要也是因为这个原因。

  6. JavaScript中有回收机制,函数被引用执行完成后不再被引用,这个函数的作用域就会被回收销毁,如果两个对象互相引用,而不再被第三者所引用,那么这两个互相引用的对象也会被回收。但是两个对象互相引用,且在被第三者引用,那么就不会被回收。闭包的作用就是让这个函数的作用域不会被回收销毁。

什么是闭包?

闭包的解释:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
这是说的什么鬼,字都认识,可是合成一句话就一脸懵逼了,还是举个栗子吧:

1
2
3
4
5
6
7
8
9
10
11
function a(){
var i = 0;
function b(){
i ++;
console.log(i);
}
return b;
}
var c = a();
c(); //=> 1
c(); //=> 2

这段代码会输出1和2,我们会发现在这段代码中有两个特点:

1. 函数b嵌套在函数a中;
2. 函数a返回函数b;

这就是一个最简单的闭包。
在执行完var c = a()之后,c其实是指向b的函数,那么c();就相当于在函数a外执行了a里的函数b。函数b被c引用,而c是全局变量,并不会被回收,而b是在a里面的函数,所以最后函数a的作用域就会被保存在内存中而不会被销毁。
所以如果把上面的代码修改一下,让全局变量c不引用b,就会释放a的作用域,闭包也就释放了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function a(){
var i = 0;
function b(){
i ++;
console.log(i);
}
return b;
}
var c = a();
c(); //=> 1
c(); //=> 2
c = a();
c(); //=> 1
c(); //=> 2

上面代码在会输出两个1、2、1、2,而不是1、2、3、4,原因就是在第一个闭包形成输出1、2之后,将闭包释放了,然后又形成了一个新的闭包,调用又输出1和2。

还有一个更为常见的闭包写法。

1
2
3
4
5
6
7
8
9
10
var a = (function() {
var i = 0;
function b() {
i++;
console.log(i);
}
return b;
})();
a();
a();

匿名函数自执行把函数b赋给了a,这个a就是return回去的函数b;执行a();就是执行内函数b,这样就形成了一个闭包。

闭包的作用

闭包是非常重要的一个概念,利用闭包很多的好处:

保护函数内的变量安全,加强了封装性

以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。以第一个闭包来说,函数a中的i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。

在内存中维持一个变量,使逻辑连续(用的太多就变成了缺点,占内存)

依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。

实现JS私有属性和私有方法

通过保护函数内变量的安全,可以利用面向对象的方法创建函数的私有属性和私有方法,且不能被Constructor外访问到。

闭包的缺点

闭包有一个非常严重的问题,那就是内存浪费问题,这个内存浪费不仅仅因为它常驻内存,更重要的是,对闭包的使用不当会造成无效内存的产生。看下面的例子:

1
2
3
4
5
6
7
8
for (var i = 1; i <= 10000; i++) {
function a(i) {
setTimeout(function() {
console.log(i)
}, 0)
};
a(i);
}

这个例子会创建10000个闭包,存在内存中不会释放,会造成内存的极大浪费,所以闭包要慎用。

闭包的应用

解决for循环后执行输出全为循环后值的问题。

面试题,结果是什么?

1
2
3
4
5
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, 0);
}

这个就是我之前遇到过的一个面试题,答案当然很简单,是3个4,不了解的童鞋可以去看一下定时器的运行机制。
那么怎么能让它输出1,2,3呢?
这个当然有很多的方法,我们这里用闭包来做。

1
2
3
4
5
6
7
8
for (var i = 1; i <= 3; i++) {
function a(i) {
setTimeout(function() {
console.log(i)
}, 0)
};
a(i);
}

上面这个函数就是使用闭包实现了输出1,2,3;在这里a就是外包函数,setTimeout的回调函数就是内函数,而setTimeout就是在外部对内函数的调用。所以这里其实有3个闭包,每个闭包函数被调用了一次,然后就释放掉了。
这个函数还可以简化一下:

1
2
3
4
5
6
7
for (var i = 1; i <= 3; i++) {
+ function a(i) {
setTimeout(function() {
console.log(i)
}, 0)
}(i);
}

这是用了函数自调来实现的闭包,同样有3个闭包,那么怎么使用1个闭包来实现呢?

1
2
3
4
5
6
7
8
+ function() {
var a = 1;
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(a++);
}, 0);
}
}();

这里就是用了1个闭包实现了输出1,2,3;这里的a是内部变量,然后for循环和setTimeout是对内函数的3次外部调用。

利用闭包模拟对象的私有属性

JavaScript以前缺少块级作用域,没有private修饰符,但是具有函数作用域。作用域的好处是内部函数可以访问它们的外部函数的参数和变量(除了this和argument。内部中的函数中的this指向全局对象,argument指向内部函数的函数参数)。我们可以利用这种属性来模拟面向对象中的私有属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test(aname) {
var aname = aname || 'No Input';
return {
get name() {
return aname;
},
set name(newNmae) {
aname = newNmae;
}
}
}
var fun = test();
console.log(fun.name); //=>No Input
fun.name = "mervyn";
console.log(fun.name); //=>mervyn

在这里利用闭包和访问器属性模拟出了对象的私有属性的读取与修改。

利用闭包模拟对象的私有方法

模拟私有方法和模拟私有属相差不多,不过返回的直接是函数而不是访问器而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test(aname) {
var aname = aname || 'No Input';
return {
getName: function() {
return aname;
},
setName: function(newName) {
aname = newName;
}
}
}
var fun = test();
console.log(fun.getName()); //=>No Input
fun.setName('mervyn');
console.log(fun.getName()); //=>mervyn

在这里利用闭包直接返回多个函数模拟出了对象的私有方法,可以再外部直接调用,像调用对象方法一样使用。

更多闭包的应用请参考这里;

总结

  1. 闭包是一种非常强大的设计原则,在js中学会使用闭包是非常重要的,使用闭包可以大大简化用户的调用,达到目的。
  2. 不需要知道闭包的原理和细节,只要知道闭包的基本使用就可以了。
  3. 尽量少学习。
文章目錄
  1. 1. 关于变量
  2. 2. 关于函数
  3. 3. 什么是闭包?
  4. 4. 闭包的作用
    1. 4.1. 保护函数内的变量安全,加强了封装性
    2. 4.2. 在内存中维持一个变量,使逻辑连续(用的太多就变成了缺点,占内存)
    3. 4.3. 实现JS私有属性和私有方法
  5. 5. 闭包的缺点
  6. 6. 闭包的应用
    1. 6.1. 解决for循环后执行输出全为循环后值的问题。
    2. 6.2. 利用闭包模拟对象的私有属性
    3. 6.3. 利用闭包模拟对象的私有方法
  7. 7. 总结
|