关于闭包

本文章著作权归饥人谷_Lyndon和饥人谷所有,转载请注明出处。

闭包对于我而言是一个难点,但闭包又是一个很有用的知识点,很多高级应用都需要依赖闭包。
所以在参考一些文章加上大量练习后,我来写一写自己理解闭包的过程,首先是弄清楚以下几个知识点。


>>> Part 1. 变量的作用域

JS中,变量的作用域只有两种:全局作用域、函数作用域。对应的变量也只有两种:全局变量、局部变量。

函数内部可以直接读取全局变量。

var a = 1;
function f(){
    console.log(a);
}
f();  // 1

但是函数外部无法读取到函数内部的局部变量。

function f(){
    var a = 1;
}
console.log(a);  // Uncaught ReferenceError: a is not defined

这一个Part是比较好理解的。


>>> Part 2. 如何从外部读取到局部变量?

在禄永老师的公开课中,老师将从外部读取局部变量这一情况称作“伟大的逃脱”。总结而言,有两种方法来实现。

  • 返回值的方法:函数作为返回值
function f1(){
    var a = 1;
    function f2(){
        console.log(a);
    }
    return f2;
}
var result = f1();
result();  // 1

函数f2包裹在函数f1内,根据作用域链的原理:子对象会一级一级向上寻找父对象的变量,f1所有的局部变量都可以被f2访问到,反之则不行。因此只要把f2作为返回值,就可以在f1外部读取到其中的内部变量。

  • 句柄的方法:定义全局变量
var innerHandler = null;
function outerFunc(){
    var outerVar = 1;
    function innerFunc(){
        console.log(outerVar);
        var innerVar = 2;
    }
    innerHandler = innerFunc;
}
outerFunc();
innerHandler();  // 1

这一方法首先定义了一个值为null的全局变量innerHandler,然后让innerHandler等于函数内部的函数,函数内部的函数则可以通过作用域链访问到父对象的变量outerVar,之后在外部调用innerHandler的时候,就可以访问到outerFunc函数中的内部变量outerVar


>>> Part 3. 闭包

网络上有千万种对闭包的解释,其实闭包就是上面例子中的两个函数:f2以及innerFunc。书面解释就是:能够读取其他函数内部变量的函数

在JS中,因为父函数内部的子函数才能够读取局部变量,因此闭包的常见形式就是:定义在函数内部的函数。前者是后者的充分不必要条件。

一言以蔽之,闭包就是连接函数内部外部的渠道。


>>> Part 4. 对示例代码段的解答

  • 第一段代码
function outerFn() {
    console.log("Outer function");
    function innerFn() {
        var innerVar = 0;
        innerVar++;
        console.log("Inner function\t");
        console.log("innerVar = "+innerVar+"");
    }
    return innerFn;
}

var fnRef = outerFn();
fnRef();
fnRef();

var fnRef2 = outerFn();
fnRef2();
fnRef2();

在这一段代码当中,innerFn不是一个闭包,因为它并不需要读取其他函数的内部变量,唯一的变量innerVar就在innerFn函数内部。在第一个fnRef()之后,结果就是首先输出Outer function,然后输出Inner function,由于innerVar是函数innerFn的内部变量且自增,因此从0变为1,再输出innerVar = 1.

这时候需要明白,当再次运行fnRef()时,由于fnRef本身已经变成了函数innerFn,所以其输出结果就不再有Outer Function这一句,而是直接输出:Inner function以及innerVar = 1.原因是此时的innerVar是一个内部变量,其作用域限定在innerFn函数中,每次调用执行innerFn函数,innerVar都会被重写。

对于下面的fnRef2(),也是同理。最后的输出结果见下图:

  • 第二段代码
var globalVar = 0;
function outerFn() {
    console.log("Outer function");
    function innerFn() {
        globalVar++;
        console.log("Inner function\t");
        console.log("globalVar = " + globalVar + "");
    }
    return innerFn;
}

var fnRef = outerFn();
fnRef();
fnRef();

var fnRef2 = outerFn();
fnRef2();
fnRef2();

这里的globalVar是一个外部变量,也是一个全局变量,处于全局作用域下。所以当执行innerFn时,innerFn函数将会访问到一个每次都自增的全局作用域下的活动对象,因此输出的结果会从globalVar = 1一直到globalVar = 4.在执行间歇中,globalVar处于两个函数的作用域之外,天高地远谁也管不了,所以它的值会被保存在内存中,并不会立刻被抹去。最后的输出结果见下图:

  • 第三段代码
function outerFn() {
    var outerVar = 0;
    console.log("Outer function");
    function innerFn() {
        outerVar++;
        console.log("Inner function\t");
        console.log("outerVar = " + outerVar + "");
    }
    return innerFn;
}

var fnRef = outerFn();
fnRef();
fnRef();

var fnRef2 = outerFn();
fnRef2();
fnRef2();

