Go to comments

JavaScript 闭包

闭包之前复习一下之前的知识



一个函数能产生一个东西叫作用域,并且这个作用域就是函数的一个属性叫 [[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

pic1589642278425913.jpg

第二图

由于 a() 函数的执行产生了函数 b 的定义,函数 的定义拿的是函数 a() 执行的劳动成果,并且函数 b 定义的 [[scope]] 和a函数执行时候的 [[scope]] 是一样的,他们指向的是同样的房间。

由于 b 函数是被定义的状态,他等待被执行,还没生成自己的AO。

002.jpg

第三图

直到 a 函数执行完了 b 函数也没有被执行,但 a 函数执行完的状态是把 b 函数的引用  return b  到全局(全局里面的东西永远不会销毁,除非html文件执行完),并保存到的变量 demo 上后,a函数才彻底执行完了。


a函数执行完后把自己执行期上下文AO销毁( 把指向AO的房间的线砍断,a函数回归到被定义状态,所以指向GO的线不能剪断 )。但是非常尴尬的是在a函数销毁之前,b函数被保存出来了,保存在外部被变量demo存起来了。


b函数在全局范围保持着被定义的状态,被定义状态拿着a函数执行时的劳动成果。虽然a函数执行完后把线砍断了,但是b函数作用域链的线还指向着这个房间。

003.jpg

第四图

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。

004.jpg

这个过程代码在视觉上,函数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、内存泄漏

闭包会导致原有作用域链该释放时不释放,导致占用内存空间,这个过程叫做内存泄漏。


什么是内存泄漏?

正常来说是内存被占用,占用和泄露是什么关系呢?


比如,手里捧起一把沙子,沙子肯定会从手里面流失,手里剩的沙子变的越来越少了,

内存被占用和手里剩下的沙子是一个道理,内存被占用的越多剩的内存越少,剩的少就解释成像内存泄漏一样,只是说像泄漏了一样,他是反向理解的。泄漏的多了剩的就少了,换句话说占用的多了剩的也少了,这是内存泄漏的意思。


内存泄漏是一个计算机名词,把过多的占用内存当成一种内存被泄漏,泄露的多剩的就少。把过多的占用系统资源叫做内存泄漏,这是比喻的一个形式。

函数执行完销毁的机制就是为了省空间,由于闭包导致作用域链不能被释放,造成系统的空间过多的被占用,这是闭包产生的一个不好的效果。


闭包有不好的一点会导致内存泄漏,但也不能说完全都不好。在后期编程用高级应用的时候,应用闭包做了很多积极的事,包括系统模块化开发,它提供了很多帮助。


二、闭包的作用


作用一:实现公有变量

利用闭包实现一个不依赖外部变量,并且能反复执行的函数累加器,执行一次函数可以从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,然后函数 加加、函数 减减

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)

image.png


正常执行函数,就是在函数引用后面加一对小括号 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

image.png


当在函数表达式后面,加一对括号的时候,函数被立即执行了,或者说被马上执行了,执行结果是123

var test = function(){
  console.log(123);
}();

console.log(test); // undefined

再次引用test的时候,test不再代表一个函数了

image.png

一旦一个表达式被执行符号执行后,就失去了对原来函数的索引,就会自动放弃函数的名称。


这种写法就和立即执行函数没什么太大区别,函数被执行一次后,函数就被永久的放弃了

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);
    
}();

先变量声明再把函数赋到test里面去,这是两个步骤。

把这个函数赋到test里面的过程叫做表达式,所以在执行的时候会放弃这个函数,而储存引用到test里面。

四、实例、用闭包解决闭包

1). for循环十次,每次循环给数组的一个单元加上一个函数 值

2). return数组保存到全局,数组里面的十个函数都和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]是执行语句,
		// function(){ document.write(i + "<br/>") }后面的是执行语句
		// 前面后面是有区别的

		arr[i] = function(){
            document.write(i + "<br/>");
        }
    }

    return arr;
}

***/

理想的效果是打印0至9,实际效果打印的是十个数字10,为什么会出现这样的效果?

因为数组里面的十个函数同时被保存到了外部,这十个函数都和test形成闭包。


也就是这十个函数都保存了同一个test的劳动成果,test劳动成果里面包含一个变量 i,在这十个函数在被保存出去之前 i 已变成了10,i变成了10之后这十个函数才被保存出去,

这十个函数不是在定义的时候执行,是被保存到外部执行,所以访问的 i 打印出来是10。

如何用闭包解决闭包?

这个闭包的特点是一对十,十个函数共用一个作用域,所以打印变量i 的结果都是10。一对十的意思是十个函数共用一个test函数产生的作用域,这样打印结果都是一样的(除非前面的函数在执行的时改变i的值,但这是不允许的)。


如何变成一对一的关系呢?

1). 循环里写一个立即执行函数,把数组赋值的语句套起来。立即执行函数执行完就销毁,所以循环产生十个的立即执行函数都是独一无二的

2). 把变量i 当做实参传给立即执行函数,每个立即执行函数都保存着不同的变量i 

function test(){
    var arr = [];     
    for(var i = 0; i < 10; i++){

        // 形参num接收变量i
        (function(num){
            arr[num] = function(){
                console.log(num);
            }
        }(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函数,这是 一对一对十 的关系不是 一对十 的关系了,关系不一样结果也一定不一样。



Leave a comment 0 Comments.

Leave a Reply

换一张