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

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 函数,这是 一对一对十 的关系,不是 一对十 的关系了,关系不一样结果也一定不一样。



Leave a comment 0 Comments.

Leave a Reply

换一张