关于js闭包

本文首发于我的博客这是我的github,欢迎来访。

  闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数。
  之所以一个内部的函数可以访问其外部的变量,而且在其被返回或是调用时还可以访问,是因为这个内部函数的作用域链中包含外部函数的作用域。

知识储备

在了解闭包之前,先要熟悉以下几点:
  1. 首先要理解执行环境,执行环境定义了变量或函数有权访问的其他数据。
  2. 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
  3. 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入到一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
  4. 当某个函数被调用时,会创建一个执行环境及其相应的作用域链。然后使用arguments和其他命名参数的值来初始化函数的活动对象。在函数中,活动对象作为变量对象使用(作用域链是由每层的变量对象链起来的)。
  5. 在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,直到作用域链终点即全局执行环境。
  6. 作用域链的本质是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。


1.一般情况下

  不谈论闭包,一般的,从在全局执行环境创建一个函数开始。
  在创建一个函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数内部的[[Scope]]
  然后执行流进入这个函数,函数的执行环境被压入环境栈中,此函数执行环境的活动对象作为变量对象被创建并推入执行环境作用域链的前端。
  对这个例子中的函数而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。
  无论在什么时候在函数中访问变量时,会从作用域链搜索变量名。
  一般情况下,函数执行完,局部活动对象就会被销毁,内存中仅有全局作用域(里边只有全局执行环境的变量对象)。
  以下面这段代码为例:

function compare (value1, value2) {         
//创建一个预先包含全局变量对象的作用域链,保存在[[Scope]]
    if (value1 < value2) {
//访问函数变量时,即在代码最后一条语句执行过程中,会从作用域链前端开始搜索变量名
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
var result = compare(5, 10);
//执行流进入函数时,compare的执行环境压入环境栈
//compare执行环境的活动对象作为变量对象接到作用域链的前端
//函数执行完,compare执行环境弹出栈,compare活动对象销毁

如图,作用域链从0开始向后查找:


闭包1.1

2.产生闭包的情况下

如下是一个以属性名作为参数,按其属性的值对数据进行排序的函数:

function createComparisonFunction(propertyName) {     
    return function(object1,object2){         //返回一个匿名函数
        var value1=object1[propertyName];
        var value2=object2[propertyName];
        if(value1<value2){
            return -1;
        } else if (value1>value2){
            return 1;
        } else {
            return 0;
        }
    };
}
var data=[{name:"Zachary",age:28},{name:"Nicholas",age:29}];
data.sort(createComparisonFunction("name"));
console.log(data[0]);           //Object {name: "Nicholas", age: 29}
data.sort(createComparisonFunction("age"));
console.log(data[0]);           //Object {name: "Zachary", age: 28}

createComparisonFunction()函数和返回的匿名函数的作用域链如下图所示:

闭包2.1

  在匿名函数从createComparisonFunction()中被返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()中定义的所有变量。

更为重要的是:

  createComparisonFunction()函数在执行完毕后,其他活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。
  当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁,createComparisonFunction()的活动对象才会被销毁。

  例如以下代码,返回的匿名函数被保存在变量compareNames中,通过将compareNames设置为null来解除对匿名函数的引用,解除引用之后垃圾回收例程将会清除该匿名函数,随之该匿名函数的作用域链也会被销毁,则其作用域链上的其他作用域也会安全的销毁(全局作用域除外)。

var compareNames = createComparisonFunction("name");
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
compareNames = null;

3.经典的闭包实例

在学习闭包的时候我们很容易见到以下代码:

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

  如果不理解闭包,很容易认为这段代码输出的是1,2,3,4,5,每隔一秒输出一个。但是其实是以每秒一次的频率输出56。因为延迟函数的回调会在循环执行完之后再执行(即使setTimeout的第二个参数为0,由于事件循环的机制,回调函数依然会在循环结束后执行)。

之所以我们错误的认为它会输出1~5,是因为我们自己假设每个迭代在运行时都会给自己“捕获”一个i的副本。但是,根据作用域的工作原理,实际情况是尽管循环中五个函数是在各个迭代中分别定义的,但是它们都被封闭在同一个全局作用域中,即实际上只有一个i

下面我们尝试将其结果修改为输出1~5。那么下边的代码行不行呢?

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

  答案是不行。确实,上边的代码在每个迭代中都添加了一个独有的作用域,但是这个作用域是空的,没有对于i的定义,所以每次查找i的时候还是会向上查找,找到全局作用域中的i并输出。在循环执行完之后的i6,所以依然每次输出6。现在我们为每个小的作用域添加一个变量,其值为循环时当前的i的值。

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

  现在的结果已经和理想的情况一样了。ES6拥有更简洁的方法创建一个封闭的作用域:let,它会使一个块转换成一个可以被关闭的作用域(对于let定义的变量来说块就是封闭的)。所以你可以写成下边的代码:

for(let i=1; i<=5; i++) {         
    setTimeout( function timer() {
        console.log(i);
    }, i*1000);
}

4.利用闭包创建模块

我们看以下代码:

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
    return {
        doSomething: doSomething,
        doAnother: doAnother
    };
}
var foo = CoolModule();
foo.doSomething();               //cool
foo.doAnother();                 //1 ! 2 ! 3

这个模式就被称为模块,最常见的实现模块模式的方法通常被称为模块暴露,这里展示其变体。

模块模式需要具备两个必要条件:
  1.必须有外部的封闭函数(CoolModule()),该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。


  以上就是我在学习闭包时的一些总结,欢迎讨论。

参考资料:《JavaScript高级程序设计》
     《你不知道的JavaScript》

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

推荐阅读更多精彩内容