JavaScript闭包浅谈

文章参考:《彻底理解js中的闭包》 闭包是js开发惯用的技巧,什么是闭包?闭包指的是:能够访问另一个函数作用域的变量的函数。 清晰的讲:闭包就是一个函数,这个函数能够访问其他函数的作用域中的变量。

众所周知,Js的作用域分为两种,全局和局部,基于我们所熟悉的作用域链相关知识,我们知道在js作用域环境中访问变量的权利是由内向外的。即通常情况下,一个函数无法访问另一个函数内的变量。 那么我们想在一个函数内部也有限权访问另一个函数内部的变量该怎么办呢?闭包就是用来解决这一需求的,闭包的本质就是在一个函数内部创建另一个函数。

首先得知道闭包的3个特性:

  1. 函数嵌套函数;
  2. 函数内部可以引用函数外部的参数和变量;
  3. 参数和变量不会被垃圾回收机制回收。

下面我们将以闭包的两种主要形式来学习:

函数作为返回值

function a() {
  var name = 'dov';
  return function () {
    return name
  }
}
var b = a();
console.log(b()); //dov

在这段代码中,a()中的返回值是一个匿名函数,这个函数在a()作用域内部,所以它可以获取a()作用域下变量name的值,将这个值作为返回值赋给全局作用域下的变量b,实现了在全局变量下获取到局部变量中的变量的值。

再来看一个闭包的经典例子:

function fn() {
  var num = 3;
  return function () {
    var n = 0;
    console.log(++n);
    console.log(++num);
  }
}
var showFn = fn();
showFn(); //1 4
showFn(); //1 5

一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为fn的返回值被赋值给了showFn,这时候相当于showFn = function() {var n = 0 ... },并且匿名函数内部引用着fn里的变量num,所以变量num无法被销毁,而变量n是每次被调用时新创建的,所以每次showFn执行完后它就把属于自己的变量连同自己一起销毁,于是乎最后就剩下孤零零的num,于是这里就产生了内存消耗的问题。

再来看一个经典的例子 - 定时器和闭包。 写一个for循环,让它按照顺序打印出当前循环次数:

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i + '')
  }, 100)
}

// 输出了5次 5

按照预期它应该依次输出1 2 3 4 5,而结果它输出了五次5,这是为什么呢? 原来由于js是单线程的,所以在执行for循环的时候定时器setTimeout被安排到任务队列中排队等待执行,而在等待过程中for循环就已经在执行,等到setTimeout可以执行的时候,for循环已经结束,i的值也已经编程5,所以打印出来五个5。 那么我们为了实现预期结果应该怎么改这段代码呢?(ps:如果把for循环里面的var变成let,也能实现预期结果,但是这个是为什么呢?)

for (var i = 0; i < 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i + ' ')
    })
  }(i));
}

// 0
// 1
// 2
// 3
// 4

引入闭包来保存变量i,将setTimeout放入立即执行函数中,将for循环中的循环值i作为参数传递,100毫秒后同时打印出1 2 3 4 5。 那如果我们想实现每隔100毫秒分别依次输出数字,又该怎么改呢?

for (var i = 1; i < 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i)
    }, i * 100)
  })(i)
}

// 1
// 2
// 3
// 4

在这段代码中,相当于同时启动3个定时器,i*100是为4个定时器分别设置了不同的时间,同时启动,但是执行时间不同,每个定时器间隔都是100毫秒,实现了每隔100毫秒就执行一次打印的效果。

闭包作为参数传递

var num = 15;
var fn1 = function (x) {
  if (x > num) {
    console.log(x);
  }
}
void function (fn2) {
  var num = 100;
  fn2(30)
}(fn1)

// 30

在这段代码中,函数fn1作为参数传入立即执行函数中,在执行到fn2(30)的时候,30 作为参数传入fn1中,这时候if (x > num)中的num取的并不是立即执行函数中的num,而是取创建函数的作用域中的num这里函数创建的作用域是全局作用域下,所以num取的是全局作用域中的值 15,即 30 > 15,打印 30

最后总结一下闭包的优缺点: 优点:

  1. 保护函数内的变量安全,实现封装,防止变量流入其他环境发生命名冲突;
  2. 在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存);
  3. 匿名自执行函数可以减少内存消耗。

缺点:

  1. 其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
  2. 其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响。