Javascript基础--闭包

写在前面

闭包是JavaScript中一个重要的概念,本文使用3w(what,why,how)原则总结一下闭包这个概念。

what,什么是闭包?

网上关于闭包的定义有很多,但大多过于繁杂。一种简洁的说法:闭包是一个有状态的函数(a stateful function)---- 首先,闭包是一个函数,它一定存在于外层函数内;其次,闭包“记住了”其外层函数拥有的变量,它能够访问外层函数的作用域。
下面的例子就形成了一个闭包:

function foo() {
    var data = "hello";
    function bar() {
        console.log(data);  // 打印 hello
    }
    return bar;
}
foo()();

函数bar就是一个闭包,它能够访问外层函数foo的变量data。当调用foo()()时,也就是调用了bar函数,将打印foo函数内部变量data的值。

why,为什么要用闭包,作用是什么?

闭包多用来提供“模块化”,起到“封装”代码的目的。
由于外部执行环境无法访问函数内部的变量,但可以通过将闭包暴露出来的方式,使外部获取对函数内部变量的访问权。
闭包的这个用法,类似于C++、Java等语言的公有函数(public),提供外界访问类的私有成员变量(private)的能力。
下面是一个闭包提供的例子:

function Person() {
  var name = "Joe";
  return {
    getName: function() {
      return name;
    },
    setName: function(newName) {
      name = newName;
    }
  }
}

var p = Person();
console.log(p.name);  // 打印undefined
console.log(p.getName()); // 打印Joe
p.setName('Mike');
console.log(p.getName()); // 打印Mike

Person函数的内部变量name只存在于Person的函数作用域内,无法被外界访问到,所以打印undefined。
通过闭包getName和setName,可以达到访问变量name的目的。

在es6中终于有了类的概念,上面的代码可以用类的方式改写:

class Person {
  constructor() {
    this.name = 'Joe';
  }
  getName() {
    return this.name;
  }
  setName(newName) {
    this.name = newName;
  }
}

var p = new Person();
console.log(p.name);  // 打印Joe
console.log(p.getName()); // 打印Joe
p.setName('Mike');
console.log(p.getName()); // 打印Mike

es6由于没有private概念,可以看到这里的name属性其实是可以被外界直接调用到的。

另外在回调函数(如callback,setTimeout)、异步执行的函数(如ajax请求)中,经常看到闭包的身影。闭包一定程度上简化了代码的写法。

how,闭包是如何形成的?

说起闭包的底层机制,就首先要搞懂js语言中的执行环境和作用域链的概念,这篇文章介绍了这些概念。
闭包的原理其实就是形成闭包的这个内层函数,在其自身的作用域链上,头结点(活动对象)是自身的变量对象,记录了自身内部的变量信息;而作用域链上的第二个节点是其外层函数的变量对象,记录了外层函数的变量信息----很显然,这个节点上保存的信息能够被闭包函数访问的到,闭包定义所说的“周围环境”,其实就是这个数据。
一般的,当外层函数执行完成后,js引擎会释放掉它的执行环境,作用域链等信息。但如果其内部含有闭包函数,由于闭包函数的作用域链上仍然引用着外层函数的变量对象(第二个节点),垃圾回收器判定这块内存仍然有在被使用(被闭包引用),就不会释放掉这块内存,所以即使外层函数执行完,我们依然能通过闭包函数,访问其外层函数的变量信息。经常有说闭包容易引起内存泄漏,也就是因为内存一直被闭包引用者,无法被垃圾回收导致的结果。
闭包的这种底层实现,有时候会引起看上去有点预想不到的结果,比如在for循环中。思考下面的例子,为什么打印出来的都是5呢?

function arrayFun() {
    var arr = [];
    for (var i = 0; i < 5; i++) {
        arr.push(function () {
            console.log(i);
        });
    }
    return arr;
}
var retArr = arrayFun();
for (var i = 0; i < 5; i++) {
    retArr[i]();   // 打印 5,5,5,5,5
}

由于在es6之前,只有全局作用域和函数作用域,i变量在函数arrayFun中,只有一份副本。那么闭包(此处是个匿名函数)在作用域链上引用的是同一个i值(i在执行完for循环后值变为5),当调用闭包时,访问作用域链上的i值,就都是5了。
在es6中引入了let和块作用域,i的值在块作用域中是不同的副本,所以就不会出现上面的情况了。仍然是这篇文章有代码实例,不再赘述。

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

推荐阅读更多精彩内容