0%

我是怎么搞懂闭包的?

先从一个简单的闭包开始

想从函数外部访问到函数内部的变量,当然是不可以访问的,但是你如果在这个函数里面返回一个方法用来对这个函数里面的变量进行操作的话,就可以像是在外面访问量函数内的变量(其实我暂时是认为没有访问函数里面变量的,实质上只是调用了对相应参数进行操作的方法),举个栗子把

1
2
3
4
function test(){
var i = 0;
}
console.log(++i);
这当然会报错
然后如果想执行这样的操作呢
闭包是这么解决的
我在方法里面返回一个这样的操作就好了
1
2
3
4
5
6
7
8
9
function test(){
var i = 0;
return function(){
console.log(++i);
}
}
var doAdd = test();
doAdd();
doAdd();

所以我认为就是返回一个函数来供调用自加

问题

但是吧,我说这是一个闭包,你肯定会认为是的,如果要你自己写一个闭包出来,那就写不出了,肯定会遇到很多问题,所以接下来我就讲讲我学习闭包的过程吧。

先从闭包开始

先是在犀牛书上看闭包,然后他就要我先去了解,作用域和作用域链。

作用域

在没有es6的let之前js的作用域只是函数作用域,每个函数有自己的作用域,然后其他的就是全局作用域了。

作用域链

在全局中每定义一个变量,这些变量都会存在于window下,也可以通过最外层的this调用到

立即执行函数表达式

在我手写闭包的时候发现这样编写可以实现累加

1
2
3
4
5
6
var test2 = (function () {
var a2 = 0;
return function () {return ++a2;}
})()
console.log(test2())
console.log(test2())

这么写的话每次都是一个新的累加

1
2
3
4
5
6
var tests = function () {
var a = 0;
return function () {return ++a;}
}
console.log((tests())())
console.log((tests())())
于是我就开始思考是不是立即执行函数的原因
于是马上学习了一波立即执行函数(IIFE)
其实立即执行函数就是在定义函数的时刻,马上去把这个函数执行了

一个简单的栗子

1
(function () { dosomething; })();

如果这个转换成正常的函数是

1
2
var a = function () { dosomething; }
a();

所以我们上面的闭包的代码转换成正常的函数是

1
2
3
4
5
6
7
var test2 = function () {
var a2 = 0;
return function () {return ++a2;}
}
var test = test2()
console.log(test())
console.log(test())
如果这样的话就可以实现累加
和我们上面的代码一比较就发现
console.log((tests())())
他在(tests())的时候定义了一个空间然后执行了(tests())()方法
但是第二次的时候又分配了一个新的空间
然后在新的空间里面累加,当然不会记录在一起
然而我们立即执行函数的写法是创建了一个test的空间
然后其他的操作都是在这个闭包的空间里面执行
的(也就是在test2返回的那个函数的空间里面)
1
2
3
4
5
6
7
8
9
10
11
12
13
function test () {
var i = 0;
return function () {
document.write(++i)
document.write('<br>')
}
}
var test1 = test();
test1();
test1();
var test2 = test();
test2();
test2();

test1和test2是两个不同的闭包空间

再次对闭包的理解

看了廖雪峰大佬的博客突然对闭包有了另外一种理解

闭包就像java里面类的私有变量一样
你在外面通过实例化了也不能操作那些私有变量
你只能通过get方法和set方法等类似的方法来操作这个值
而且你实际上并不是操作了这个值
你知道调用了这个对象里面的方法
然后这个对象里面的方法来操作这个值

上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function student () {
var name = 'xxx';
var getName = function () {
return name
}
var setName = function (newName) {
name = newName;
}
return {
getName: getName,
setName: setName,
}
}
var studentA = student();
console.log(studentA.getName())//xxx
studentA.setName("aaa");
console.log(studentA.getName())//aaa

然后出现新问题

问题1

我跟以为大佬分享后她跟我提出

如果在return那里加个name:name 然后studenta.name结果是啥

我发现居然还是之前的xxx我改成aaa了并没有变化
后来发现在get和set里面加了个this后再调用name的时候就可以看到变化了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function student () {
var name = 'xxx';
var getName = function () {
return this.name
}
var setName = function (newName) {
this.name = newName;
}
return {
getName: getName,
setName: setName,
name: name
}
}
var studentA = student();
console.log(studentA.getName())//xxx
studentA.setName("aaa");
console.log(studentA.getName())//aaa
console.log(studentA.name);//aaa