闭包来临了,这里的fnRef是一个闭包innerFn函数,但是此时的变量outerVar来到了父函数的作用域内,不像之前一样处于子函数作用域内或者处于全局作用域下。可以发现,这和Part 2中的例子非常相似。

其原理是:外部函数的调用环境为相互独立的封闭闭包的环境,第二次的fnRef2调用outerFn没有沿用第一次调用fnRefouterVar的值,第二次函数调用的作用域创建并绑定了一个新的outerVar实例,两个闭包环境中的计数器是相互独立,不存在关联的。

进一步来说,在每个封闭闭包环境中,外部函数的局部变量会保存在内存中,并不会在外部函数调用后被自动清除。原因在于:outerFninnerFn的父函数,而innerFn被赋值给一个全局变量,因此innerFn始终在内存当中,而它又依赖于outerFn,所以outerFn也必须始终在内存中,不会再函数被调用后就被抹去,因此闭包也有一点点不好,有可能造成内存泄漏。

所以,结果应该是:outerVar = 1, outerVar = 2, outerVar = 1, outerVar = 2.结果如下图所示:

我写到这自己已经完全明白了,我现在要用自己的理解来理顺一下最经典的问题。


>>> Part 5. 理顺最经典问题

<div id="divTest">
    <span>0</span>
    <span>1</span>
    <span>2</span>
    <span>3</span>
</div>
<script>
    var spans = document.querySelectorAll("#divTest span");
    for(var i = 0; i < spans.length; i++){
        spans[i].onclick = function(){
            console.log(i);
        }
    }
</script>

最经典的问题是:为什么我点击任何数字,控制台的输出结果永远是4?

这里可使用作用域链来帮助理解,不妨将以上代码转化为:

// function只是传递给了NodeList类型对象中的元素却并未执行,因为后面无括号
spans[0] = function fn0(){console.log(i)};
spans[1] = function fn1(){console.log(i)};
spans[2] = function fn2(){console.log(i)};
spans[3] = function fn3(){console.log(i)};
globalContext = {
    AO: {
        i: undefined, // 0(fn0)1(fn1)2(fn2)3(fn3)4(终止循环)
        spans:[0], [1], [2], [3]
    },
    scope: null
}
fn0[[scope]] = globalContext.AO,
fn1[[scope]] = globalContext.AO,
fn2[[scope]] = globalContext.AO,
fn3[[scope]] = globalContext.AO

fn0Context = {
    AO:{
    },
    scope: fn0[[scope]]
}

fn1Context = {
    AO:{
    },
    scope: fn1[[scope]]
}

fn2Context = {
    AO:{
    },
    scope: fn2[[scope]]
}

fn3Context = {
    AO:{
    },
    scope: fn3[[scope]]
}

最后点击span元素的时候i早已变为4,因此永远输出4.

改进的方法可以使用闭包,也就是:

var spans = document.querySelectorAll("#divTest span");
for(var i = 0; i < spans.length; i++) {
    spans[i].onclick = function(i){
        return function (){
            console.log(i);
        }
    }(i);
}

这个闭包也可以用作用域链来理解:

globalContext = {
    AO:{
        i: undefined,
        spans: [0], [1], [2], [3]
    }
}
fn0.scope = globalContext.AO,
fn1.scope = globalContext.AO,
fn2.scope = globalContext.AO,
fn3.scope = globalContext.AO

fn0Context = {
    AO:{
        i: 0,
        function: anonymous
    }
    fn0[[scope]] = fn0.scope // globalContext.AO 
}

function_anonymousContext = {
    AO: {
    }
    function_anonymous[[scope]] = fn0Context.AO
}
...

>>> Part 6. 闭包的问题

如同刚才的分析一样,当涉及到闭包时,函数中的变量都会被保存在内存中,因此需要避免滥用闭包,否则就有可能导致内存泄露。


>>> 参考资料

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

推荐阅读更多精彩内容

  • 最近看了js高级程序,书上对于闭包的解释是:''闭包是指有权访问另一个函数作用域中的变量''.我觉得过于抽象,经过...
    _三月阅读 550评论 0 14
  • 卡尔维诺中文站留言板这个帖子专门用作卡尔维诺中文站的留言板,欢迎大家留言和提问。...阮一峰2007-01-04T...
    舟渔行舟阅读 316评论 0 1
  • 此学习笔记仅供参考,有不实之处请及时指出,以免误人子弟。欢迎吐槽与指教! ...
    瓜皮大大阅读 174评论 0 2
  • 1.什么是闭包? 有什么作用 闭包指有权访问另一个函数作用域的变量的函数。创建闭包的常见方式 是 在一个函数...
    JunVincetHuo阅读 1,366评论 0 2
  • 为什么写这篇文章:网上关于闭包的解释五花八门,很多人自己往往也未清楚闭包,就尝试用蹩脚的语言去描述它,而闭包是一个...
    泽拉丶阅读 354评论 0 0