浅谈Javascript闭包的一些理解

写这篇文章的原因很简单,临近毕业,不得不考虑找工作的问题,面临着就业的压力,开始在前辈的教育下,开始刷题之旅,从leetcode到牛客,从编程到客观题。无奈自己实在是真的渣,牛客上的Javascript客观题的准确率一度掉到了53%,很多一些经典的知识,如闭包,继承等都完全不知所云。于是从头开始系统学习一些不了解的知识,并在总结别人博客的基础上谈一下自己的理解,如有错误,还请在评论区留言纠正我。
这里感谢一下阮一峰大神,本文很大程度都借鉴了阮老师的学习Javascript闭包(Closure),查看原文请点击这里

1 变量的作用域

阮老师解释到,变量的作用域分为两种,全局变量和局部变量。这两个概念理解起来很容易,Javascript函数内部可以直接读取到全局变量,如下例所示。

var name = "John";//name定义在函数外部 此时的name是一个全局变量
function print(){
    console.log(name);//此时控制台输出John
}

Javascript函数外部不能读取函数内部的局部变量,如下例所示。

function print(){
    var name = "Jenny";//name定义在函数内部 此时的name是一个局部变量
}
console.log(name);//此时控制台输出undefined 因为无法在全局中找到name变量 

2 闭包简介

一般的,不能通过外部函数读取局部变量。但是,在实际开发中,我们有时候需要得到函数内的局部变量。解决这个问题,可以两种方法,方法一,在函数a的内部再定义一个函数b,来获取函数a的局部变量。

function print(){//此时print相当于函数a
    var name = "Jay";
    function print2(){//此时print2相当于函数b
        console.log(name);
    }
}

在上面的代码中,函数print2就被包括在函数print内部,这是print内部的所有局部变量,对print2都是可见的,但是反过来就不行,print2内部的局部变量,对print就是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope)。子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
——阮一峰

方法二,既然print2可以读取print中的局部变量,我们只需返回print2的值,就可以在print外部读取它的内部变量。

function print(){
    var name = "Jay";
    function print2(){
        console.log(name);
    }
    return print2;
}
var result = print();
result();//此时控制台输出Jay

这里的print2函数就是一个闭包。阮老师对闭包的理解是,闭包就是能够读取其他函数内部变量的函数。闭包的本质,就是将函数内部和函数外部连接起来供我们使用。

3 闭包的用途

闭包的最大用处有两个,一个是前面提到的可以读取函数 ,另一个就是让这些变量的值始终保持在内存中。

function print(){
    var num = 4399;
    add = function(){//这里add要写成全局函数,否则在调用的时候会说没有定义
        num += 1;
    }
    function print2(){
        console.log(num);
    }
    return print2;
}
var result = print();//result实际是闭包print2函数
result();//控制台输出4399
add();
result();//控制台输出4400,说明num一直保存在内存中,调用add函数后执行加1运算

注意:
1、闭包会是的函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。

4 几个例子

首先我们看下阮老师文章里面留下的两个思考题。

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            //console.log(this);//控制台输出Window {...}
            return this.name;
        }
    }
}
console.log(object.getNameFunc()());//控制台输出The Window

个人理解,object.getNameFunc()()指的是return function(){return this.name;}这个函数,可以把object.getNameFunc()整体看成一个函数A,即A()返回this.name,函数里面的this找不到就往上一层找,因此,this指的是Window对象。

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        var that = this;
        //console.log(this);//控制台输出Object{...}
        //console.log(that);//控制台输出Object{...}
        return function(){
            //console.log(this);//控制台输出Window{...}
            //console.log(that);//控制台输出Object{...}
            //return this.name;//控制台输出The Window
            return that.name;
        }
    }
}
console.log(object.getNameFunc()());//控制台输出My Object

