闭包与立即执行函数

在学习js的过程中,我们都会遇到闭包和立即执行函数的相关概念,今天就这两个概念做一个大致的整理。

本文结构:

  • 闭包
    1. 闭包的概念
  • 立即执行函数
    1. 什么是立即执行函数
    2. 一个经典面试题
    3. es6 中的 let

一、闭包

我们先来看闭包的作用:闭包通常用来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。

对于不理解闭包的朋友来说,上面这句话并没有什么用。所以我们通过一个简单的例子来理解这个概念。

比如说我们现在要写一个游戏,每个人初始都有5点精力,赢了就可以加1点精力,输了就要减1点精力。

var amount=5;  //初始有5点精力
function add(){  //增加精力
  amount+=1;
  return amount;
}
function minus(){  //减少精力
  amount-=1;
  return amount;
}

//第一局赢了,调用增加精力函数
add();console.log(amount);  //结果为6

//第二局输了,调用减少精力函数
minus();console.log(amount);  //结果为5

这样就初步实现了我们想要的效果。

但是,这种做法存在这样一个问题: 由于amount暴露在全局作用域下,是一个全局变量,所以任何人都可以随意修改我的精力值。如果我们不小心在另一个js文件里也声明了amount这样一个全局变量并给它赋值,那么我们这里的amount就可能被覆盖。

我们当前不希望amount被覆盖,我们希望amount能“私有”一些,因此,我们对amount提出了两点要求:

  1. 它不能被外部随意修改;
  2. 需要能通过指定的方式去操作它。

首先,我们把上述代码包在一个函数里:

function xxx(){
  var amount=5;
  function add(){
    amount+=1;
    return amount;
  }
  function minus(){
    amount-=1;
    return amount;
  }
}

这样,amount的作用域就变为了函数xxx的内部,在函数外面无法访问,这样就实现了“不能被外部随意修改”第一个要求。

但是,在函数xxx的外部,我们完全操作不到amount。因此,我们还需要给函数xxx留一个接口,让我们能够在函数外部去操作它。这该怎么办呢?

经过一番苦思冥想,我们终于想到,是否可以利用函数的返回值呢?

function xxx(){
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);  //实时打印amount的当前值
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);  //实时打印amount的当前值
    return amount;
  }
  return add;  //为该函数添加一个返回值,返回add函数
}

然后调用函数xxx:

var outer_add=xxx();
outer_add();   //结果为6
outer_add();   //结果为7

我们发现,amount的值改变了,我们实现了我们想要实现的效果。

当调用函数xxx时,我们将xxx内部的add函数作为操作amount的一个接口返回,outer_add函数和xxx内部的add函数实际上是“一模一样”的(因为它们都指向同一块内存),而函数add可以访问amount,因此我们可以通过outer_add来访问amount。

需要注意的是,由于内部函数add在被访问后一直处于被引用状态,不能够被垃圾回收。一旦我们不再使用outer_add函数,可以将其设置为null ,使其内存得到释放。

实际上,这就是闭包的典型用法。现在我们再回头看看开头提到的闭包的作用,是否理解一些了呢?

细心的朋友可能已经发现了,按这种做法,我们只能返回一个函数add,那函数minus怎么办呢?

我们这样修改:

function xxx(){
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  return {
    outer_add:add,
    outer_minus:minus
  }
}

然后调用函数xxx:

var outer=xxx();
outer.outer_add();   //结果为6
outer.outer_add();   //结果为7
outer.outer_minus();   //结果为6

这样,我们虽然不能直接操作amount这个局部变量,但可以通过函数xxx暴露出的函数outer_add和outer_minus来间接操作amount。

我们也可以这样修改:

function xxx(){
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  window.outer_add=add;  //暴露outer_add函数,可间接访问amount
  window.outer_minus=minus;  //暴露outer_minus函数,可间接访问amount
}

然后调用函数xxx:

xxx();
outer_add();   //结果为6
outer_add();   //结果为7
outer_minus();   //结果为6

结果相同。


二、立即执行函数

1、什么是立即执行函数

立即执行函数,顾名思义,声明一个函数,并立即执行它。在上面的代码中,我们定义了一个函数xxx,并调用了它。这个函数用于页面初始化,并且只会调用一次,因此我们可以使用立即执行函数来代替函数xxx。

改写代码如下:

function(){  //声明了一个匿名函数
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  window.outer_add=add;  //暴露outer_add函数,可间接访问amount
  window.outer_minus=minus;  //暴露outer_minus函数,可间接访问amount
}();  //立即调用这个匿名函数

但是我们发现,浏览器竟然报错了。

首先我们区分一下函数声明和函数表达式:区分函数声明和函数表达式最简单的方法是看function关键字出现在声明中的位置。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

所以,上述代码之所以报错,是因为浏览器认为函数声明后再加 “(” 是错误的。所以我们需要通过加()、+、-、~、!等运算符,使上面的代码变为函数表达式,这样就不会报错了,如:

(function(){
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  window.outer_add=add;  //暴露outer_add函数,可间接访问amount
  window.outer_minus=minus;  //暴露outer_minus函数,可间接访问amount
}());

然后我们就可以操作outer_add和outer_minus了:

outer_add();   //结果为6
outer_add();   //结果为7
outer_minus();   //结果为6

这就是立即执行函数的作用,创建一个局部作用域,使不想暴露在全局的变量变为局部变量。

2、一个经典面试题

题目如下:

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

结果:1s后,连续打印出5个5。

但是我们的本意是1s后分别打印出0、1、2、3、4,这时,我们就可以利用闭包和立即执行函数:

for(var i=0;i<5;i++){
  !function(j){
    setTimeout(function(){
      console.log(j);
    },1000);
  }(i);
}

结果:1s后,连续打印出0、1、2、3、4。

3、es6中的let

因为let声明的变量是块级作用域变量(而var声明的变量为函数作用域变量),因此,我们可以利用let的特性来创建一个局部作用域,从而取代立即执行函数:

{  //用块级作用域{}代替立即执行函数
  let amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  window.outer_add=add;  //暴露outer_add函数,可间接访问amount
  window.outer_minus=minus;  //暴露outer_minus函数,可间接访问amount
}

然后我们就可以操作outer_add和outer_minus了:

outer_add();   //结果为6
outer_add();   //结果为7
outer_minus();   //结果为6

块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。

这篇博客就先整理到这里。由于个人水平有限,博客错误之处,烦请指正!