JavaScript开发和面试之闭包

什么是闭包

要理解闭包需要先理解变量作用域

作用域

明确几点:

  • JavaScript的变量作用域是基于其特有的作用域链的
  • JavaScript没有块级作用域
  • 函数中声明的变量在整个函数中都有定义

按照作用域区分,变量有全局变量和局部变量,由于作用域链,函数内部是可以直接读取全局变量的,而函数外部无法直接读取函数内的局部变量。

闭包定义

「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。

本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁

闭包的用途

  • 间接访问变量
  • 另一个就是让这些变量的值始终保持在内存中

来看一个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function fn(){
// default
var n=66;
// get
function getN(){
return n;
}
// set
function setN(num){
n=num;
}
// add
function addN(){
n++;
}
return {
getN:getN,
setN:setN,
addN:addN
}
}

var test=fn();

console.log(test.getN()); //66
test.setN(666);
console.log(test.getN()); // 666
test.addN();
console.log(test.getN()); // 667

我们通过fn暴露出来的接口访问到了函数内部的n,同时我们对n的值做了修改,可以看到n依然是存在于内存中的。

具体应用方面:

  • 块级作用域,防止变量名污染
  • 封装私有变量

闭包的注意事项

1)由于闭包会使得函数中的变量都被保存在内存中,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

闭包题目收集

让我们理论结合实(mian)践(shi)来理解以下闭包
以下皆为chrome浏览器环境运行的结果

第一题

1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";

var object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name;
};
}
};
console.log(object.getNameFunc()()); // The Window

拆成两步就很容易看懂

  • 第一步:object.getNameFunc()等价于function(){ return this.name; }
  • 第二步:(function(){ return this.name; })(),此时调用的是一个匿名行数,this指向的是window。

第二题

1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
var that = this;
return function() {
return that.name;
};
}
};
console.log(object.getNameFunc()()); // My Object

我们同样分两步

  • 第一步:object.getNameFunc()===function(){ return that.name }。
  • 第二步:(function(){ return that.name })(),此时的that在匿名函数里面并没有申明,所以它会向上级作用域寻找that,也就是getNameFunc里面的thatthat=this,此时的this指向调用getNameFunc的对象object

第三题

1
2
3
4
5
6
7
8
9
10
11
function foo(x) {
var tmp = 3;
function bar(y) {
console.log(x + y + (++tmp));
}
bar(10);
}
foo(2); //16
foo(2); //16
foo(2); //16
foo(2); //16

这里只是函数调用哦,不是闭包哦

1
2
3
4
5
6
7
8
9
10
11
function foo(x) {
var tmp = 3;
return function (y) {
console.log(x + y + (++tmp));
}
}
var bar = foo(2);
bar(10); //16
bar(10); //17
bar(10); //18
bar(10); //19

当你return的是内部function时,就是一个闭包。

一个函数访问了它的外部变量,那么它就是一个闭包

第四题

1
2
3
4
5
6
7
8
9
10
11
12
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0); // undefined
a.fun(1); // 0
a.fun(2); // 0
a.fun(3); // 0

这题有点绕,我们来分解一下:

  • a = fun(0)等价于执行后,console.log(o)没有在当前以及父级作用域中寻找到o,所以输出的是undefined,同时a被赋值为{fun:function(m){return fun(m,0)}}
  • a.fun(1)执行,简化后为(function(1){return fun(1,0)})()===fun(1,0)当前作用域里面没有fun,最后找到了顶级的function fun(n,o),执行console.log(0)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}

var b = fun(0).fun(1).fun(2).fun(3);
// undefined
// 0
// 1
// 2

这链式看的我好慌,我们继续分解一下

  • fun(0)会输出o,此时没有o所以为undefind,同时fun(0)表达式执行结果为{fun:function(m){return fun(m,0)}}(先忽略掉后面的fun(1)、fun(2)、fun(3))。
  • 再执行fun(1)等价于执行(function(1){return fun(1,0)})(),继续输出o,此时o为0,表达式简化为{fun:function(m){ return fun(m,1)}}
  • 同理继续执行,依次输出1,2

参考资料