闭包与立即执行函数

在学习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)不再必要了。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,302评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,563评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,433评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,628评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,467评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,354评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,777评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,419评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,725评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,768评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,543评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,387评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,794评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,032评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,305评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,741评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,946评论 2 336