JavaScript 闭包
闭包之前复习一下之前的知识
作用域,一个函数产生了作用域,作用域就是函数的一个属性叫[[scope]],在函数里要访问一个变量基本上都要上[[scope]]属性里去找。
[[scope]]里面存储着一切能被访问的数据或变量。
执行器上下文叫AO,是预编译产生的对象,每次执行一个函数都会产生一个AO,多次执行一个函数会产生多个独一无二的AO。
一个函数能产生一个东西叫作用域,并且这个作用域就是函数的一个属性叫 [[scope]] 属性,
在函数里面如果想访问一个变量,基本上都会到 [[scope]] 里面去找,[[scope]]里面存储着一切能被访问的数据或变量。
当函数执行时,会创建一个称为 执行期上下文 的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。
查找变量:从作用域链的顶端依次向下查找。
执行期上下文叫AO,
AO是预编译环节产生的一个必然的对象,
然后多次执行函数,会产生多个独一无二的AO,就是每次执行函数都会产生一个AO。
一、触发闭包
闭包
当内部函数被保存到外部时,将会生成闭包。闭包会导致原有作用域链不释放,造成内存泄露。
1、闭包在什么样的情况下会触发?
两个函数互相嵌套(或多个函数互相嵌套,一般关注两个的),把里面的函数保存到外部全局上,这样的情况必然会生成闭包。然后里面的函数在外面执行的时候,一定能够调用的他原来在的那个函数环境里面的变量。
看下面的例子,从一开始执行到执行完发生了什么?
function a(){ function b(){ var bbb = 234; console.log(aaa); } var aaa = 123; return b; } var glob = 100; var demo = a(); demo();
第一图
首先函数 a 的定义就有一个 a.[[scope]] 属性了,然后 a() 函数的执行产生一个执行期上下文放到作用域链的最顶端,形成自己的作用域(上面一个AO下面一个GO链到一起了)。
a.[[scope]] 的 scope chain 指向两个空间,第0位指向AO、第1位指向GO
第二图
由于 a() 函数的执行产生了函数 b 的定义,函数 b 的定义拿的是函数 a() 执行的劳动成果,并且函数 b 定义的 [[scope]] 和a函数执行时候的 [[scope]] 是一样的,他们指向的是同样的房间。
由于 b 函数是被定义的状态,他等待被执行,还没生成自己的AO。
第三图
直到 a 函数执行完了 b 函数也没有被执行,但 a 函数执行完的状态是把 b 函数的引用 return b 到全局(全局里面的东西永远不会销毁,除非html文件执行完),并保存到的变量 demo 上后,a函数才彻底执行完了。
a函数执行完后把自己执行期上下文AO销毁( 把指向AO的房间的线砍断,a函数回归到被定义状态,所以指向GO的线不能剪断 )。但是非常尴尬的是在a函数销毁之前,b函数被保存出来了,保存在外部被变量demo存起来了。
b函数在全局范围保持着被定义的状态,被定义状态拿着a函数执行时的劳动成果。虽然a函数执行完后把线砍断了,但是b函数作用域链的线还指向着这个房间。
第四图
demo()执行就相当于b()函数执行,b()函数在外部被执行的时候,访问的是a函数里面的变量aaa。
b函数定义在a函数里面,执行却在全局,会发生什么?
b()函数在外面执行的时候生成一个新的执行期上下文,链到自己作用域链的最顶端,查找顺序是 0 - 1 - 2(b.AO -> a.AO -> GO)。
demo()执行访问变量aaa,b函数自己的执行期上下文里面没有,没有就忽略,从第0位沿着作用域链往下找,第1位有变量aaa等于123,所以就把b函数作用域里面存的变量aaa打印出来,结果是123。
这个过程代码在视觉上,函数b被保存到外部,好像不能访问a函数里面的变量aaa。但从结果上是可以访问的,这个过程就叫做闭包。
简单的说,但凡是内部的函数被保存到外部,就一定会生成闭包的。
2、如何不用return,也把函数保存到外边
直接操作全局变量demo,把函数赋给全局变量,不用return也行。也就是说里面的函数不管用什么样的方法,只要让它保存到外部都能产生闭包。
var demo; // 全局变量demo function test(){ var abc = 100; function a (){ console.log(abc); } demo = a; } test(); demo();
里面的函数不管用什么方法,只要保存到外部,他都能产生闭包。
3、一个闭包的小应用
第一次打印什么,第二次打印什么?
function a(){ var num = 100; function b(){ num ++; console.log(num); } return b; } var demo = a(); // 在这执行的时候函数a已经被销毁了 demo(); // 第一次打印101 demo(); // 第二次打印102
b扔给demo,函数是引用值赋值就是把地址赋给变量dome,变量domo 和 函数b是同一个人的两个不同名字。
b函数的定义在全局范围永远不会消失,不执行就是定义的状态。b函数被定义的状态拿着a函数完整的作用域链,每次b被执行的时候都在自己的执行期上下文上面形成一个新的AO,执行完后把自己形成的新AO扔掉,等待下一次被执行,这样来回新建、扔掉的是b函数自己产生的AO,a函数形成的AO在这个环节始终就是这一个。
所以b函数在外部被执行的时候,一直保存着a函数执行时产生的AO既不销毁也不不新增,就在这个基础上操作。
所以第一次dome()执行的时候,加加的变量num是a函数AO里面的,第一次加加变成101,然后b函数执行完了把自己的AO扔掉,下一次demo()在执行再生成一个新的b函数的AO放的作用域链的顶端,加加的还是a函数执行产生的AO里面的变量num,在101的基础上再加加变成102。
4、内存泄漏
闭包会导致原有作用域链该释放时不释放,导致占用内存空间,这个过程叫做内存泄漏。
什么是内存泄漏?
正常来说是内存被占用,占用和泄露是什么关系呢?
比如,手里捧起一把沙子,沙子肯定会从手里面流失,手里剩的沙子变的越来越少了,
内存被占用和手里剩下的沙子是一个道理,内存被占用的越多剩的内存越少,剩的少就解释成像内存泄漏一样,只是说像泄漏了一样,他是反向理解的。泄漏的多了剩的就少了,换句话说占用的多了剩的也少了,这是内存泄漏的意思。
内存泄漏是一个计算机名词,把过多的占用内存当成一种内存被泄漏,泄露的多剩的就少。把过多的占用系统资源叫做内存泄漏,这是比喻的一个形式。
函数执行完销毁的机制就是为了省空间,由于闭包导致作用域链不能被释放,造成系统的空间过多的被占用,这是闭包产生的一个不好的效果。
闭包有不好的一点会导致内存泄漏,但也不能说完全都不好。在后期编程用高级应用的时候,应用闭包做了很多积极的事,包括系统模块化开发,它提供了很多帮助。
二、闭包的作用
实现公有变量
eg: 函数累加器
可以做缓存(存储结构)
eg: eater
可以实现封装,属性私有化。
eg: Person();
模块化开发,防止污染全局变量
作用一:实现公有变量
利用闭包实现一个不依赖外部变量,并且能反复执行的函数累加器,执行一次函数可以从0加到1,再执行函数从1加到2……每次执行都代表一次累加
如果不用闭包做这个功能,需要用一个全局变量count
var count = 0; function test(){ count ++; console.log(count); } test(); test(); test();
准确的说这不叫独立的累加器,因为依赖了一个外部的变量count的配合,一个独立的功能不能依赖于这个模块外的东西。
利用闭包的机制实现一个不依赖于外部的全局变量实现函数累加器。
function add(){ var count = 0; function demo () { count ++; console.log(count); } return demo; } var counter = add(); counter(); counter(); counter();
counter变量可以无限的累加,调用一次counter()就会在原有基础上加一次,虽然可以不这样做,但这样做会更加模块化。
模块化开发的根本要义就是用闭包来做的,只不过是立即执行函数里面套函数来做。好处是不互相污染变量,即使变量名一样也不冲突。
作用二:可以做缓存(存储结构)
缓存的意思是暂时当做一个存储结构,说做缓存说的有点牵强,只不过效果可以硬说成做缓存。这个存储结构是外部不可见的,但是它确实有存储结构。
第1步
在函数 test 里面定义 a、b 两个函数,再定义一个变量 num = 100,然后函数 a 加加、函数 b 减减
function test() { var num = 100; function a() { num++; console.log(num); } function b() { num--; console.log(num); } }
第2步
把函数a 和 函数b放数组里面同时保存出来,在外部定义一个数组myArr接收,数组的第0位 myArr[0]() 是函数a,数组的第1位 myArr[1]() 是函数b,打印结果分别是多少?
函数 a 和 test 形成的闭包 和 函数 b 也和 test 形成的闭包,共用的是一个 test 形成的 AO
function test() { var num = 100; function a() { num++; console.log(num); } function b() { num--; console.log(num); } return [a, b]; } var myArr = test(); myArr[0](); //101 myArr[1](); //100 // a doing a.[[scope]] --> 0 : a.AO // 1 : test.AO * // 2 : GO * // b doing b.[[scope]] --> 0 : b.AO // 1 : test.AO * // 2 : GO * // a执行,b执行 // *号的意思,是同一个AO,同一个GO
上面只是做一个铺垫,下面真正做一个缓存,
对象里可以有属性、方法。对象也是一种函数只不过定义方法不一样,方法就是函数的另外一种表现形式。
对象 obj 里面两个方法(eat方法,push方法),都是操作food变量的,
eat 方法修改完变量food,push方法能接着修改变量food,这两个方法修改的是同一个food,food相当于一个隐式的存储结构。
把obj对象保存到外部,就相当于把两个函数保存到外部了,两个函数跟变量food也形成闭包了,两个函数操作的是同一个变量food。
function eater(){ var food = ""; // food相当于隐式的存储结构 var obj = { eat : function(){ console.log('I am eating ' + food); food = ""; }, push : function (myFood) { food = myFood; } } return obj; } var Li = eater(); Li.push('banana'); Li.eat();
eat方法被保存出来了形成闭包保存了 test 函数的劳动成果,push方法被保存出来也要和test形成闭包保存test函数的劳动成果,
两个方法都保存了同一个 test 函数的劳动成果,修改的是同一个test作用域里面的变量food,eat方法修改了test空间里的food变量,push方法也保存着test空间的food变量。
多个函数同时被保存到了外部和一个函数形成闭包,这些闭包所保存出来的是同一个作用域,这个域内里面的变量大家可以共用,
就像在全局的范围内定义一个变量,第一条语句修改了这个变量,第二条语句在访问这个变量是被修改过的,
这样的结构有点像缓存,什么是缓存?一个看不到的储存空间,这次访问的是上次修改过的。
作用三:可以实现封装,属性私有化。要到后面对象以后讲
作用四:模块化开发,防止污染全局变量。后面讲
三、立即执行函数
定义:此类函数没有声明,在一次执行过后即释放。适合做初始化工作。
1、立即执行函数的定义
函数定义有两种方式,一种是函数声明、一种是函数表达式,还有一种定义函数的方式叫立即执行函数
立即执行函数
1. 形式:立即执行函数没有名字,不能调用也调用不了,立即执行函数只执行一次
2. 特点:是定义完立即执行函数,读到这会立即就执行,执行完之后立即销毁函数本身
(function (){ console.log('立即执行函数'); }());
2、立即执行函数有什么作用?
如果在全局范围内定义两个函数(函数a、函数b)
function a(){ } function b(){ }
两个函数虽然只用一次不会用第二次,但是函数没有消失,但凡在全局定义的函数,他永远不用消失,永远是等待被执行的状态,就会占用内存空间,程序多了占用内存越多影响效率,会一直占用空间。
有些函数从定义到最后被执行完,只执行了一次,比如用写一个求100阶乘的函数,把100阶乘的结果计算出来后,这个函数就不再用了。把计算完的"数"保存出来之后,以后就只用这个数了。
这种函数会占用内存空间,这种只执行一次处理完数据,返回结果的函数叫初始的函数。只执行一次的初始化功能的函数,我们用执行完就立即销毁的"立即执行函数"。
3、立即执行函数的形式
1. (); 首先写一个括号
2. ( function(){} ); 在括号里写一个匿名函数,这个函数不用写名字
3. ( function(){} () ); 在匿名函数的最后再加一对小括号。最后面这个小括号是执行符合,在执行符号里面传实参
立即执行函数执行完后就销毁了,函数里面的代码会被执行打印果是 375
(function(){ var a = 123; var b = 234; console.log(a + b); }());
立即执行函数除了执行完就释放以外,就和普通函数基本没有区别,同样有执行期上下文,内部也经历预编译,也可以有参数。
(function(a, b ,c){ // 形参a, b, c console.log(a + b + c * 2); }(1, 2, 3)); // 这对括号叫执行符号,往执行符号里传实参 1, 2, 3
4、立即执行函数的返回值
立即执行函数执行完就销毁了,即便是这样也依然有返回值(函数没有返回值要他什么用),前面定义一个num变量接收
var num = (function(a, b ,c){ return a + b + c * 2; }(1, 2, 3)); console.log(num);
一般立即执行函数除非是初始化页面用的,比如改个背景颜色不需要返回值,只要初始数据的都要这么写接收返回值。
5、官方给出里两种写法
立即执行函数的不是javascript语法定义的,是后来发现可以这么用的
(function(){}()); // 第一种 W3C建议使用第一种 (function(){})(); // 第二种
6、深入探究立即执行函数
下面标准的说法叫函数的声明
function test(){ var a = 123; }
一切定义的函数都叫函数定义,
函数定义里面分两种,一种叫函数声明,还有一种叫函数表达式,记住函数声明和函数表达式是两个东西,虽然他们都能定义函数。
在函数声明后面加一个括号,函数能执行吗?
function test(){ var a = 123; }();
先回答一个问题:
单独写一个 test 代表一个函数引用,函数引用就代表这个函数
function test(){ var a = 123; } console.log(test);
函数引用就代表这个函数(打印函数名test)
正常执行函数,就是在函数引用后面加一对小括号 text() 就执行了
function test(){ var a = 123; } test(); // 正常函数执行
现在函数声明 和 函数引用一摸一样,在函数引用后面加一个小括号,也应该也差不多啊,能执行吗?
function test(){ var a = 123; }();
不能执行系统报错,低端的语法解析错误 Uncaught SyntaxError: Unexpected token 执行前扫描一遍就报错了,错的不能在错了
为什么这样 test() 就可以执行呢?
记住重点 ,只有表达式才能够被执行符号执行。
什么是表达式?
单写一个 123 也叫表达式,只要是式子哪怕只有一个数他也叫表达式,表达式定义的就这么松散,
要记什么是表达式,不如记什么不是表达式,但凡名都不叫表达式的他就不叫表达式。
test() test不是被执行了吗?
test;
123;
234;
test 就和 123、234 这么单独写一样,虽然看着不像表达式,但依然叫表达式,叫数字表达式。
123 + 234; 这也叫表达式,数字单独矗着就已经足够叫表达式了,所以单独的一个函数引用 test 能够被执行。
为什么下面不能被执行?因为他的名字就不叫表达式,他叫函数声明,所以他不能被执行,一定记住只有表达是才能被执行
function test(){ var a = 123; }();
之前学过的函数表达式,后面加一对括号能被执行吗?他都叫表达式了,那必须能被执行
var test = function(){ console.log(123); }(); console.log(test); // ƒ (){console.log(123);}
PS: 执行符号就是一对括号
接下来一个问题,
能被执行符号执行的表达式,这个函数的名字就会被忽略,比如现在 test 还代表一个函数,因为 test 是函数的名字
var test = function(){ console.log(123); }; console.log(test); // test
当在函数表达式后面,加一对括号的时候,函数被立即执行了,或者说被马上执行了,执行结果是123
var test = function(){ console.log(123); }(); console.log(test); // undefined
再次引用test的时候,test不再代表一个函数了
一旦一个表达式被执行符号执行后,就失去了对原来函数的索引,就会自动放弃函数的名称。
这种写法就和立即执行函数没什么太大区别,函数被执行一次后,函数就被永久的放弃了
var test = function(){ console.log(123); }();
函数表达式不加执行符号,代表定义函数,加了执行符号就变成一个立即执行的表达式,是另一种新形式的立即执行函数,和立即执行函数的原理是一样的,执行完就销毁就剩下一个空的变量test什么都没有。
隐式类型转换的一系列符号,正号,符号,非……,致力于把后面的东西转化成数字,
在函数声明前面加一个一元运算符的正号,正号就会致力于连同后面的东西变成表达式了。加上正号自此就变成表达式了,既然是表达式了就能被执行,在后面加一对括号。
+ function test(){ console.log('既然是表达式了就能被执行'); }();
一个表达式被执行了之后,他就不在是定义函数的语句,就忽略了函数的名字 或者 函数的引用,再打印test系统报错
+ function test(){ console.log(123); }(); console.log(test);// Uncaught ReferenceError: test is not defined
这样和立即执行函数差不多了,这就是立即执行函数了。
前面写负号也可以
- function test(){ console.log(123); }(); console.log(test); // Uncaught ReferenceError: test is not defined
还可以用叹号
! function test(){ console.log('叹号也行'); }();
乘号、除号是不行的,因为上面不是加号、减号是正负号,
与运算符 或 或运算符也都是可以的,但运算符前面要加东西能运行到后面才行
与运算符
1 && function test(){ console.log('与运算符'); }();
或运算符
0 || function test(){ console.log('或运算符'); }();
最典型的还是正号、负号和叹号,这些都可以把函数声明变成一个可执行的表达式。
7、立即执行函数的形成
括号是执行符号也是 数学运算符
下面先算括号内的加减再算乘除,括号括起来的都叫表达式
(1 + 2) * 3;
括号是计算符号和正号,负号,非差不多,把函数声明function test(){}放括号里面包起来了,括号把里面的函数就变成表达式了
(function test(){});
函数定义变表达式了,后面加运算就能被执行了
(function test(){ console.log(123); })()
再打印函数名test时系统就报错了,既然没有名字用就把名字去掉,这样和第二种立即执行函数就一样了。这是一个演化发育的过程。
(function (){ console.log(123); })();
再看第一种立即执行函数是怎么形成的
下面先执行最外层的括号,再执行里面的,最外面括号的优先级最高
(1 - 2 * (2 + 3));
把立即执行函数后面的小括号放到括号里面,外面的括号叫数学符号,里面的括号叫执行符号,这是两个东西要区分开,数学符号的优先级高
( function (){ console.log(123); }() );
最外面的括号优先级高先执行,就会先把里面东西的变成表达式,变成表达式之后再被里面的括号执行,所以这样写也是可以执行,原理是一样的
(function (){ console.log(123); }());
立即执行函数执行完立即被释放,也就是说函数的引用不被保存,
如果问只有上面这样是立即执行函数对不对?不对,有的是方法可以是立即执行函数,只要是表达式他就能被执行。
8、看一道阿里巴巴的一道面试题,是基于立即执行函数考的
首先这道题系统不会报错,理论上肯定不能执行
function test(a, b, c, d){ console.log(a + b + c + d); }(1, 2, 3, 4);
如果这样写把括号内的数字去掉系统就报错了,因为括号会被认为是执行符合,函数定义是不能被执行的。
function test(a, b, c, d){ console.log(a + b + c + d); }();
为什么这么写系统就不会当做执行符号
function test(a, b, c, d){ console.log(a + b + c + d); }(1, 2, 3, 4);
系统会这样识别,一个函数定义,一个是逗号运算符,不会把括号当做运算符。
function test(a, b, c, d){ console.log(a + b + c + d); } // 系统给分开看,上面是函数定义,下面是逗号运输符,所以系统不报错也不执行。 (1, 2, 3, 4);
补充记录一下
等号前面的 var test 叫变量声明,等号后面叫表达式
var test = function (){ console.log(123); }();
1. 先变量声明,2. 在把函数赋到 test 里面去,这是两个步骤,
把这个函数赋到 test 里面的过程叫做表达式,所以在执行的时候会放弃这个函数,而储存引用到 test 里面
四、示例、用闭包解决闭包
1. for 循环十次,每次循环给数组的一个单元加上一个函数值
2. return arr 数组保存到全局,数组里面的十个函数都和 test 形成了十对一的闭包
理想效果是实现,执行数组
第一位 myArr[0]() 的函数执行打印 0
第二位 myArr[1]() 的函数执行打印 1
第三位myArr[2](); 的函数执行打印 2
一直到数组最后一位的函数打印 9
function test(){ var arr = []; for(var i = 0; i < 10; i++){ arr[i] = function(){ document.write(i + '<br/>'); // 这句定义时候不会执行,i取到的值要到函数执行的时候才知道 } } return arr; } var myArr = test(); // console.log(myArr); // 打印数组里确实有十个函数(10) [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ] for(var j = 0; j < 10; j++){ myArr[j](); } /*** function test(){ var arr = []; for(var i = 0; i < 10; i ++){ arr[i] = function(){ document.write(i + "<br/>"); } // 把函数体或函数引用赋值到arr数组的当前位,arr[i]数组当前位是要立即要索取到的,这是执行语句。 // 后面的函数体没有执行,没有执行获取不到变量i的值。 // 没有执行就是函数引用,系统不读里面的代码,在执行的时候系统才会读里面的语句。 // att[i]前面的i立马变现,因为这att[i]是执行语句, // 前面后面是有区别的 } return arr; } ***/
理想的效果是打印 0至 9,实际效果打印的是十个数字10,为什么会出现这样的效果?
因为数组里面的十个函数同时被保存到了外部,这十个函数都和 test 形成闭包。
也就是这十个函数都保存了同一个test的劳动成果,test劳动成果里面包含一个变量 i,在这十个函数在被保存出去之前 i 已变成了10,i变成了10之后这十个函数才被保存出去,
这十个函数不是在定义的时候执行,是被保存到外部执行,所以访问的 i 打印出来是10。
如何用闭包解决闭包?
这个闭包的特点是一对十,十个函数共用一个作用域,所以打印变量i 的结果都是10。一对十的意思是十个函数共用一个test函数产生的作用域,这样打印结果都是一样的(除非前面的函数在执行的时改变i的值,但这是不允许的)。
如何变成一对一的关系呢?
1. 循环里写一个立即执行函数,把数组的赋值语句套起来
循环产生的十个立即执行函数都是独一无二的,就相当于手写了十个立即执行函数
2. 循环的时候,十个立即执行函数就被执行完了
3. 每一次循环,变量 i 当做实参传给立即执行函数,每个立即执行函数都保存着不同的变量 i
function test(){ var arr = []; for(var i = 0; i < 10; i++){ (function(num){ // 形参num接收变量i arr[num] = function(){ console.log(num); } }(i));// 把变量i传给立即执行函数,这个i就是实参了 } return arr; } var myArr = test(); for(var j = 0; j < 10; j++){ myArr[j](); } /*------------------------- function test(){ var arr = []; for(var i = 0; i < 10; i++){ (function(j){ j = 0; arr[j] = function(){ console.log(j); }; }(i = 0)); (function(j){ j = 1; arr[j] = function(){ console.log(j); }; }(i = 1)); (function(j){ j = 2; arr[j] = function(){ console.log(j); }; }(i = 2)); (function(j){ j = 3; arr[j] = function(){ console.log(j); }; }(i = 3)); ...... } return arr; } --------------------------*/
保存在外边的十个函数,每个函数都对应一个立即执行函数产生的作用域,
十个函数里,第一个函数对应的是第一个立即执行函数的作用域,访问的是所对应的立即执行函数里的变量 i
这十个函数分别对应一个立即执行函数,这样是一对一的闭包关系,
循环产生的十个立即执行函数对应一个 test 函数,这是 一对一对十 的关系,不是 一对十 的关系了,关系不一样结果也一定不一样。