这里一开始this赋值给了一个对象中的局部变量,此时这里的this和that指的都是Object对象。后面return函数里面this没有对其赋值,找不到this,所以只有在方法调用的时候,才能确定其指向(换言之,继续往上一层找),而that指的是Object对象,所以that.name输出"My Object"。
接下来看一个更让你头大的。下面这个例子是Zing大神给我耐心解答的时候举出的,觉得理解起来更有帮助。这里也要感谢下Zing大神

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            //console.log(this);//控制台输出Object{name : "I have a headache"}
            return this.name;
        }
    }
}
var test = {"name" : "I have a headache", b2 : object.getNameFunc()}
console.log(test.b2());//输出I have a headache

这里调用的时候test调用自己的b2方法,b2又调用了object的getNameFunc()方法。getNameFunc返回的this.name在这个函数中找不到this,因此需要在上一层找,于是就找到了test对象,这里this.name即test.name。

function f1(){
    var arr = new Array();
    for (var i = 0;i < 3;i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var test = f1();
console.log(test[0]());//控制台输出3
console.log(test[1]());//控制台输出3
console.log(test[2]());//控制台输出3

这道题就是我遇到的一个很经典的错题,当时我认为应该返回0,1,2,万万没想到,/(ㄒoㄒ)/~~,一个都没猜对。这里要注意,var test = f1();只是定义函数,真正执行方法的是test[0]()test[1]()test[2](),当这些方法执行的时候,i已经变成了3,因此控制台都会输出3。那么怎么修改一下才能让它输出0,1,2呢?这里给出三种方法。
方法一

function f1(){
    var arr = new Array();
    for (var i = 0;i < 3;i++){
        arr[i] = function(num){
            return function(){
                return num;
            }
        }(i);
    }
    return arr;
}
var test = f1();
console.log(test[0]());//控制台输出0
console.log(test[1]());//控制台输出1
console.log(test[2]());//控制台输出2

方法一给出的这种解法是我个人认为最难理解的一种,这里没有直接返回i,而是通过一个匿名函数,返回一个num值。在调用匿名函数的时候,传入一个变量i。由于函数是按值传递的,所以就会将变量i的值复制给参数num,而在匿名函数内部,又返回一个访问num的闭包。这样一来,就可以返回各自不同的数值。
下面看方法二,方法二是方法一的改进版,不过方法二比方法一理解起来要好一点。
方法二

function f1(){
    var arr = new Array();
    function test(num){
        return function(){
            return num;
        }
    }
    for (var i = 0;i < 3;i++){
        arr[i] = test(i);
    }
    return arr;
}
var test = f1();
console.log(test[0]());//控制台输出0
console.log(test[1]());//控制台输出1
console.log(test[2]());//控制台输出2

这里将test函数单独抽出来,个人认为比方法一看上去好理解一些,这里不再详细解释,请读者自行理解。
下面看方法三,方法三是前些日子看ES6时无意间看到的。

ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
——阮一峰《ECMAScript 6 入门》

方法三

function f1(){
    var arr = new Array();
    for (let i = 0;i < 3;i++){
        arr[i] =function(){
            return i;
        }
    }
    return arr;
}
var test = f1();
console.log(test[0]());//控制台输出0
console.log(test[1]());//控制台输出1
console.log(test[2]());//控制台输出2

let声明的变量只在本轮循环有效,所以每次循环的i都是一个新的变量。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为Javascript引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

5 总结

以上是我个人对Javascript闭包的一些理解,希望对各位有所帮助。写的不对的地方还请各位大神提出意见,我会加以改正。码字不易,请尊重作者版权,转载注明出处。
By BeLLESS 2017.4.7 9:26

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

推荐阅读更多精彩内容

  • 作用域和闭包是 JavaScript 最重要的概念之一,想要进一步学习 JavaScript,就必须理解 Java...
    劼哥stone阅读 1,170评论 1 13
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zock阅读 1,074评论 2 6
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zouCode阅读 1,270评论 0 13
  • JS的闭包真的是一个老生常谈的知识点了,无奈它并不是那么好掌握,但是它又是那么重要,很多高级应用的开发都会用到闭包...
    一木_qintb阅读 306评论 0 1
  • 有人说失眠多梦是不健康的表现,那我大概是个病人。 有好有坏,但是经历的每一帧都值得记住,从今天开始,每一晚的梦我都...
    清潭洞豹子阅读 137评论 0 0