深入理解闭包原理

闭包原理是什么?闭包应用场景是什么?


  从一年前刚入门前端,就知道闭包是javascript语言中较为抽象和难理解的一个知识点,自己也看相关的书籍和博客,但是可能因为基础比较差的原因,一直对闭包都是一知半解,没有完全摸透闭包的原理和闭包的应用场景,因此准备对闭包做一个长达一周的深入学习。主要参考2本书,js高级程序设计,你不知道的javascript,对闭包做一个深入理解和学习。
  学习闭包前,首先需要了解执行环境和作用域链,引用js高设的一段总结:内部环境可以通过作用域链访问外部环境,而外部环境不能通过作用域链访问内部环境。每个环境都可以沿着作用域链,向上查询变量和函数名。举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
var myPC = thinkPad;
function changePC(){
var herPC = MacAirpro;
function swapPC(){
var tempPC = myPC;
myPC = herPC;
herPC = myPC;//swap()执行环境,可以访问tempPC,swapPC(),herPC,chagnePC(),myPC和window。
}
swapPC();//changePC()执行环境,可以访问herPC,swapPC(),changePC(),myPC和window。
}
changePC();//全局执行环境,可以访问myPC,changePC和window。

  理解作用域链,是理解闭包的第一步,如果这一步不理解,需要认真研读js高设(第3版)的73,74页。
  上面我们理解了执行环境和作用域链,但是为了理解闭包,还有几个关键词需要理解,活动对象,变量对象。
什么是活动对象?
  当调用已经定义好的函数时,会创建一个由arguments和其他命名参数的值构成的活动对象,一般来说分为2种,一种是普通活动对象,一种是闭包活动对象。当闭包活动对象存在时,至少有一个普通活动对象在它的作用域链上,只有当闭包活动对象对应的函数被销毁或者叫接触引用时,才能将普通对象的内存释放。
  举1个例子:

1
2
3
4
5
6
7
8
9
10
function compare(value1,value2){
if(value1<value2){
return -1;
}else if(value1>value2){
return 1;
}else{
return 0;
}
}
var result = compare(1,9);

  当compare(1,9)被调用时,回立刻产生compare函数的执行环境及其作用域链,本地的活动对象会立刻产生一个包含1和9的arguments的数组,value1=1和value2=2,处于作用域链的第一位。
形象直观的表示出来就是。

再看1个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createComparisonFunction(propertyName){
return function(object1,object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1<value2){
return -1;
}else if(value1>value2){
return 1;
}else{
return 0;
}
}
};
var compare = createComparisonFunction("name");
var result = compare({name:"Frank"},{name:"Rose"});

  在这个例子中,有一句非常重要的话,createComparisonFunction()函数执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象,这就产生了闭包,因为正常情况下,当函数执行完毕,局部的活动对象会被销毁,在内存中仅保存全局的变量对象,若想释放内存,需要将新创建的函数设置为null。
下图形象展示了闭包的活动对象。

学习完活动对象,那么什么是变量对象呢?
  其实在上面2张图中就写明了变量对象是什么?目前我们知道的变量对象只有全局变量对象,即window对象下的对象。
但是细心的你会发现,这和我们平时在网上看到的哪些闭包题目并不一样,对,这就是我们后面要研究的内容:闭包与变量。
  闭包返回的是整个变量对象,而不是某个特殊的变量,而且闭包只能取得包含函数中任何变量的最后一个值。
  来看一个经典的闭包例子:

1
2
3
4
5
6
7
8
9
function createFunctions(){
var result = new Array();
for(var i=0;i<10;i++){
result[i]=function(){
return i;
};
}
return result;
}

这里,闭包直接赋值给数组。
再来看一个经典的立即执行函数解决闭包问题的例子:
1
2
3
4
5
6
7
8
9
10
11
function createFunctions(){
var result = new Array();
for(var i=0;i<10;i++){
result[i]=function(num){
return function(){
return num;
};
}(i);
}
return result;
}
这里,先定义匿名函数,然后将立即执行结果返回给数组。

