19

image

一、什么是闭包

  MDN中对闭包有以下定义:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

从上述定义中我们可以总结出4点(重点):

  • 1、闭包是在函数创建时创建的,即有函数创建就会生成闭包
  • 2、闭包其函数同一上下文中
  • 3、闭包包含该作用域下的所有变量/引用地址
  • 4、定义函数不会创建闭包,只有创建/执行函数同时才创建闭包;

注意:使用闭包时一定要注意其作用域!

如下图:第1行到第9行,只涉及到变量的声明赋值函数的定义,所以不会有闭包产生;当代码执行到第10行,执行函数fn创建函数实例,因此会伴随着创建该作用域的一个闭包,闭包中包含变量a和b;当第10行执行完,函数实例销毁(函数内部没有引用外部变量),闭包也就随之销毁。
在这里插入图片描述

二、函数引用外部变量后的闭包

  “一”中代码实例主要介绍了闭包和函数的创建,如果仅仅是这些描述,我们也没有必要去了解和使用闭包;但是,当函数中“引用”(借用)了外部的变量后,一切都变得精彩了:
代码分析:

function makeAdder() {
  var sum = 0
  return function(y) {
    sum += y;
    return sum
  };
}

var add1 = makeAdder();
var add2 = makeAdder();

console.log(add1(1));  // 1
console.log(add1(1));  // 2

console.log(add2(2));  // 2
console.log(add2(2));  // 4

分析下图:

  • 1、1-7行定义了makeAdder函数,并将函数定义存储在内存中(蓝色圈圈);
  • 2、第9行,调用makerAdder定义执行1-7行代码,声明并赋值sum;将3-6行函数定义返回并赋值给变量add1,同时创建对应闭包,存放sum变量,且值为0;第9行执行完毕,销毁本地执行上下文和sum变量,控制权交给调用上下文;
  • 3、第10行,调用makerAdder定义执行1-7行代码,声明并赋值sum;将3-6行函数定义返回并赋值给变量add2,同时创建对应闭包,存放sum变量,且值为0;第10行执行完毕,销毁本地执行上下文和sum变量,控制权交给调用上下文;
  • 4、由于add1和add2创建两个新的函数实例,所以其相对应闭包是相互不影响的;
  • 5、执行到12行,调用add1实例并执行函数(3-6行),传入参数y为1,执行sum += y ,在查找本地或全局执行上下文之前,让我们检查一下闭包,结果闭包包含一个名为sum的变量,sum变量从0变为1,同时返回sum,最终打印出1。执行完毕,销毁本地执行上下文;
  • 6、执行到13行,调用add1实例并执行函数(3-6行),传入参数y为1,执行sum += y ,在查找本地或全局执行上下文之前,让我们检查一下闭包,结果闭包包含一个名为sum的变量,sum变量从1变为2,同时返回sum,最终打印出2。执行完毕,销毁本地执行上下文;
  • 7、执行到15行,调用add2实例并执行函数(3-6行),传入参数y为1,执行sum += y ,在查找本地或全局执行上下文之前,让我们检查一下闭包(此时的闭包和add1的闭包处于不同函数实例,故相互不不影响),结果闭包包含一个名为sum的变量,sum变量从0变为2,同时返回sum,最终打印出2。执行完毕,销毁本地执行上下文;
  • 8、执行到16行,调用add2实例并执行函数(3-6行),传入参数y为1,执行sum += y ,在查找本地或全局执行上下文之前,让我们检查一下闭包,结果闭包包含一个名为sum的变量,sum变量从2变为4,同时返回sum,最终打印出4。执行完毕,销毁本地执行上下文;

在这里插入图片描述
  在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量闭包的概念并不重要。当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。

三、循环中闭包让人很意外

  标题所说的循环是指在for循环的代码块中使用闭包所带来的的烦恼:

function starfunc() {
  var tipText = [
      'hello tom',
      'hello jerry',
      'hello jack'
    ];

  for (var i = 0; i < tipText.length; i++) {
    var item = tipText[i];
    setTimeout(() => {
      console.log(item);
    }, 100);
  }
}

starfunc();

  来,猜一猜最终会输出什么?按照期望,我们想最终打印出来的是hello tom;hello jerry;hello jack,但是实际上会连续打印三次hello jack为什么呢?这里会涉及到执行上下文(全局作用域、函数作用域、块级作用域)和事件循环相关内容(传送门)(默认大家都会哈)。
在这里插入图片描述
因为for循环体中变量 itemvar声明的,所以会变量提前starfunc函数的顶部,整个函数内部是一个函数作用域/执行上下文

  • 1、根据事件循环规则,执行到第10行时,setTimeout进入调用栈等待,回调函数会进入Event Table执行,在回调函数创建时生成闭包,将item存入;
  • 2、当i0时,闭包中itemhello tom
  • 3、for循环继续,当i1时,闭包中itemhello jerry
  • 4、for循环继续,当i2时,闭包中itemhello jack
  • 5、循环结束主线程执行完毕出现空闲时间,调用栈Event Queue中依次打印三次item,而item则是从闭包中获取,这就是为什么最终输出三遍hello jack

那有什么方法解决呢,请看代码:
在这里插入图片描述
for循环体中变量 item 使用let声明,此时starfunc函数是一个函数作用域,而每次for循环时,都会创建一个块级作用域,产生不同的执行上下文,各个上下文单独管理自己的变量:

  • 1、根据事件循环规则,执行到第10行时,setTimeout进入调用栈等待,回调函数会进入Event Table执行,在回调函数创建时生成闭包,将item存入;
  • 2、当i0时,单独作用域产生自己的执行上下文,该上下文对应闭包中itemhello tom
  • 3、for循环继续,当i1时,单独作用域产生自己的执行上下文,该上下文对应闭包中itemhello jerry
  • 4、for循环继续,当i2时,单独作用域产生自己的执行上下文,该上下文对应闭包中itemhello jack
  • 5、循环结束主线程执行完毕出现空闲时间,调用栈Event Queue中依次打印三次item,而item则是从三个不同互不影响的闭包中获取,最终输出hello tom;hello jerry;hello jack

四、闭包的性能

  如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度内存消耗方面对脚本性能具有负面影响。例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
示例:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

在上面的代码中,并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

五、小结

  闭包的使用涉及到很多方面,例如框架中数据的双向绑定函数的私有属性等;了解闭包的相关知识一是为了在开发中避免因为闭包带来的不利影响、二则是要善于利用闭包的特性解决实际问题


哇喔WEB
156 声望1.3k 粉丝

欢迎交流学习