开始对闭包有一个非常大的误解,一直以为我修改这个闭包的变量的值就会直接修改前面的方法里面的变量(直接修改student这个方法里面的name),其实不是的,当你创建一个闭包后(var studentA = student())然后内存中就会对应于你创建的闭包分配一个空间(然后直接修改是修改studentA闭包里面的name)(不信的话你可以修改了name后再新建一个studentB发现还是最开始的值)。
然后我又有一段时间把闭包分配的空间和我return的这个对象视为一个空间了,其实不然,返回的对象是用studentA接受的一个空间,而闭包的空间是对应studentA生成的空间

对应上面为什么用this的话就可以name的值,用之前的setName改变的值只能通过getName查看。因为不用this和用this修改的地方不同,你可以在setName中试着输出this,因为this在studentA中,所以这里的this指向的是return的对象。所以加了this是修改return对象,没加this是修改闭包的原始值。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function student () {
let name = 'xxx';
let getName = function () {
return name;
};
let getName2 = function () {
return this.name;
};
let setName = function (newName) {
name = newName;
};
let setName2 = function (newName) {
this.name = newName;
};

return {
getName: getName,
getName2: getName2,
setName: setName,
setName2: setName2,
name2: name //这里我写成Rname为了后面setName2会看到新建了一个属性
};
}

let studentA = student(); //在这里你分配了两个空间,一个是return的空间还有一个是studentA对应的闭包的空间

console.log(studentA.name2); //xxx (在return中的name2的值是xxx)
console.log(studentA.getName()); //xxx (在闭包空间中name的值是xxx)
console.log(studentA.getName2()); //undefined (因为你在return的对象里面没有name这个属性,所以就根本找不到)

studentA.setName('aaa'); //修改了闭包原始值
console.log(studentA.name2); //xxx (在return中的name2的值是xxx)
console.log(studentA.getName()); //aaa (在闭包空间中name的值是aaa)
console.log(studentA.getName2()); //undefined (因为你在return的对象里面没有name这个属性,所以就根本找不到)

studentA.setName2('bbb'); //修改了return对象,但是发现没有name这个属性于是创建了一个name赋值为bbb
console.log(studentA.name2); //xxx (在return中的name2的值是xxx)
console.log(studentA.getName()); //aaa (在闭包空间中name的值是aaa)
console.log(studentA.getName2()); //undefined (在return中的name的值是bbb)

let studentB = student(); //两个新的空间一个studentB的对象,一个studentB对应的闭包

console.log(studentB.name2); //xxx (在return中的name2的值是xxx)
console.log(studentB.getName()); //xxx (在闭包空间中name的值是xxx)
console.log(studentB.getName2()); //undefined (在return中没有name)

问题2

还有一个问题是一个很经典的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i;
});
}
return arr;
}
var results = count();
console.log(results[0]());//4
console.log(results[1]());//4
console.log(results[2]());//4

按道理是输出1,2,3呀
然后大佬的解释是

返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

这是优化后的代码,加了一个立即执行函数后让i等于1的时候立即把push执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n;
}
})(i));
}
return arr;
}
var results = count();
console.log(results[0]());//1
console.log(results[1]());//2
console.log(results[2]());//3

但是为什么是这样的呢

是因为在一个方法或者是变量的定义到执行会经历
1.进入编译环境
2.创建变量i
3.把i先初始化为undefined
4.执行代码给i赋值
所以在push里面的方法,在循环阶段的时候只进过了创建、初始化、赋值的操作,并没有执行,所以里面的i还是i没有变成真正的值。只有在后面执行的时候才会去找这个i。
所以只需要改成立即执行函数就可以在定义的时候同时执行一波,然后这个i就已经赋值了,后面就不会改变了。

然后最后附上一个总结(来自这里

  1. let 的「创建」过程被提升了,但是初始化没有提升。
  2. var 的「创建」和「初始化」都被提升了。
  3. function 的「创建」「初始化」和「赋值」都被提升了。
  4. const 和 let 只有一个区别,那就是 const 只有「创建」和「初始化」,没有「赋值」过程。都被提升了。