但其实说实话,到这里我还不是很懂,因为js高设并没有将闭包与与变量的问题与之前的作用域链的配置机制联系在一起。其实这里,需要文章开头的那句:闭包是指,有权访问另一个函数作用域的变量的函数。
  在闭包问题例子中,这个函数指的是匿名函数function(){return i;};,它可以访问createFunctions()函数作用域中的变量i,但是访问时,由于每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以他们引用的都是同一个变量i。
  这句很关键!!!意思是,调用createFunctions()函数时,会产生执行环境,作用域链,以及createFunction()函数的活动对象,这个函数活动完之后,才轮的着匿名函数活动,而当匿名函数活动时,由于匿名函数的作用域链上有createFunction()函数的活动对象,因此它未被销毁,此时内存中的i已经成为了10,所以每次执行匿名函数,函数返回值都是i=10。
  所以说,闭包原理是:作用域链上的活动对象未被销毁,变量存在内存中,导致闭包函数调用活动对象函数中的变量时,变量值发生错误,这很像web缓存问题,开发过程中,为了刷新数据或者资源,需要清理缓存,而闭包类似一种不能被清理的缓存。
  暂时的闭包问题解决方案是,立即执行函数(IFE)。
后续的闭包问题和解觉方案继续探索。
  为什么必须有但凡出现闭包的地方都有return语句呢?引用js秘密花园的一段话:

闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为 函数 是 JavaScript 中唯一拥有自身作用域的>结构,因此闭包的创建依赖于函数。

  知乎上的答案众说纷纭,不如回去研读你不知道的javascript,来深入理解闭包的工作原理。至于闭包的应用场景,需要继续探索。

  你不知道的javascript一书中,一句话点名闭包的使用场景:

在定时器、事件监听器、Ajax请求、跨窗口通信、WebWorkers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。所谓回调函数,则是如果将访问它们各自函数作用域的函数当做第一级的值类型并到处传递,就会产生闭包。

  举个回调函数的例子:

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

  其中的function timer(){console.log(i)}作为setTimeout函数的第一个参数进行传递,因此产生闭包。借用js高设中的理解就是,全局变量对象中的i是5,而执行setTimeout()时,创建执行环境,创建作用域链,创建setTimeout()函数活动对象,arguments,timer()函数和i*1000作为活动对象内容,当1秒后执行代码时,timer()函数被调用,创建timer()函数活动对象,arguments是作为i作为活动对象内容,原型链的1位置是setTimeout活动对象,2位置是全局变量对象,全局变量对象中,保存了变量i的值,因此for循环执行结果是:每隔一秒,输出一个6。
  尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,实际上只有1个i。
所以上面代码的变形形式是,因为他们都有全局变量对象:

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

那么我们如何才能实现输出1,2,3,4,5呢?
需要立即执行函数,需要为循环中的每一个setTimeout()函数添加一个闭包作用域,并且将参数i传递进去。
所以变形和IIFE形式是:

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

此时会报错: Uncaught ReferenceError: j is not defined
  说明在timer函数中,没有j的定义,因此需要再setTimeout函数中去定义timer函数,来实现闭包作用域中的参数传递。
  到这里,我们意识到,闭包原来是一种将作用域封闭的手段,隔断与全局变量对象之间的联系,让函数不再沿着作用域链去找那个最顶层的变量,而是去关注局部的一个变量。闭包可能是因为没有块作用域而出现的问题,另外就是这很像冒泡事件。如果javascript有块作用域,那么闭包问题迎刃而解。
所以正确的形式是:

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

不出意外会正确输出1,2,3,4,5。
既然闭包问题这么影响工作效率,那么新版本JS语法有闭包吗?
  答案是,可以使用块作用域解决闭包问题。

1
2
3
4
5
for(let i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000)
}

不出意外会每隔一秒正确输出1,2,3,4,5。
  熟悉模块化的同学应该明白,现在越来越提倡模块化,各种第三方库,各种npm包,各种模块化工具,包括模块化语言,其实组件化也类似模块化,都有模块化的思想,vue的组件化思想,es6的模块机制,nodejs中的fs包,http,等等等等,都是模块。
那么模块的实现机制是什么呢?
说出来你不敢相信,那就是闭包。
  闭包将一个封闭函数中的内部函数包装在一个对象中返回,这样在声明一个新对象实例后,可以将方法直接按引用传递给新对象,新对象可以直接封闭函数中的内部函数进行操作,极大得提高了开发效率。
  举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function CoolModule(){
var something="cool";
var another = [1,2,3];
function doSomething(){
console.log(someting);
}
function doAnother(){
console.log(another.join("!"));
}
return {
doSomething:doSomething,
doAnother:doAnother
};
}
var foo=CoolModule();
foo.doSomething();//cool
foo.doAnother();//1!2!3!

这个模式在javascript中被称作模块,这是模块暴露的一种方法,是非常简单的一种。
  现在只有转变一个观念,闭包不是一个阻塞我们发展的概念,而是促进我们开发,促使我们写出好的代码的一个强有力的工具,因为模块就是在闭包的强大威力下产生的。
  对于模块化的学习,我将在近几天的博客中进行学习。

趁你还年轻 wechat
欢迎扫码关注我的微信公众号!